SA Model Viewer base
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
// 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]);
|
||||
const indices = new Uint32Array(numTriangles * 3);
|
||||
const 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;
|
||||
});
|
||||
|
||||
return {
|
||||
numVertices,
|
||||
positions,
|
||||
normals,
|
||||
uvs,
|
||||
colors,
|
||||
indices,
|
||||
groups,
|
||||
materials,
|
||||
skinned,
|
||||
tristrip: !!(format & FLAG_TRISTRIP),
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user