180 lines
4.8 KiB
JavaScript
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 };
|
|
}
|