@flighthq/scene3d-formats 0.2.1-next.840.857425c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/awd2Inflate.d.ts +4 -0
- package/dist/awd2Inflate.d.ts.map +1 -0
- package/dist/awd2Inflate.js +249 -0
- package/dist/awd2Inflate.js.map +1 -0
- package/dist/awd2Parse.d.ts +7 -0
- package/dist/awd2Parse.d.ts.map +1 -0
- package/dist/awd2Parse.js +1533 -0
- package/dist/awd2Parse.js.map +1 -0
- package/dist/awd2Schema.d.ts +44 -0
- package/dist/awd2Schema.d.ts.map +1 -0
- package/dist/awd2Schema.js +67 -0
- package/dist/awd2Schema.js.map +1 -0
- package/dist/gltfParse.d.ts +9 -0
- package/dist/gltfParse.d.ts.map +1 -0
- package/dist/gltfParse.js +1456 -0
- package/dist/gltfParse.js.map +1 -0
- package/dist/gltfPunctualLights.d.ts +3 -0
- package/dist/gltfPunctualLights.d.ts.map +1 -0
- package/dist/gltfPunctualLights.js +103 -0
- package/dist/gltfPunctualLights.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/md2Parse.d.ts +5 -0
- package/dist/md2Parse.d.ts.map +1 -0
- package/dist/md2Parse.js +408 -0
- package/dist/md2Parse.js.map +1 -0
- package/dist/md2Schema.d.ts +12 -0
- package/dist/md2Schema.d.ts.map +1 -0
- package/dist/md2Schema.js +193 -0
- package/dist/md2Schema.js.map +1 -0
- package/dist/md5AnimParse.d.ts +3 -0
- package/dist/md5AnimParse.d.ts.map +1 -0
- package/dist/md5AnimParse.js +385 -0
- package/dist/md5AnimParse.js.map +1 -0
- package/dist/md5Parse.d.ts +6 -0
- package/dist/md5Parse.d.ts.map +1 -0
- package/dist/md5Parse.js +635 -0
- package/dist/md5Parse.js.map +1 -0
- package/dist/mtlParse.d.ts +3 -0
- package/dist/mtlParse.d.ts.map +1 -0
- package/dist/mtlParse.js +256 -0
- package/dist/mtlParse.js.map +1 -0
- package/dist/objParse.d.ts +5 -0
- package/dist/objParse.d.ts.map +1 -0
- package/dist/objParse.js +464 -0
- package/dist/objParse.js.map +1 -0
- package/dist/sceneSkeleton.d.ts +3 -0
- package/dist/sceneSkeleton.d.ts.map +1 -0
- package/dist/sceneSkeleton.js +34 -0
- package/dist/sceneSkeleton.js.map +1 -0
- package/dist/shared.d.ts +18 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +143 -0
- package/dist/shared.js.map +1 -0
- package/dist/threeDsParse.d.ts +4 -0
- package/dist/threeDsParse.d.ts.map +1 -0
- package/dist/threeDsParse.js +718 -0
- package/dist/threeDsParse.js.map +1 -0
- package/package.json +55 -0
- package/src/awd2Inflate.test.ts +118 -0
- package/src/awd2Parse.test.ts +2431 -0
- package/src/gltfParse.test.ts +2339 -0
- package/src/gltfPunctualLights.test.ts +184 -0
- package/src/gltfTreeShaking.test.ts +45 -0
- package/src/md2Parse.test.ts +1033 -0
- package/src/md5AnimParse.test.ts +673 -0
- package/src/md5Parse.test.ts +1094 -0
- package/src/mtlParse.test.ts +244 -0
- package/src/objParse.test.ts +690 -0
- package/src/sceneSkeleton.test.ts +35 -0
- package/src/shared.test.ts +210 -0
- package/src/threeDsParse.test.ts +1089 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { createTransform3D } from '@flighthq/geometry';
|
|
2
|
+
import { reportImportDiagnostic } from '@flighthq/importdiagnostics';
|
|
3
|
+
import { createBlinnPhongMaterial } from '@flighthq/materials';
|
|
4
|
+
import { createMeshGeometry } from '@flighthq/mesh';
|
|
5
|
+
import { createScene3DFromDocument } from '@flighthq/scene3d';
|
|
6
|
+
import { ImportDiagnosticSeverity, MeshKind } from '@flighthq/types';
|
|
7
|
+
import { THREE_DS_CHUNK_HEADER_BYTES, THREE_DS_COLOR_BYTE, THREE_DS_COLOR_FLOAT, THREE_DS_EDITOR, THREE_DS_FACE_MATERIAL, THREE_DS_FACES, THREE_DS_MAIN, THREE_DS_MATERIAL, THREE_DS_MATERIAL_AMBIENT, THREE_DS_MATERIAL_BUMP_MAP, THREE_DS_MATERIAL_DIFFUSE, THREE_DS_MATERIAL_NAME, THREE_DS_MATERIAL_SHININESS, THREE_DS_MATERIAL_SPECULAR, THREE_DS_MATERIAL_TEXTURE_FILENAME, THREE_DS_MATERIAL_TEXTURE_MAP, THREE_DS_MATERIAL_TRANSPARENCY, THREE_DS_OBJECT, THREE_DS_PERCENT_FLOAT, THREE_DS_PERCENT_INT, THREE_DS_SMOOTH_GROUP, THREE_DS_TRIMESH, THREE_DS_UV_COORDS, THREE_DS_VERTICES, } from '@flighthq/types';
|
|
8
|
+
import { CANONICAL_FLOATS_PER_VERTEX, CANONICAL_LAYOUT, convertPositionsZUpToYUp, createExternalTextureRef, } from './shared';
|
|
9
|
+
// Parses an Autodesk 3DS binary file into a Scene3D. The 3DS format is a recursive chunk tree
|
|
10
|
+
// (little-endian): each chunk has a uint16 ID, a uint32 total length (including the 6-byte header),
|
|
11
|
+
// and a payload of sub-chunks and/or inline data. The editor chunk (0x3D3D) contains named objects
|
|
12
|
+
// (0x4000), each of which may contain a triangle mesh (0x4100) with vertex, face, and UV sub-chunks.
|
|
13
|
+
//
|
|
14
|
+
// Each mesh becomes a Mesh scene node with the canonical PBR vertex layout. The 3DS coordinate
|
|
15
|
+
// system is right-handed Z-up (like MD2/MD5); positions are converted to Flight's right-handed
|
|
16
|
+
// Y-up via convertPositionsZUpToYUp, a -90° rotation about X ((x, y, z) → (x, z, -y), det = +1).
|
|
17
|
+
// Because the conversion is a rotation, not a reflection, triangle winding and computed normals
|
|
18
|
+
// are preserved as-is from the file — no winding reversal is needed.
|
|
19
|
+
//
|
|
20
|
+
// The 3DS format limits each mesh to 65535 vertices (uint16 indices). Multiple mesh objects are
|
|
21
|
+
// common in practice and each becomes a separate Mesh child of the scene.
|
|
22
|
+
//
|
|
23
|
+
// Malformed or truncated input records a diagnostic and returns an empty or partial scene; the function
|
|
24
|
+
// never throws on bad input. Convenience over `createScene3DFromDocument(parse3ds(bytes))`.
|
|
25
|
+
export function createScene3DFrom3ds(bytes, diagnostics) {
|
|
26
|
+
return createScene3DFromDocument(parse3ds(bytes, diagnostics));
|
|
27
|
+
}
|
|
28
|
+
// Parses an Autodesk 3DS binary file into a format-neutral Scene3DDocument. Each named-object trimesh
|
|
29
|
+
// becomes one document Mesh node (inline geometry, canonical PBR layout, RH Z-up → Y-up). Referenced
|
|
30
|
+
// materials are registered into the document's materials table (deduped by name) and named per mesh by
|
|
31
|
+
// index. Assemble into a live Scene3D with `createScene3DFromDocument`. Malformed input returns an empty or
|
|
32
|
+
// partial document with a diagnostic.
|
|
33
|
+
export function parse3ds(bytes, diagnostics) {
|
|
34
|
+
const document = {
|
|
35
|
+
animations: [],
|
|
36
|
+
cameras: [],
|
|
37
|
+
lights: [],
|
|
38
|
+
materials: [],
|
|
39
|
+
meshes: [],
|
|
40
|
+
metadata: null,
|
|
41
|
+
nodes: [],
|
|
42
|
+
resources: [],
|
|
43
|
+
scenes: [{ rootNodes: [] }],
|
|
44
|
+
skins: [],
|
|
45
|
+
};
|
|
46
|
+
if (bytes.byteLength < THREE_DS_CHUNK_HEADER_BYTES) {
|
|
47
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, '3ds.input-too-small', 'parse3ds');
|
|
48
|
+
return document;
|
|
49
|
+
}
|
|
50
|
+
const source = bytes;
|
|
51
|
+
const view = new DataView(source.buffer, source.byteOffset, source.byteLength);
|
|
52
|
+
const mainId = view.getUint16(0, true);
|
|
53
|
+
if (mainId !== THREE_DS_MAIN) {
|
|
54
|
+
reportImportDiagnostic(diagnostics, ImportDiagnosticSeverity.Reject, '3ds.wrong-main-chunk', 'parse3ds', {
|
|
55
|
+
foundId: mainId,
|
|
56
|
+
});
|
|
57
|
+
return document;
|
|
58
|
+
}
|
|
59
|
+
// The material table (0xAFFF chunks) and the meshes are siblings under the editor chunk, and a mesh
|
|
60
|
+
// references its materials by name via FACE_MATERIAL — so collect the whole table first, then
|
|
61
|
+
// resolve each mesh's referenced names against it.
|
|
62
|
+
const threeDsDrops = diagnostics ? new Map() : null;
|
|
63
|
+
const materials = new Map();
|
|
64
|
+
const meshes = collectMeshes(view, 0, materials, threeDsDrops);
|
|
65
|
+
const materialIndexByName = new Map();
|
|
66
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
67
|
+
appendMeshDocument(meshes[i], materials, materialIndexByName, document, threeDsDrops);
|
|
68
|
+
}
|
|
69
|
+
// parse3ds is the single physical emitter for every aggregated crumb (hence the origin); the tallies
|
|
70
|
+
// store no origin. Flush once so per-chunk faults collapse to one crumb per kind/discriminator + count.
|
|
71
|
+
if (threeDsDrops !== null) {
|
|
72
|
+
for (const tally of threeDsDrops.values()) {
|
|
73
|
+
reportImportDiagnostic(diagnostics, tally.severity, tally.kind, 'parse3ds', {
|
|
74
|
+
...tally.detail,
|
|
75
|
+
count: tally.count,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return document;
|
|
80
|
+
}
|
|
81
|
+
// Recursively walks the chunk tree starting at `offset`, collecting all trimesh descriptors found
|
|
82
|
+
// within editor → object → trimesh sub-chunks, and populating `materials` with every material block
|
|
83
|
+
// (0xAFFF) found alongside them under the editor chunk.
|
|
84
|
+
function collectMeshes(view, offset, materials, threeDsDrops) {
|
|
85
|
+
const end = Math.min(offset + readChunkLength(view, offset), view.byteLength);
|
|
86
|
+
const meshes = [];
|
|
87
|
+
let cursor = offset + THREE_DS_CHUNK_HEADER_BYTES;
|
|
88
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
89
|
+
const chunkId = view.getUint16(cursor, true);
|
|
90
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
91
|
+
const chunkEnd = cursor + chunkLength;
|
|
92
|
+
if (chunkEnd > end) {
|
|
93
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.chunk-exceeds-parent', '', {
|
|
94
|
+
firstChunkId: chunkId,
|
|
95
|
+
firstLength: chunkLength,
|
|
96
|
+
firstOffset: cursor,
|
|
97
|
+
});
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (chunkId === THREE_DS_EDITOR || chunkId === THREE_DS_MAIN) {
|
|
101
|
+
const inner = collectMeshes(view, cursor, materials, threeDsDrops);
|
|
102
|
+
for (let i = 0; i < inner.length; i++)
|
|
103
|
+
meshes.push(inner[i]);
|
|
104
|
+
}
|
|
105
|
+
else if (chunkId === THREE_DS_OBJECT) {
|
|
106
|
+
const mesh = parseObject(view, cursor, chunkEnd, threeDsDrops);
|
|
107
|
+
if (mesh !== null)
|
|
108
|
+
meshes.push(mesh);
|
|
109
|
+
}
|
|
110
|
+
else if (chunkId === THREE_DS_MATERIAL) {
|
|
111
|
+
const material = parseMaterial(view, cursor, chunkEnd);
|
|
112
|
+
if (material.name.length > 0)
|
|
113
|
+
materials.set(material.name, material);
|
|
114
|
+
}
|
|
115
|
+
cursor = chunkEnd;
|
|
116
|
+
}
|
|
117
|
+
return meshes;
|
|
118
|
+
}
|
|
119
|
+
// Parses a named object chunk (0x4000). The payload starts with a null-terminated ASCII name string,
|
|
120
|
+
// followed by sub-chunks. If a trimesh sub-chunk (0x4100) is found, its vertex/face/UV data is
|
|
121
|
+
// returned as a ThreeDsMesh.
|
|
122
|
+
function parseObject(view, offset, end, threeDsDrops) {
|
|
123
|
+
let cursor = offset + THREE_DS_CHUNK_HEADER_BYTES;
|
|
124
|
+
const name = readNullTerminatedString(view, cursor, end);
|
|
125
|
+
cursor += name.length + 1; // advance past the name and null terminator
|
|
126
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
127
|
+
const chunkId = view.getUint16(cursor, true);
|
|
128
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
129
|
+
const chunkEnd = cursor + chunkLength;
|
|
130
|
+
if (chunkEnd > end) {
|
|
131
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.subchunk-exceeds-object', '', {
|
|
132
|
+
firstOffset: cursor,
|
|
133
|
+
});
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (chunkId === THREE_DS_TRIMESH) {
|
|
137
|
+
return parseTrimesh(view, cursor, chunkEnd, name, threeDsDrops);
|
|
138
|
+
}
|
|
139
|
+
cursor = chunkEnd;
|
|
140
|
+
}
|
|
141
|
+
// A named object with no trimesh sub-chunk (a 3DS light/camera/dummy object): Flight imports meshes only,
|
|
142
|
+
// so the object is not modeled — recognized but skipped.
|
|
143
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Skip, '3ds.non-mesh-object', '', { firstName: name });
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Parses a trimesh chunk (0x4100) and its sub-chunks (vertices, faces, UVs) into a ThreeDsMesh
|
|
147
|
+
// descriptor.
|
|
148
|
+
function parseTrimesh(view, offset, end, name, threeDsDrops) {
|
|
149
|
+
let vertices = null;
|
|
150
|
+
let faces = null;
|
|
151
|
+
let uvs = null;
|
|
152
|
+
let materialGroups = [];
|
|
153
|
+
let smoothingGroups = null;
|
|
154
|
+
let cursor = offset + THREE_DS_CHUNK_HEADER_BYTES;
|
|
155
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
156
|
+
const chunkId = view.getUint16(cursor, true);
|
|
157
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
158
|
+
const chunkEnd = cursor + chunkLength;
|
|
159
|
+
if (chunkEnd > end) {
|
|
160
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.subchunk-exceeds-trimesh', '', {
|
|
161
|
+
firstChunkId: chunkId,
|
|
162
|
+
firstName: name,
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
const dataStart = cursor + THREE_DS_CHUNK_HEADER_BYTES;
|
|
167
|
+
if (chunkId === THREE_DS_VERTICES) {
|
|
168
|
+
vertices = parseVertices(view, dataStart, chunkEnd, threeDsDrops);
|
|
169
|
+
}
|
|
170
|
+
else if (chunkId === THREE_DS_FACES) {
|
|
171
|
+
const parsed = parseFaces(view, dataStart, chunkEnd, threeDsDrops);
|
|
172
|
+
if (parsed !== null) {
|
|
173
|
+
faces = parsed.faces;
|
|
174
|
+
materialGroups = parsed.materialGroups;
|
|
175
|
+
smoothingGroups = parsed.smoothingGroups;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (chunkId === THREE_DS_UV_COORDS) {
|
|
179
|
+
uvs = parseUvCoords(view, dataStart, chunkEnd, threeDsDrops);
|
|
180
|
+
}
|
|
181
|
+
cursor = chunkEnd;
|
|
182
|
+
}
|
|
183
|
+
if (vertices === null || faces === null) {
|
|
184
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.mesh-missing-geometry', vertices === null ? 'vertices' : 'faces', { firstName: name, missing: vertices === null ? 'vertices' : 'faces' });
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return { faces, materialGroups, name, smoothingGroups, uvs, vertices };
|
|
188
|
+
}
|
|
189
|
+
// Reads the vertex list sub-chunk (0x4110): uint16 count followed by count * 3 float32 values
|
|
190
|
+
// (x, y, z per vertex).
|
|
191
|
+
function parseVertices(view, dataStart, end, threeDsDrops) {
|
|
192
|
+
if (dataStart + 2 > end) {
|
|
193
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.vertices-truncated', 'no-count', {
|
|
194
|
+
reason: 'no-count',
|
|
195
|
+
});
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const count = view.getUint16(dataStart, true);
|
|
199
|
+
const floatsNeeded = count * 3;
|
|
200
|
+
const bytesNeeded = dataStart + 2 + floatsNeeded * 4;
|
|
201
|
+
if (bytesNeeded > end) {
|
|
202
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.vertices-truncated', 'truncated', {
|
|
203
|
+
firstCount: count,
|
|
204
|
+
reason: 'truncated',
|
|
205
|
+
});
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const vertices = new Float32Array(floatsNeeded);
|
|
209
|
+
let offset = dataStart + 2;
|
|
210
|
+
for (let i = 0; i < floatsNeeded; i++) {
|
|
211
|
+
vertices[i] = view.getFloat32(offset, true);
|
|
212
|
+
offset += 4;
|
|
213
|
+
}
|
|
214
|
+
return vertices;
|
|
215
|
+
}
|
|
216
|
+
// Reads the face list sub-chunk (0x4120): uint16 count followed by count * 4 uint16 values
|
|
217
|
+
// (v0, v1, v2, flags per face). Only the first 3 values (triangle indices) are kept. Two sub-chunks
|
|
218
|
+
// follow the face array within the same chunk: FACE_MATERIAL (0x4130) — a material name plus the list of
|
|
219
|
+
// face indices that use it, one per material subset — and SMOOTH_GROUP (0x4150) — one uint32 smoothing
|
|
220
|
+
// bitmask per face. Returns the triangle indices, the per-material face groups, and the smoothing masks.
|
|
221
|
+
function parseFaces(view, dataStart, end, threeDsDrops) {
|
|
222
|
+
if (dataStart + 2 > end) {
|
|
223
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.faces-truncated', 'no-count', {
|
|
224
|
+
reason: 'no-count',
|
|
225
|
+
});
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const count = view.getUint16(dataStart, true);
|
|
229
|
+
const facesEnd = dataStart + 2 + count * 4 * 2;
|
|
230
|
+
if (facesEnd > end) {
|
|
231
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.faces-truncated', 'truncated', {
|
|
232
|
+
firstCount: count,
|
|
233
|
+
reason: 'truncated',
|
|
234
|
+
});
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const faces = new Uint16Array(count * 3);
|
|
238
|
+
let offset = dataStart + 2;
|
|
239
|
+
for (let i = 0; i < count; i++) {
|
|
240
|
+
faces[i * 3] = view.getUint16(offset, true);
|
|
241
|
+
faces[i * 3 + 1] = view.getUint16(offset + 2, true);
|
|
242
|
+
faces[i * 3 + 2] = view.getUint16(offset + 4, true);
|
|
243
|
+
// Skip the 4th uint16 (flags).
|
|
244
|
+
offset += 8;
|
|
245
|
+
}
|
|
246
|
+
// Sub-chunks (FACE_MATERIAL, SMOOTH_GROUP, …) follow the face array up to the chunk boundary.
|
|
247
|
+
const materialGroups = [];
|
|
248
|
+
let smoothingGroups = null;
|
|
249
|
+
let cursor = facesEnd;
|
|
250
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
251
|
+
const subId = view.getUint16(cursor, true);
|
|
252
|
+
const subLength = readChunkLength(view, cursor);
|
|
253
|
+
const subEnd = cursor + subLength;
|
|
254
|
+
if (subLength < THREE_DS_CHUNK_HEADER_BYTES || subEnd > end) {
|
|
255
|
+
// The faces survive; only trailing FACE_MATERIAL/SMOOTH_GROUP sub-chunks past this malformed one are
|
|
256
|
+
// abandoned — a break-keeps-parsed recovery, mirroring the chunk-exceeds guards in the sibling walks.
|
|
257
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.face-subchunk-exceeds', '', {});
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
const dataOffset = cursor + THREE_DS_CHUNK_HEADER_BYTES;
|
|
261
|
+
if (subId === THREE_DS_FACE_MATERIAL) {
|
|
262
|
+
const group = parseFaceMaterialGroup(view, dataOffset, subEnd, count, threeDsDrops);
|
|
263
|
+
if (group !== null)
|
|
264
|
+
materialGroups.push(group);
|
|
265
|
+
}
|
|
266
|
+
else if (subId === THREE_DS_SMOOTH_GROUP) {
|
|
267
|
+
smoothingGroups = parseSmoothingGroups(view, dataOffset, subEnd, count, threeDsDrops);
|
|
268
|
+
}
|
|
269
|
+
cursor = subEnd;
|
|
270
|
+
}
|
|
271
|
+
return { faces, materialGroups, smoothingGroups };
|
|
272
|
+
}
|
|
273
|
+
// Reads one FACE_MATERIAL (0x4130) group: a null-terminated material name, then uint16 nFaces, then
|
|
274
|
+
// nFaces uint16 face indices (into the mesh's triangle list) that bind that material. Returns null for a
|
|
275
|
+
// nameless or truncated group. Face indices past the mesh's face count are dropped with a warning.
|
|
276
|
+
function parseFaceMaterialGroup(view, dataStart, end, faceCount, threeDsDrops) {
|
|
277
|
+
const name = readNullTerminatedString(view, dataStart, end);
|
|
278
|
+
if (name.length === 0)
|
|
279
|
+
return null;
|
|
280
|
+
let offset = dataStart + name.length + 1; // past the name and null terminator
|
|
281
|
+
if (offset + 2 > end) {
|
|
282
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.material-group-truncated', 'no-count', {
|
|
283
|
+
firstName: name,
|
|
284
|
+
reason: 'no-count',
|
|
285
|
+
});
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const groupFaceCount = view.getUint16(offset, true);
|
|
289
|
+
offset += 2;
|
|
290
|
+
if (offset + groupFaceCount * 2 > end) {
|
|
291
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.material-group-truncated', 'truncated', {
|
|
292
|
+
firstCount: groupFaceCount,
|
|
293
|
+
firstName: name,
|
|
294
|
+
reason: 'truncated',
|
|
295
|
+
});
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const faces = new Uint16Array(groupFaceCount);
|
|
299
|
+
let kept = 0;
|
|
300
|
+
for (let i = 0; i < groupFaceCount; i++) {
|
|
301
|
+
const faceIndex = view.getUint16(offset + i * 2, true);
|
|
302
|
+
if (faceIndex < faceCount)
|
|
303
|
+
faces[kept++] = faceIndex;
|
|
304
|
+
}
|
|
305
|
+
if (kept < groupFaceCount) {
|
|
306
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.material-group-face-out-of-range', '', {
|
|
307
|
+
firstFaceCount: faceCount,
|
|
308
|
+
firstName: name,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return { faces: faces.subarray(0, kept), name };
|
|
312
|
+
}
|
|
313
|
+
// Reads the SMOOTH_GROUP (0x4150) sub-chunk: one uint32 smoothing-group bitmask per face. Two faces
|
|
314
|
+
// share a smoothed vertex normal only where their masks share a set bit; a face with mask 0 is flat.
|
|
315
|
+
// Returns null when the chunk is truncated (the mesh then smooths every shared vertex).
|
|
316
|
+
function parseSmoothingGroups(view, dataStart, end, faceCount, threeDsDrops) {
|
|
317
|
+
if (dataStart + faceCount * 4 > end) {
|
|
318
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.smoothing-truncated', '', {
|
|
319
|
+
firstFaceCount: faceCount,
|
|
320
|
+
});
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const groups = new Uint32Array(faceCount);
|
|
324
|
+
for (let i = 0; i < faceCount; i++)
|
|
325
|
+
groups[i] = view.getUint32(dataStart + i * 4, true);
|
|
326
|
+
return groups;
|
|
327
|
+
}
|
|
328
|
+
// Reads the UV coordinate sub-chunk (0x4140): uint16 count followed by count * 2 float32 values
|
|
329
|
+
// (u, v per vertex). The UV array is 1:1 with the vertex array — no re-indexing needed.
|
|
330
|
+
function parseUvCoords(view, dataStart, end, threeDsDrops) {
|
|
331
|
+
if (dataStart + 2 > end) {
|
|
332
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.uv-truncated', 'no-count', {
|
|
333
|
+
reason: 'no-count',
|
|
334
|
+
});
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const count = view.getUint16(dataStart, true);
|
|
338
|
+
const floatsNeeded = count * 2;
|
|
339
|
+
const bytesNeeded = dataStart + 2 + floatsNeeded * 4;
|
|
340
|
+
if (bytesNeeded > end) {
|
|
341
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Recover, '3ds.uv-truncated', 'truncated', {
|
|
342
|
+
firstCount: count,
|
|
343
|
+
reason: 'truncated',
|
|
344
|
+
});
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const uvCoords = new Float32Array(floatsNeeded);
|
|
348
|
+
let offset = dataStart + 2;
|
|
349
|
+
for (let i = 0; i < floatsNeeded; i++) {
|
|
350
|
+
uvCoords[i] = view.getFloat32(offset, true);
|
|
351
|
+
offset += 4;
|
|
352
|
+
}
|
|
353
|
+
return uvCoords;
|
|
354
|
+
}
|
|
355
|
+
// Builds a Mesh scene node from a parsed ThreeDsMesh descriptor. Vertex positions are converted from RH
|
|
356
|
+
// Z-up to RH Y-up via convertPositionsZUpToYUp. Normals are generated per smoothing group — a vertex
|
|
357
|
+
// shared by faces in different smoothing groups is split so each side keeps its own normal (so hard edges
|
|
358
|
+
// stay hard) — and the geometry is partitioned into one MeshSubset per FACE_MATERIAL group, with any
|
|
359
|
+
// faces belonging to no group forming a trailing default subset. Materials are resolved against the
|
|
360
|
+
// file's material table (memoized in `materialIndexByName`) and named per subset by index (-1 = default).
|
|
361
|
+
function appendMeshDocument(mesh, materials, materialIndexByName, document, threeDsDrops) {
|
|
362
|
+
const vertexCount = mesh.vertices.length / 3;
|
|
363
|
+
const faceCount = mesh.faces.length / 3;
|
|
364
|
+
// A parsed-but-empty trimesh (no vertices or no faces) yields no document mesh — an omitted element,
|
|
365
|
+
// mirroring md5mesh.mesh-empty.
|
|
366
|
+
if (vertexCount === 0 || faceCount === 0) {
|
|
367
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.mesh-empty', '', { firstName: mesh.name });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Convert positions from RH Z-up to RH Y-up before normal computation so all geometry operates in
|
|
371
|
+
// Flight's coordinate space. The rotation preserves winding, so computed normals face outward.
|
|
372
|
+
const positions = Array.from(mesh.vertices);
|
|
373
|
+
convertPositionsZUpToYUp(positions);
|
|
374
|
+
// Per-face normals, area-weighted (the raw edge cross product, magnitude ∝ 2×area). Faces that
|
|
375
|
+
// reference a vertex past the buffer are skipped from all normal/emit work.
|
|
376
|
+
const faceNormals = new Float64Array(faceCount * 3);
|
|
377
|
+
const incidentFaces = Array.from({ length: vertexCount }, () => []);
|
|
378
|
+
const faceValid = new Uint8Array(faceCount);
|
|
379
|
+
let droppedFaces = 0;
|
|
380
|
+
for (let f = 0; f < faceCount; f++) {
|
|
381
|
+
const i0 = mesh.faces[f * 3];
|
|
382
|
+
const i1 = mesh.faces[f * 3 + 1];
|
|
383
|
+
const i2 = mesh.faces[f * 3 + 2];
|
|
384
|
+
if (i0 >= vertexCount || i1 >= vertexCount || i2 >= vertexCount) {
|
|
385
|
+
droppedFaces++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
faceValid[f] = 1;
|
|
389
|
+
const e1x = positions[i1 * 3] - positions[i0 * 3];
|
|
390
|
+
const e1y = positions[i1 * 3 + 1] - positions[i0 * 3 + 1];
|
|
391
|
+
const e1z = positions[i1 * 3 + 2] - positions[i0 * 3 + 2];
|
|
392
|
+
const e2x = positions[i2 * 3] - positions[i0 * 3];
|
|
393
|
+
const e2y = positions[i2 * 3 + 1] - positions[i0 * 3 + 1];
|
|
394
|
+
const e2z = positions[i2 * 3 + 2] - positions[i0 * 3 + 2];
|
|
395
|
+
faceNormals[f * 3] = e1y * e2z - e1z * e2y;
|
|
396
|
+
faceNormals[f * 3 + 1] = e1z * e2x - e1x * e2z;
|
|
397
|
+
faceNormals[f * 3 + 2] = e1x * e2y - e1y * e2x;
|
|
398
|
+
incidentFaces[i0].push(f);
|
|
399
|
+
incidentFaces[i1].push(f);
|
|
400
|
+
incidentFaces[i2].push(f);
|
|
401
|
+
}
|
|
402
|
+
if (droppedFaces > 0) {
|
|
403
|
+
// Drop, not Recover: the offending faces are omitted entirely (the mesh keeps its valid faces), so
|
|
404
|
+
// this mirrors obj.position-index-out-of-range / md2.triangle-vertex-index-out-of-range — the dropped
|
|
405
|
+
// face is the element the crumb names, not the surviving mesh.
|
|
406
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.face-index-out-of-range', '', {
|
|
407
|
+
firstDroppedFaces: droppedFaces,
|
|
408
|
+
firstName: mesh.name,
|
|
409
|
+
firstVertexCount: vertexCount,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const smoothing = mesh.smoothingGroups;
|
|
413
|
+
// Output vertex buffer, grown lazily. Each source vertex keeps its original output index for its first
|
|
414
|
+
// resolved normal, so a mesh with no hard edges reindexes identically to the source; a vertex needing a
|
|
415
|
+
// second (differently-smoothed) normal appends a split copy. `vertexSlots[v]` lists (normal, outIndex).
|
|
416
|
+
const outVertices = [];
|
|
417
|
+
const vertexSlots = Array.from({ length: vertexCount }, () => []);
|
|
418
|
+
const emitCorner = (face, vertex) => {
|
|
419
|
+
// Sum the area-weighted normals of every face sharing this vertex that smooths with `face` (always
|
|
420
|
+
// itself; others only where a smoothing bit overlaps, or unconditionally when no smoothing chunk).
|
|
421
|
+
let nx = 0;
|
|
422
|
+
let ny = 0;
|
|
423
|
+
let nz = 0;
|
|
424
|
+
const incident = incidentFaces[vertex];
|
|
425
|
+
for (let k = 0; k < incident.length; k++) {
|
|
426
|
+
const other = incident[k];
|
|
427
|
+
if (other === face || smoothing === null || (smoothing[face] & smoothing[other]) !== 0) {
|
|
428
|
+
nx += faceNormals[other * 3];
|
|
429
|
+
ny += faceNormals[other * 3 + 1];
|
|
430
|
+
nz += faceNormals[other * 3 + 2];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
434
|
+
if (len > 0) {
|
|
435
|
+
nx /= len;
|
|
436
|
+
ny /= len;
|
|
437
|
+
nz /= len;
|
|
438
|
+
}
|
|
439
|
+
const slots = vertexSlots[vertex];
|
|
440
|
+
for (let s = 0; s < slots.length; s++) {
|
|
441
|
+
const slot = slots[s];
|
|
442
|
+
if (Math.abs(slot.nx - nx) < 1e-6 && Math.abs(slot.ny - ny) < 1e-6 && Math.abs(slot.nz - nz) < 1e-6) {
|
|
443
|
+
return slot.outIndex;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const outIndex = outVertices.length / CANONICAL_FLOATS_PER_VERTEX;
|
|
447
|
+
outVertices.push(positions[vertex * 3], positions[vertex * 3 + 1], positions[vertex * 3 + 2]);
|
|
448
|
+
outVertices.push(nx, ny, nz);
|
|
449
|
+
outVertices.push(0, 0, 0, 0); // tangent — 3DS carries none
|
|
450
|
+
if (mesh.uvs !== null && vertex < mesh.uvs.length / 2) {
|
|
451
|
+
outVertices.push(mesh.uvs[vertex * 2], 1 - mesh.uvs[vertex * 2 + 1]);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
outVertices.push(0, 0);
|
|
455
|
+
}
|
|
456
|
+
slots.push({ nx, ny, nz, outIndex });
|
|
457
|
+
return outIndex;
|
|
458
|
+
};
|
|
459
|
+
// Map each face to the ordinal of the last material group that claims it (-1 = unassigned). Then emit
|
|
460
|
+
// faces grouped by material so each material's triangles form one contiguous MeshSubset range.
|
|
461
|
+
const faceGroup = new Int32Array(faceCount).fill(-1);
|
|
462
|
+
mesh.materialGroups.forEach((group, groupIndex) => {
|
|
463
|
+
for (let i = 0; i < group.faces.length; i++)
|
|
464
|
+
faceGroup[group.faces[i]] = groupIndex;
|
|
465
|
+
});
|
|
466
|
+
const indices = [];
|
|
467
|
+
const subsets = [];
|
|
468
|
+
const meshMaterials = [];
|
|
469
|
+
const emitSubset = (predicate, materialIndex) => {
|
|
470
|
+
const indexOffset = indices.length;
|
|
471
|
+
for (let f = 0; f < faceCount; f++) {
|
|
472
|
+
if (!faceValid[f] || !predicate(f))
|
|
473
|
+
continue;
|
|
474
|
+
indices.push(emitCorner(f, mesh.faces[f * 3]), emitCorner(f, mesh.faces[f * 3 + 1]), emitCorner(f, mesh.faces[f * 3 + 2]));
|
|
475
|
+
}
|
|
476
|
+
const indexCount = indices.length - indexOffset;
|
|
477
|
+
if (indexCount > 0) {
|
|
478
|
+
subsets.push({ indexCount, indexOffset });
|
|
479
|
+
meshMaterials.push(materialIndex);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
mesh.materialGroups.forEach((group, groupIndex) => {
|
|
483
|
+
emitSubset((f) => faceGroup[f] === groupIndex, resolveThreeDsMaterial(group.name, materials, materialIndexByName, document, threeDsDrops));
|
|
484
|
+
});
|
|
485
|
+
emitSubset((f) => faceGroup[f] === -1, -1);
|
|
486
|
+
if (subsets.length === 0)
|
|
487
|
+
return; // every face was dropped as malformed
|
|
488
|
+
const geometry = createMeshGeometry({
|
|
489
|
+
indices: Uint32Array.from(indices),
|
|
490
|
+
layout: CANONICAL_LAYOUT,
|
|
491
|
+
subsets,
|
|
492
|
+
vertices: new Float32Array(outVertices),
|
|
493
|
+
});
|
|
494
|
+
const documentMesh = { geometry, materials: meshMaterials };
|
|
495
|
+
const meshIndex = document.meshes.length;
|
|
496
|
+
document.meshes.push(documentMesh);
|
|
497
|
+
// A 3DS named object holds a single trimesh, so the name belongs on the Mesh node itself. Match glTF:
|
|
498
|
+
// a lone mesh is a bare Mesh node, named.
|
|
499
|
+
const node = { children: [], kind: MeshKind, mesh: meshIndex, transform: createTransform3D() };
|
|
500
|
+
if (mesh.name.length > 0)
|
|
501
|
+
node.name = mesh.name;
|
|
502
|
+
const nodeIndex = document.nodes.length;
|
|
503
|
+
document.nodes.push(node);
|
|
504
|
+
document.scenes[0].rootNodes.push(nodeIndex);
|
|
505
|
+
}
|
|
506
|
+
// Resolves a 3DS material name to its document material index, registering it (converted to BlinnPhong)
|
|
507
|
+
// on first use and memoizing in `materialIndexByName` so a material shared across meshes registers once.
|
|
508
|
+
// Returns -1 for an empty name or a name absent from the file's material table (a default-material subset).
|
|
509
|
+
function resolveThreeDsMaterial(name, materials, materialIndexByName, document, threeDsDrops) {
|
|
510
|
+
if (name.length === 0)
|
|
511
|
+
return -1; // an empty name is a spec-valid default-material subset — silent
|
|
512
|
+
const parsed = materials.get(name);
|
|
513
|
+
if (parsed === undefined) {
|
|
514
|
+
// A FACE_MATERIAL group naming a material absent from the file's table: the authored binding is dropped
|
|
515
|
+
// and the subset falls back to the default material — mirrors obj.material-missing (Drop).
|
|
516
|
+
tallyThreeDsDrop(threeDsDrops, ImportDiagnosticSeverity.Drop, '3ds.material-missing', '', { firstName: name });
|
|
517
|
+
return -1;
|
|
518
|
+
}
|
|
519
|
+
const cached = materialIndexByName.get(name);
|
|
520
|
+
if (cached !== undefined)
|
|
521
|
+
return cached;
|
|
522
|
+
const index = document.materials.length;
|
|
523
|
+
document.materials.push(threeDsMaterialToBlinnPhong(parsed, document));
|
|
524
|
+
materialIndexByName.set(name, index);
|
|
525
|
+
return index;
|
|
526
|
+
}
|
|
527
|
+
// Converts a parsed 3DS material to Flight's BlinnPhongMaterial — 3DS's own diffuse/specular shading
|
|
528
|
+
// model. Diffuse and specular colors map directly; shininess maps to the specular exponent; the diffuse
|
|
529
|
+
// texture filename becomes an Unresolved External diffuseMap ref (the bump filename stays parsed
|
|
530
|
+
// metadata — see the NOTE below); and a below-opaque
|
|
531
|
+
// material folds its opacity into the diffuse alpha plus a blend alphaMode. The ambient color has no
|
|
532
|
+
// Blinn-Phong equivalent (ambient is a scene light in Flight), so it is dropped; a caller wanting PBR
|
|
533
|
+
// converts explicitly.
|
|
534
|
+
function threeDsMaterialToBlinnPhong(material, document) {
|
|
535
|
+
const result = createBlinnPhongMaterial({
|
|
536
|
+
diffuse: packThreeDsColor(material.diffuse, material.opacity),
|
|
537
|
+
diffuseMap: material.textureFilename !== null
|
|
538
|
+
? createExternalTextureRef(material.textureFilename, null, document.resources)
|
|
539
|
+
: null,
|
|
540
|
+
specular: packThreeDsColor(material.specular),
|
|
541
|
+
// shininess is nullable so an explicit MAT_SHININESS of 0 (a valid, matte value) is passed through
|
|
542
|
+
// rather than dropped to createBlinnPhongMaterial's non-zero default; absent stays absent.
|
|
543
|
+
...(material.shininess !== null ? { shininess: material.shininess } : {}),
|
|
544
|
+
});
|
|
545
|
+
// NOTE: MAT_BUMPMAP (0xA230) is a legacy grayscale HEIGHT map, not a tangent-space normal map — binding
|
|
546
|
+
// it to `normalMap` (which the shaders sample as RGB*2-1 normals) would render bogus vectors. It is
|
|
547
|
+
// parsed into `material.bumpFilename` as metadata but intentionally NOT bound here; an honest bump→normal
|
|
548
|
+
// seam is a renderer feature (see scene-formats status.md, parked alongside the opacity map).
|
|
549
|
+
// Preserve the 3DS material chunk name as the material's authored name (empty → anonymous).
|
|
550
|
+
result.name = material.name.length > 0 ? material.name : null;
|
|
551
|
+
// A material below full opacity blends: the opacity rode into the diffuse alpha above; the blend
|
|
552
|
+
// alphaMode makes the renderer actually blend rather than treat the alpha as coverage.
|
|
553
|
+
if (material.opacity < 1)
|
|
554
|
+
result.alphaMode = 'blend';
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
// Parses a material block (0xAFFF): walks sub-chunks for the name, the diffuse/specular/ambient color
|
|
558
|
+
// blocks, the shininess and transparency percentages, and the diffuse and bump texture-map filenames.
|
|
559
|
+
function parseMaterial(view, offset, end) {
|
|
560
|
+
let name = '';
|
|
561
|
+
let ambient = [0, 0, 0];
|
|
562
|
+
let bumpFilename = null;
|
|
563
|
+
let diffuse = [1, 1, 1];
|
|
564
|
+
let opacity = 1;
|
|
565
|
+
// null = absent (use the material default); a parsed value (including an explicit 0) is passed through.
|
|
566
|
+
let shininess = null;
|
|
567
|
+
let specular = [1, 1, 1];
|
|
568
|
+
let textureFilename = null;
|
|
569
|
+
let cursor = offset + THREE_DS_CHUNK_HEADER_BYTES;
|
|
570
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
571
|
+
const chunkId = view.getUint16(cursor, true);
|
|
572
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
573
|
+
const chunkEnd = cursor + chunkLength;
|
|
574
|
+
if (chunkLength < THREE_DS_CHUNK_HEADER_BYTES || chunkEnd > end)
|
|
575
|
+
break;
|
|
576
|
+
const dataStart = cursor + THREE_DS_CHUNK_HEADER_BYTES;
|
|
577
|
+
if (chunkId === THREE_DS_MATERIAL_NAME) {
|
|
578
|
+
name = readNullTerminatedString(view, dataStart, chunkEnd);
|
|
579
|
+
}
|
|
580
|
+
else if (chunkId === THREE_DS_MATERIAL_AMBIENT) {
|
|
581
|
+
ambient = parseColorChunk(view, dataStart, chunkEnd) ?? ambient;
|
|
582
|
+
}
|
|
583
|
+
else if (chunkId === THREE_DS_MATERIAL_DIFFUSE) {
|
|
584
|
+
diffuse = parseColorChunk(view, dataStart, chunkEnd) ?? diffuse;
|
|
585
|
+
}
|
|
586
|
+
else if (chunkId === THREE_DS_MATERIAL_SPECULAR) {
|
|
587
|
+
specular = parseColorChunk(view, dataStart, chunkEnd) ?? specular;
|
|
588
|
+
}
|
|
589
|
+
else if (chunkId === THREE_DS_MATERIAL_SHININESS) {
|
|
590
|
+
// The MAT_SHININESS percentage (0..1) maps to a Blinn-Phong specular exponent; 100% → 128, a
|
|
591
|
+
// conventional maximum. 3DS's shininess slider has no exact Phong-exponent equivalent.
|
|
592
|
+
const fraction = parsePercentageChunk(view, dataStart, chunkEnd);
|
|
593
|
+
if (fraction !== null)
|
|
594
|
+
shininess = fraction * 128;
|
|
595
|
+
}
|
|
596
|
+
else if (chunkId === THREE_DS_MATERIAL_TRANSPARENCY) {
|
|
597
|
+
// MAT_TRANSPARENCY is the transparent fraction (0 = opaque); opacity is its complement.
|
|
598
|
+
const fraction = parsePercentageChunk(view, dataStart, chunkEnd);
|
|
599
|
+
if (fraction !== null)
|
|
600
|
+
opacity = 1 - fraction;
|
|
601
|
+
}
|
|
602
|
+
else if (chunkId === THREE_DS_MATERIAL_TEXTURE_MAP) {
|
|
603
|
+
textureFilename = parseTextureFilename(view, dataStart, chunkEnd);
|
|
604
|
+
}
|
|
605
|
+
else if (chunkId === THREE_DS_MATERIAL_BUMP_MAP) {
|
|
606
|
+
bumpFilename = parseTextureFilename(view, dataStart, chunkEnd);
|
|
607
|
+
}
|
|
608
|
+
cursor = chunkEnd;
|
|
609
|
+
}
|
|
610
|
+
return { ambient, bumpFilename, diffuse, name, opacity, shininess, specular, textureFilename };
|
|
611
|
+
}
|
|
612
|
+
// Reads the nested color sub-chunk of a material color block: COLOR_FLOAT (0x0010, 3 float32 in [0,1])
|
|
613
|
+
// or COLOR_BYTE (0x0011, 3 uint8 in [0,255], normalized). Returns [r,g,b] in [0,1], or null if absent.
|
|
614
|
+
function parseColorChunk(view, offset, end) {
|
|
615
|
+
let cursor = offset;
|
|
616
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
617
|
+
const chunkId = view.getUint16(cursor, true);
|
|
618
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
619
|
+
const chunkEnd = cursor + chunkLength;
|
|
620
|
+
if (chunkLength < THREE_DS_CHUNK_HEADER_BYTES || chunkEnd > end)
|
|
621
|
+
break;
|
|
622
|
+
const dataStart = cursor + THREE_DS_CHUNK_HEADER_BYTES;
|
|
623
|
+
if (chunkId === THREE_DS_COLOR_FLOAT && dataStart + 12 <= chunkEnd) {
|
|
624
|
+
return [
|
|
625
|
+
view.getFloat32(dataStart, true),
|
|
626
|
+
view.getFloat32(dataStart + 4, true),
|
|
627
|
+
view.getFloat32(dataStart + 8, true),
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
if (chunkId === THREE_DS_COLOR_BYTE && dataStart + 3 <= chunkEnd) {
|
|
631
|
+
return [view.getUint8(dataStart) / 255, view.getUint8(dataStart + 1) / 255, view.getUint8(dataStart + 2) / 255];
|
|
632
|
+
}
|
|
633
|
+
cursor = chunkEnd;
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
// Reads a percentage material sub-chunk (shininess/transparency), returning a fraction in [0,1]: an
|
|
638
|
+
// INT_PERCENTAGE (0x0030) uint16 in [0,100] is divided by 100; a FLOAT_PERCENTAGE (0x0031) float32 is a
|
|
639
|
+
// fraction already. Returns null if neither is present.
|
|
640
|
+
function parsePercentageChunk(view, offset, end) {
|
|
641
|
+
let cursor = offset;
|
|
642
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
643
|
+
const chunkId = view.getUint16(cursor, true);
|
|
644
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
645
|
+
const chunkEnd = cursor + chunkLength;
|
|
646
|
+
if (chunkLength < THREE_DS_CHUNK_HEADER_BYTES || chunkEnd > end)
|
|
647
|
+
break;
|
|
648
|
+
const dataStart = cursor + THREE_DS_CHUNK_HEADER_BYTES;
|
|
649
|
+
if (chunkId === THREE_DS_PERCENT_INT && dataStart + 2 <= chunkEnd) {
|
|
650
|
+
return Math.min(1, Math.max(0, view.getUint16(dataStart, true) / 100));
|
|
651
|
+
}
|
|
652
|
+
if (chunkId === THREE_DS_PERCENT_FLOAT && dataStart + 4 <= chunkEnd) {
|
|
653
|
+
return Math.min(1, Math.max(0, view.getFloat32(dataStart, true)));
|
|
654
|
+
}
|
|
655
|
+
cursor = chunkEnd;
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
// Reads a texture map block (0xA200 diffuse, 0xA230 bump, …), returning its filename sub-chunk (0xA300)
|
|
660
|
+
// or null.
|
|
661
|
+
function parseTextureFilename(view, offset, end) {
|
|
662
|
+
let cursor = offset;
|
|
663
|
+
while (cursor + THREE_DS_CHUNK_HEADER_BYTES <= end) {
|
|
664
|
+
const chunkId = view.getUint16(cursor, true);
|
|
665
|
+
const chunkLength = readChunkLength(view, cursor);
|
|
666
|
+
const chunkEnd = cursor + chunkLength;
|
|
667
|
+
if (chunkLength < THREE_DS_CHUNK_HEADER_BYTES || chunkEnd > end)
|
|
668
|
+
break;
|
|
669
|
+
if (chunkId === THREE_DS_MATERIAL_TEXTURE_FILENAME) {
|
|
670
|
+
const name = readNullTerminatedString(view, cursor + THREE_DS_CHUNK_HEADER_BYTES, chunkEnd);
|
|
671
|
+
return name.length > 0 ? name : null;
|
|
672
|
+
}
|
|
673
|
+
cursor = chunkEnd;
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
// Packs a 3DS sRGB-space [r,g,b] triple plus an alpha (each in [0,1]) into a 0xRRGGBBAA integer.
|
|
678
|
+
function packThreeDsColor(rgb, alpha = 1) {
|
|
679
|
+
const r = Math.round(Math.min(1, Math.max(0, rgb[0])) * 0xff);
|
|
680
|
+
const g = Math.round(Math.min(1, Math.max(0, rgb[1])) * 0xff);
|
|
681
|
+
const b = Math.round(Math.min(1, Math.max(0, rgb[2])) * 0xff);
|
|
682
|
+
const a = Math.round(Math.min(1, Math.max(0, alpha)) * 0xff);
|
|
683
|
+
return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;
|
|
684
|
+
}
|
|
685
|
+
// Reads a null-terminated ASCII string starting at `offset`, stopping at the first null byte or at
|
|
686
|
+
// `end` (whichever comes first).
|
|
687
|
+
function readNullTerminatedString(view, offset, end) {
|
|
688
|
+
const chars = [];
|
|
689
|
+
let cursor = offset;
|
|
690
|
+
while (cursor < end) {
|
|
691
|
+
const byte = view.getUint8(cursor);
|
|
692
|
+
if (byte === 0)
|
|
693
|
+
break;
|
|
694
|
+
chars.push(String.fromCharCode(byte));
|
|
695
|
+
cursor++;
|
|
696
|
+
}
|
|
697
|
+
return chars.join('');
|
|
698
|
+
}
|
|
699
|
+
// Reads the chunk length (uint32 at offset + 2). The length includes the 6-byte header.
|
|
700
|
+
function readChunkLength(view, offset) {
|
|
701
|
+
return view.getUint32(offset + 2, true);
|
|
702
|
+
}
|
|
703
|
+
// Records one offender against its (kind, discriminator) tally — the aggregate-once alternative to a
|
|
704
|
+
// per-chunk/per-mesh `reportImportDiagnostic` while walking the recursive chunk tree. No-op (never
|
|
705
|
+
// allocates) when no collector is engaged. `firstDetail` is kept from the FIRST offender; later ones only
|
|
706
|
+
// bump the count. The discriminator is the categorical sub-reason (never an instance name), so faults of
|
|
707
|
+
// the same kind across many meshes collapse to one crumb.
|
|
708
|
+
function tallyThreeDsDrop(tallies, severity, kind, discriminator, firstDetail) {
|
|
709
|
+
if (tallies === null)
|
|
710
|
+
return;
|
|
711
|
+
const key = `${kind}|${discriminator}`;
|
|
712
|
+
const existing = tallies.get(key);
|
|
713
|
+
if (existing === undefined)
|
|
714
|
+
tallies.set(key, { count: 1, detail: firstDetail, kind, severity });
|
|
715
|
+
else
|
|
716
|
+
existing.count++;
|
|
717
|
+
}
|
|
718
|
+
//# sourceMappingURL=threeDsParse.js.map
|