183 lines
6.7 KiB
JavaScript
183 lines
6.7 KiB
JavaScript
"use strict";
|
||
|
||
// Texture decoding for the UI Image Set editor: DDS (DXT1/DXT3/DXT5, 32/24/16-bit RGB masks) and
|
||
// TGA (true-colour, RLE) → ImageData. BMP/PNG/JPG go through the browser (createImageBitmap).
|
||
|
||
const TexDecode = (() => {
|
||
const rgb565 = (c) => [((c >> 11) & 31) * 255 / 31, ((c >> 5) & 63) * 255 / 63, (c & 31) * 255 / 31];
|
||
|
||
/** One 4×4 DXT colour block → 16 RGB(A) values into out (at pixel x,y). */
|
||
function colorBlock(src, o, out, w, h, x, y, dxt1, alphaSrc) {
|
||
const c0 = src[o] | (src[o + 1] << 8);
|
||
const c1 = src[o + 2] | (src[o + 3] << 8);
|
||
const p0 = rgb565(c0);
|
||
const p1 = rgb565(c1);
|
||
const pal = [p0, p1];
|
||
if (!dxt1 || c0 > c1) {
|
||
pal.push([(2 * p0[0] + p1[0]) / 3, (2 * p0[1] + p1[1]) / 3, (2 * p0[2] + p1[2]) / 3]);
|
||
pal.push([(p0[0] + 2 * p1[0]) / 3, (p0[1] + 2 * p1[1]) / 3, (p0[2] + 2 * p1[2]) / 3]);
|
||
} else {
|
||
pal.push([(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, (p0[2] + p1[2]) / 2]);
|
||
pal.push(null); // transparent black (DXT1 1-bit alpha)
|
||
}
|
||
const bits = src[o + 4] | (src[o + 5] << 8) | (src[o + 6] << 16) | (src[o + 7] << 24);
|
||
for (let i = 0; i < 16; i++) {
|
||
const px = x + (i & 3);
|
||
const py = y + (i >> 2);
|
||
if (px >= w || py >= h) continue;
|
||
const c = pal[(bits >>> (2 * i)) & 3];
|
||
const d = (py * w + px) * 4;
|
||
if (!c) {
|
||
out[d] = out[d + 1] = out[d + 2] = out[d + 3] = 0;
|
||
continue;
|
||
}
|
||
out[d] = c[0];
|
||
out[d + 1] = c[1];
|
||
out[d + 2] = c[2];
|
||
out[d + 3] = alphaSrc ? alphaSrc[i] : 255;
|
||
}
|
||
}
|
||
|
||
function dxt(src, offset, w, h, kind) {
|
||
const out = new Uint8ClampedArray(w * h * 4);
|
||
const blockBytes = kind === "DXT1" ? 8 : 16;
|
||
let o = offset;
|
||
const alpha = new Uint8Array(16);
|
||
for (let y = 0; y < h; y += 4) {
|
||
for (let x = 0; x < w; x += 4) {
|
||
let a = null;
|
||
if (kind === "DXT3") {
|
||
for (let i = 0; i < 8; i++) {
|
||
const b = src[o + i];
|
||
alpha[i * 2] = (b & 15) * 17;
|
||
alpha[i * 2 + 1] = (b >> 4) * 17;
|
||
}
|
||
a = alpha;
|
||
} else if (kind === "DXT5") {
|
||
const a0 = src[o];
|
||
const a1 = src[o + 1];
|
||
const pal = [a0, a1];
|
||
if (a0 > a1) for (let i = 1; i < 7; i++) pal.push(((7 - i) * a0 + i * a1) / 7);
|
||
else {
|
||
for (let i = 1; i < 5; i++) pal.push(((5 - i) * a0 + i * a1) / 5);
|
||
pal.push(0, 255);
|
||
}
|
||
// 48 bits of 3-bit indices
|
||
let lo = src[o + 2] | (src[o + 3] << 8) | (src[o + 4] << 16);
|
||
let hi = src[o + 5] | (src[o + 6] << 8) | (src[o + 7] << 16);
|
||
for (let i = 0; i < 8; i++) alpha[i] = pal[(lo >> (3 * i)) & 7];
|
||
for (let i = 0; i < 8; i++) alpha[8 + i] = pal[(hi >> (3 * i)) & 7];
|
||
a = alpha;
|
||
}
|
||
colorBlock(src, o + (kind === "DXT1" ? 0 : 8), out, w, h, x, y, kind === "DXT1", a);
|
||
o += blockBytes;
|
||
}
|
||
}
|
||
return new ImageData(out, w, h);
|
||
}
|
||
|
||
const shiftOf = (mask) => {
|
||
if (!mask) return [0, 0];
|
||
let s = 0;
|
||
while (!((mask >>> s) & 1)) s++;
|
||
let n = 0;
|
||
while ((mask >>> (s + n)) & 1) n++;
|
||
return [s, n];
|
||
};
|
||
|
||
function masked(src, offset, w, h, bpp, masks, hasAlpha) {
|
||
const out = new Uint8ClampedArray(w * h * 4);
|
||
const bytes = bpp / 8;
|
||
const pitch = w * bytes;
|
||
const chans = masks.map(shiftOf);
|
||
const dv = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
||
for (let y = 0; y < h; y++) {
|
||
for (let x = 0; x < w; x++) {
|
||
const o = offset + y * pitch + x * bytes;
|
||
const v = bytes === 4 ? dv.getUint32(o, true) : bytes === 3 ? src[o] | (src[o + 1] << 8) | (src[o + 2] << 16) : dv.getUint16(o, true);
|
||
const d = (y * w + x) * 4;
|
||
for (let c = 0; c < 3; c++) {
|
||
const [s, n] = chans[c];
|
||
out[d + c] = n ? (((v >>> s) & ((1 << n) - 1)) * 255) / ((1 << n) - 1) : 0;
|
||
}
|
||
const [as, an] = chans[3];
|
||
out[d + 3] = hasAlpha && an ? (((v >>> as) & ((1 << an) - 1)) * 255) / ((1 << an) - 1) : 255;
|
||
}
|
||
}
|
||
return new ImageData(out, w, h);
|
||
}
|
||
|
||
function dds(bytes) {
|
||
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||
if (dv.getUint32(0, true) !== 0x20534444) throw new Error("Not a DDS file");
|
||
const h = dv.getUint32(12, true);
|
||
const w = dv.getUint32(16, true);
|
||
const pf = dv.getUint32(80, true);
|
||
const fourCC = String.fromCharCode(bytes[84], bytes[85], bytes[86], bytes[87]);
|
||
const offset = 128;
|
||
if (pf & 4) {
|
||
if (fourCC === "DXT1" || fourCC === "DXT3" || fourCC === "DXT5") return dxt(bytes, offset, w, h, fourCC);
|
||
if (fourCC === "DXT2" || fourCC === "DXT4") return dxt(bytes, offset, w, h, fourCC === "DXT2" ? "DXT3" : "DXT5");
|
||
throw new Error(`DDS format ${fourCC} isn't supported`);
|
||
}
|
||
const bpp = dv.getUint32(88, true);
|
||
const masks = [dv.getUint32(92, true), dv.getUint32(96, true), dv.getUint32(100, true), dv.getUint32(104, true)];
|
||
return masked(bytes, offset, w, h, bpp, masks, !!(pf & 1));
|
||
}
|
||
|
||
function tga(bytes) {
|
||
const idLen = bytes[0];
|
||
const type = bytes[2];
|
||
const w = bytes[12] | (bytes[13] << 8);
|
||
const h = bytes[14] | (bytes[15] << 8);
|
||
const bpp = bytes[16];
|
||
const topDown = (bytes[17] & 0x20) !== 0;
|
||
if (![2, 10].includes(type) || ![24, 32].includes(bpp)) throw new Error("Only true-colour TGA (24/32-bit) is supported");
|
||
const px = bpp / 8;
|
||
const out = new Uint8ClampedArray(w * h * 4);
|
||
let o = 18 + idLen;
|
||
let i = 0;
|
||
const put = (b, g, r, a) => {
|
||
const x = i % w;
|
||
const y = Math.floor(i / w);
|
||
const d = ((topDown ? y : h - 1 - y) * w + x) * 4;
|
||
out[d] = r;
|
||
out[d + 1] = g;
|
||
out[d + 2] = b;
|
||
out[d + 3] = a;
|
||
i++;
|
||
};
|
||
while (i < w * h) {
|
||
if (type === 2) {
|
||
put(bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255);
|
||
o += px;
|
||
continue;
|
||
}
|
||
const hdr = bytes[o++];
|
||
const n = (hdr & 127) + 1;
|
||
if (hdr & 128) {
|
||
const c = [bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255];
|
||
o += px;
|
||
for (let k = 0; k < n; k++) put(...c);
|
||
} else {
|
||
for (let k = 0; k < n; k++) {
|
||
put(bytes[o], bytes[o + 1], bytes[o + 2], px === 4 ? bytes[o + 3] : 255);
|
||
o += px;
|
||
}
|
||
}
|
||
}
|
||
return new ImageData(out, w, h);
|
||
}
|
||
|
||
/** Any supported texture → ImageBitmap. */
|
||
async function decode(bytes, ext) {
|
||
const e = (ext || "").toLowerCase();
|
||
if (e === "dds") return createImageBitmap(dds(bytes));
|
||
if (e === "tga") return createImageBitmap(tga(bytes));
|
||
return createImageBitmap(new Blob([bytes]));
|
||
}
|
||
|
||
return { decode, dds, tga };
|
||
})();
|
||
window.TexDecode = TexDecode;
|