LosaDev Tools: Lost Saga modding tools in the browser
This commit is contained in:
commit
c5e04472fb
105 files changed
+38541
No files matched your search
@@ -0,0 +1,182 @@
|
||||
"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;
|
||||
Reference in new issue
Block a user