// 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, }; }