SA Model Viewer base
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user