296 lines
9.3 KiB
JavaScript
296 lines
9.3 KiB
JavaScript
// DFF (RenderWare Clump) reader.
|
|
//
|
|
// Layout: Clump -> FrameList -> GeometryList -> Geometry* -> MaterialList
|
|
// -> Atomic* (pairs a geometry with a frame)
|
|
//
|
|
// Only the parts a viewer needs are read: transforms, vertex data, triangles
|
|
// and material/texture names. Skinning, collision and 2dfx are skipped.
|
|
|
|
import { RW, Chunk, Reader, versionString } from './stream.js';
|
|
|
|
const FLAG_TRISTRIP = 0x0001;
|
|
const FLAG_TEXTURED = 0x0004;
|
|
const FLAG_PRELIT = 0x0008;
|
|
const FLAG_NORMALS = 0x0010;
|
|
const FLAG_TEXTURED2 = 0x0080;
|
|
const FLAG_NATIVE = 0x01000000;
|
|
|
|
function parseFrameList(chunk) {
|
|
const r = chunk.structReader();
|
|
const count = r.i32();
|
|
const frames = [];
|
|
for (let i = 0; i < count; i++) {
|
|
// 3x3 basis (right, up, at) then position - already column major for three.js.
|
|
const m = r.f32Array(12);
|
|
const parent = r.i32();
|
|
r.u32(); // matrix flags, unused
|
|
frames.push({
|
|
parent,
|
|
name: '',
|
|
matrix: [m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1],
|
|
});
|
|
}
|
|
// Each frame's extension follows the struct, in the same order.
|
|
let i = 0;
|
|
for (const ext of chunk.findAll(RW.EXTENSION)) {
|
|
if (i >= frames.length) break;
|
|
const nameChunk = ext.find(RW.FRAME_NAME);
|
|
if (nameChunk) frames[i].name = nameChunk.reader().fixedString(nameChunk.size);
|
|
i++;
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
function parseTexture(chunk) {
|
|
const r = chunk.structReader();
|
|
r.u16(); // filter mode
|
|
r.u16(); // uv addressing
|
|
const strings = chunk.findAll(RW.STRING);
|
|
const read = (c) => (c ? c.reader().fixedString(c.size) : '');
|
|
return { name: read(strings[0]), mask: read(strings[1]) };
|
|
}
|
|
|
|
function parseMaterial(chunk) {
|
|
const r = chunk.structReader();
|
|
r.i32(); // flags
|
|
const color = [r.u8() / 255, r.u8() / 255, r.u8() / 255, r.u8() / 255];
|
|
r.i32(); // unused
|
|
const isTextured = r.i32();
|
|
let ambient = 1, specular = 1, diffuse = 1;
|
|
if (chunk.version > 0x30400) {
|
|
ambient = r.f32();
|
|
specular = r.f32();
|
|
diffuse = r.f32();
|
|
}
|
|
const texChunk = chunk.find(RW.TEXTURE);
|
|
const texture = texChunk ? parseTexture(texChunk) : null;
|
|
return { color, isTextured: !!isTextured, ambient, specular, diffuse, texture };
|
|
}
|
|
|
|
function parseMaterialList(chunk) {
|
|
const r = chunk.structReader();
|
|
const count = r.i32();
|
|
const refs = [];
|
|
for (let i = 0; i < count; i++) refs.push(r.i32());
|
|
|
|
const chunks = chunk.findAll(RW.MATERIAL);
|
|
const materials = [];
|
|
let next = 0;
|
|
for (let i = 0; i < count; i++) {
|
|
// -1 means "a new material follows", anything else reuses an earlier one.
|
|
if (refs[i] >= 0 && materials[refs[i]]) materials.push(materials[refs[i]]);
|
|
else if (next < chunks.length) materials.push(parseMaterial(chunks[next++]));
|
|
else materials.push(parseMaterial(chunks[chunks.length - 1]));
|
|
}
|
|
return materials;
|
|
}
|
|
|
|
function parseGeometry(chunk) {
|
|
const r = chunk.structReader();
|
|
const format = r.u32();
|
|
const numTriangles = r.i32();
|
|
const numVertices = r.i32();
|
|
const numMorphTargets = r.i32();
|
|
if (chunk.version < 0x34000) {
|
|
r.f32(); r.f32(); r.f32(); // surface properties moved into the material later
|
|
}
|
|
|
|
if (format & FLAG_NATIVE) {
|
|
throw new Error('native (PS2/Xbox) geometry is not supported - use a PC dff');
|
|
}
|
|
|
|
let uvSets = (format & 0x00ff0000) >> 16;
|
|
if (!uvSets) uvSets = format & FLAG_TEXTURED2 ? 2 : format & FLAG_TEXTURED ? 1 : 0;
|
|
|
|
let colors = null;
|
|
if (format & FLAG_PRELIT) {
|
|
colors = new Float32Array(numVertices * 3);
|
|
for (let i = 0; i < numVertices; i++) {
|
|
colors[i * 3] = r.u8() / 255;
|
|
colors[i * 3 + 1] = r.u8() / 255;
|
|
colors[i * 3 + 2] = r.u8() / 255;
|
|
r.u8(); // prelit alpha, unused
|
|
}
|
|
}
|
|
|
|
let uvs = null;
|
|
for (let set = 0; set < uvSets; set++) {
|
|
const data = r.f32Array(numVertices * 2);
|
|
if (set === 0) uvs = data;
|
|
}
|
|
|
|
// Stored as (b, a, materialId, c) - the second vertex comes first and the
|
|
// material index sits between the vertices.
|
|
const tris = new Uint16Array(numTriangles * 3);
|
|
const triMaterial = new Uint16Array(numTriangles);
|
|
for (let i = 0; i < numTriangles; i++) {
|
|
const b = r.u16();
|
|
const a = r.u16();
|
|
triMaterial[i] = r.u16();
|
|
const c = r.u16();
|
|
tris[i * 3] = a;
|
|
tris[i * 3 + 1] = b;
|
|
tris[i * 3 + 2] = c;
|
|
}
|
|
|
|
let positions = null;
|
|
let normals = null;
|
|
for (let i = 0; i < numMorphTargets; i++) {
|
|
r.f32Array(4); // bounding sphere
|
|
const hasVertices = r.u32();
|
|
const hasNormals = r.u32();
|
|
const verts = hasVertices ? r.f32Array(numVertices * 3) : null;
|
|
const norms = hasNormals ? r.f32Array(numVertices * 3) : null;
|
|
if (i === 0) {
|
|
positions = verts;
|
|
normals = norms;
|
|
}
|
|
}
|
|
if (!positions) throw new Error('geometry has no vertex positions');
|
|
|
|
const matList = chunk.find(RW.MATLIST);
|
|
const materials = matList ? parseMaterialList(matList) : [];
|
|
|
|
// A skinned geometry stores its vertices in bind pose rather than in its
|
|
// frame's local space, so the viewer must not apply the frame transform.
|
|
const extension = chunk.find(RW.EXTENSION);
|
|
const skinned = !!(extension && extension.find(RW.SKIN));
|
|
|
|
// One draw group per material, so each group can get its own texture.
|
|
const order = [...Array(numTriangles).keys()].sort((x, y) => triMaterial[x] - triMaterial[y]);
|
|
let indices = new Uint32Array(numTriangles * 3);
|
|
let groups = [];
|
|
let current = null;
|
|
order.forEach((tri, slot) => {
|
|
indices[slot * 3] = tris[tri * 3];
|
|
indices[slot * 3 + 1] = tris[tri * 3 + 1];
|
|
indices[slot * 3 + 2] = tris[tri * 3 + 2];
|
|
const material = triMaterial[tri];
|
|
if (!current || current.material !== material) {
|
|
current = { start: slot * 3, count: 0, material };
|
|
groups.push(current);
|
|
}
|
|
current.count += 3;
|
|
});
|
|
|
|
// Many GTA:SA exporters leave the per-triangle material index at zero and put
|
|
// the real split in the BINMESH extension instead. The struct then yields a
|
|
// single group and the whole model renders with material 0's texture, so
|
|
// prefer BINMESH whenever it describes more submeshes than the struct did.
|
|
const binMesh = extension && extension.find(RW.BINMESH);
|
|
if (binMesh) {
|
|
const split = parseBinMesh(binMesh, numVertices);
|
|
if (split && split.groups.length > groups.length) {
|
|
indices = split.indices;
|
|
groups = split.groups;
|
|
}
|
|
}
|
|
|
|
return {
|
|
numVertices,
|
|
positions,
|
|
normals,
|
|
uvs,
|
|
colors,
|
|
indices,
|
|
groups,
|
|
materials,
|
|
skinned,
|
|
tristrip: !!(format & FLAG_TRISTRIP),
|
|
};
|
|
}
|
|
|
|
// The BINMESH extension holds the render-ready index lists, one submesh per
|
|
// material. flags bit 0 means the indices are tristrips rather than triangle
|
|
// lists; degenerate triangles are the standard way strips stitch together, so
|
|
// they are dropped rather than emitted as zero area faces.
|
|
function parseBinMesh(chunk, numVertices) {
|
|
const r = chunk.reader();
|
|
if (r.remaining < 12) return null;
|
|
const strip = r.u32() & 1;
|
|
const numMeshes = r.u32();
|
|
r.u32(); // total index count, recomputed below
|
|
const out = [];
|
|
const groups = [];
|
|
for (let i = 0; i < numMeshes; i++) {
|
|
if (r.remaining < 8) return null;
|
|
const count = r.u32();
|
|
const material = r.u32();
|
|
if (r.remaining < count * 4) return null;
|
|
const list = new Uint32Array(count);
|
|
for (let k = 0; k < count; k++) list[k] = r.u32();
|
|
|
|
const start = out.length;
|
|
if (strip) {
|
|
for (let k = 0; k + 2 < count; k++) {
|
|
const a = list[k], b = list[k + (k & 1 ? 2 : 1)], c = list[k + (k & 1 ? 1 : 2)];
|
|
if (a === b || b === c || a === c) continue; // strip stitch
|
|
out.push(a, b, c);
|
|
}
|
|
} else {
|
|
for (let k = 0; k + 2 < count; k += 3) out.push(list[k], list[k + 1], list[k + 2]);
|
|
}
|
|
if (out.length > start) groups.push({ start, count: out.length - start, material });
|
|
}
|
|
if (!groups.length) return null;
|
|
for (const index of out) {
|
|
if (index >= numVertices) return null; // not indices we can trust
|
|
}
|
|
return { indices: Uint32Array.from(out), groups };
|
|
}
|
|
|
|
export function parseDFF(buffer) {
|
|
const view = new DataView(buffer);
|
|
let clump = new Chunk(view, 0);
|
|
if (clump.type !== RW.CLUMP) {
|
|
// Some files carry a UV animation dictionary or similar before the clump.
|
|
clump = null;
|
|
for (let offset = 0; offset + 12 <= view.byteLength; ) {
|
|
const c = new Chunk(view, offset);
|
|
if (c.type === RW.CLUMP) { clump = c; break; }
|
|
if (c.size <= 0) break;
|
|
offset += 12 + c.size;
|
|
}
|
|
if (!clump) throw new Error('no clump chunk found - this is not a dff');
|
|
}
|
|
|
|
const r = clump.structReader();
|
|
const numAtomics = r.i32();
|
|
if (clump.version > 0x33000) {
|
|
r.i32(); // lights
|
|
r.i32(); // cameras
|
|
}
|
|
|
|
const frameList = clump.find(RW.FRAMELIST);
|
|
const frames = frameList ? parseFrameList(frameList) : [];
|
|
|
|
const geometryList = clump.find(RW.GEOMETRYLIST);
|
|
const geometries = [];
|
|
const errors = [];
|
|
if (geometryList) {
|
|
geometryList.findAll(RW.GEOMETRY).forEach((geo, i) => {
|
|
try {
|
|
geometries.push(parseGeometry(geo));
|
|
} catch (err) {
|
|
errors.push(`geometry ${i}: ${err.message}`);
|
|
geometries.push(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
const atomics = clump.findAll(RW.ATOMIC).map((atomic) => {
|
|
const a = atomic.structReader();
|
|
return { frame: a.i32(), geometry: a.i32(), flags: a.u32() };
|
|
});
|
|
|
|
return {
|
|
version: clump.version,
|
|
versionText: versionString(clump.version),
|
|
numAtomics,
|
|
frames,
|
|
geometries,
|
|
atomics,
|
|
errors,
|
|
};
|
|
}
|