Files
model-viewer/public/js/rw/txd.js
T
2026-08-29 20:31:31 +01:00

180 lines
4.8 KiB
JavaScript

// TXD (RenderWare Texture Dictionary) reader.
//
// TextureDictionary -> TextureNative*, each holding a D3D8 or D3D9 raster.
// Everything is converted to RGBA8 at mip level 0.
import { RW, Chunk } from './stream.js';
import { decodeDXT } from './dxt.js';
const FORMAT_MASK = 0x0f00;
const FMT_1555 = 0x0100;
const FMT_565 = 0x0200;
const FMT_4444 = 0x0300;
const FMT_LUM8 = 0x0400;
const FMT_8888 = 0x0500;
const FMT_888 = 0x0600;
const FMT_555 = 0x0700;
const EXT_PAL8 = 0x2000;
const EXT_PAL4 = 0x4000;
function fourCCToDXT(fourCC) {
switch (fourCC) {
case 0x31545844: return 1; // 'DXT1'
case 0x33545844: return 3; // 'DXT3'
case 0x35545844: return 5; // 'DXT5'
default: return 0;
}
}
// D3D rasters are stored BGRA in memory, so red and blue are swapped.
function decodeUncompressed(data, width, height, format, palette, depth) {
const out = new Uint8Array(width * height * 4);
const pixels = width * height;
if (palette) {
for (let i = 0; i < pixels; i++) {
const p = data[i] * 4;
out[i * 4] = palette[p + 2];
out[i * 4 + 1] = palette[p + 1];
out[i * 4 + 2] = palette[p];
out[i * 4 + 3] = palette[p + 3];
}
return out;
}
for (let i = 0; i < pixels; i++) {
const dst = i * 4;
switch (format) {
case FMT_8888: {
const s = i * 4;
out[dst] = data[s + 2];
out[dst + 1] = data[s + 1];
out[dst + 2] = data[s];
out[dst + 3] = data[s + 3];
break;
}
case FMT_888: {
const s = i * (depth === 24 ? 3 : 4);
out[dst] = data[s + 2];
out[dst + 1] = data[s + 1];
out[dst + 2] = data[s];
out[dst + 3] = 255;
break;
}
case FMT_565: {
const c = data[i * 2] | (data[i * 2 + 1] << 8);
out[dst] = ((c >> 11) & 0x1f) * 255 / 31;
out[dst + 1] = ((c >> 5) & 0x3f) * 255 / 63;
out[dst + 2] = (c & 0x1f) * 255 / 31;
out[dst + 3] = 255;
break;
}
case FMT_1555:
case FMT_555: {
const c = data[i * 2] | (data[i * 2 + 1] << 8);
out[dst] = ((c >> 10) & 0x1f) * 255 / 31;
out[dst + 1] = ((c >> 5) & 0x1f) * 255 / 31;
out[dst + 2] = (c & 0x1f) * 255 / 31;
out[dst + 3] = format === FMT_555 || c & 0x8000 ? 255 : 0;
break;
}
case FMT_4444: {
const c = data[i * 2] | (data[i * 2 + 1] << 8);
out[dst] = ((c >> 8) & 0x0f) * 17;
out[dst + 1] = ((c >> 4) & 0x0f) * 17;
out[dst + 2] = (c & 0x0f) * 17;
out[dst + 3] = ((c >> 12) & 0x0f) * 17;
break;
}
case FMT_LUM8: {
out[dst] = out[dst + 1] = out[dst + 2] = data[i];
out[dst + 3] = 255;
break;
}
default:
out[dst] = out[dst + 1] = out[dst + 2] = 128;
out[dst + 3] = 255;
}
}
return out;
}
function parseTextureNative(chunk) {
const r = chunk.structReader();
const platform = r.u32();
r.u32(); // filter and uv addressing modes
const name = r.fixedString(32);
const mask = r.fixedString(32);
const rasterFormat = r.u32();
let dxt = 0;
let hasAlpha = false;
if (platform === 9) {
dxt = fourCCToDXT(r.u32());
} else {
hasAlpha = !!r.u32();
}
const width = r.u16();
const height = r.u16();
const depth = r.u8();
const numLevels = r.u8();
r.u8(); // raster type
const flags = r.u8();
if (platform === 9) {
hasAlpha = !!(flags & 1);
if (!(flags & 8)) dxt = 0;
} else {
dxt = flags; // D3D8 stores the compression mode here (0, 1 or 3)
}
const format = rasterFormat & FORMAT_MASK;
let palette = null;
if (rasterFormat & EXT_PAL8) palette = r.bytes(256 * 4);
else if (rasterFormat & EXT_PAL4) palette = r.bytes(32 * 4);
const size = r.u32();
const data = r.bytes(Math.min(size, r.remaining));
let rgba;
if (dxt) rgba = decodeDXT(data, width, height, dxt);
else if (rasterFormat & EXT_PAL4) rgba = null; // 4 bit palettes are not used by SA
else rgba = decodeUncompressed(data, width, height, format, palette, depth);
return {
name,
mask,
width,
height,
depth,
numLevels,
platform,
hasAlpha,
compression: dxt ? `DXT${dxt}` : palette ? 'PAL8' : `0x${format.toString(16)}`,
rgba,
};
}
export function parseTXD(buffer) {
const view = new DataView(buffer);
const dict = new Chunk(view, 0);
if (dict.type !== RW.TEXDICT) {
throw new Error(`not a txd (root chunk is 0x${dict.type.toString(16)})`);
}
const textures = new Map();
const errors = [];
dict.findAll(RW.TEXNATIVE).forEach((chunk, i) => {
try {
const tex = parseTextureNative(chunk);
if (tex.rgba) textures.set(tex.name.toLowerCase(), tex);
else errors.push(`${tex.name}: unsupported format ${tex.compression}`);
} catch (err) {
errors.push(`texture ${i}: ${err.message}`);
}
});
return { textures, errors };
}