@es3d/es3d 1.0.0 → 1.0.2
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/LICENSE +201 -201
- package/README.md +95 -86
- package/docs/SPEC.md +426 -417
- package/package.json +1 -1
- package/scripts/es3d-cli.mjs +390 -377
- package/src/cache/geometry-store.js +392 -392
- package/src/cache/mesh-bundle.js +123 -123
- package/src/cache/sha256.js +73 -73
- package/src/format/ast.js +238 -238
- package/src/format/colors.js +93 -93
- package/src/format/compact-parser.js +171 -171
- package/src/format/compact-tokenizer.js +165 -165
- package/src/format/errors.js +247 -247
- package/src/format/json.js +55 -55
- package/src/format/limits.js +14 -14
- package/src/format/parse.js +132 -92
- package/src/format/schema.js +617 -617
- package/src/format/serializer.js +333 -333
- package/src/format/validator.js +330 -330
- package/src/format/value-error.js +12 -12
- package/src/format/value-format.js +314 -314
- package/src/format/values.js +459 -459
- package/src/format/xml-parser.js +136 -136
- package/src/format/xml-tokenizer.js +237 -237
- package/src/geometry/contour.js +86 -86
- package/src/geometry/extrude.js +175 -175
- package/src/geometry/gear.js +85 -85
- package/src/geometry/geometry-cache.js +159 -159
- package/src/geometry/lod-error.js +178 -178
- package/src/geometry/mesh.js +458 -458
- package/src/geometry/primitives.js +299 -299
- package/src/geometry/registry.js +645 -645
- package/src/geometry/revolve.js +136 -136
- package/src/geometry/sweep.js +441 -441
- package/src/index.js +47 -47
- package/src/io/compression.js +67 -67
- package/src/io/convert.js +236 -236
- package/src/io/download.js +13 -13
- package/src/io/external-scene.js +127 -127
- package/src/io/file-loader.js +28 -28
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/gltf.js +630 -630
- package/src/io/mesh-import.js +104 -104
- package/src/io/obj.js +352 -352
- package/src/io/step-brep.js +439 -439
- package/src/io/step-exact.js +1149 -1149
- package/src/io/step.js +1418 -1418
- package/src/io/stl.js +193 -193
- package/src/io/svg-import.js +787 -787
- package/src/material/materials.js +154 -154
- package/src/math/mat4.js +475 -475
- package/src/math/random.js +30 -30
- package/src/math/trig.js +65 -65
- package/src/math/vec3.js +81 -81
- package/src/optimize/optimizer.js +604 -579
- package/src/path/path-flatten.js +322 -322
- package/src/path/path-parser.js +332 -332
- package/src/path/shapes.js +129 -129
- package/src/path/triangulate.js +517 -517
- package/src/pipeline.js +74 -74
- package/src/render/camera.js +168 -168
- package/src/render/frame-builder.js +362 -362
- package/src/render/geometry-pool.js +149 -149
- package/src/render/lighting.js +149 -149
- package/src/render/orbit-controls.js +237 -237
- package/src/render/renderer.js +258 -249
- package/src/render/scroll-timeline.js +104 -104
- package/src/render/shaders/glsl.js +285 -285
- package/src/render/shaders/wgsl.js +252 -252
- package/src/render/stage.js +365 -352
- package/src/render/texture-cache.js +40 -40
- package/src/render/textures.js +223 -223
- package/src/render/webgl2/webgl2-backend.js +586 -532
- package/src/render/webgpu/webgpu-backend.js +482 -480
- package/src/scene/flatten.js +754 -754
- package/src/scene/picking.js +161 -161
- package/src/scene/scene-graph.js +320 -320
- package/src/transform/transform.js +211 -211
- package/src/wasm/kernels-binary.js +3 -3
- package/src/wasm/kernels.js +385 -385
package/src/io/obj.js
CHANGED
|
@@ -1,352 +1,352 @@
|
|
|
1
|
-
// Wavefront OBJ export (fully flattened triangles, the format has no
|
|
2
|
-
// instancing) and a reasonably fast OBJ parser used by the benchmark and the
|
|
3
|
-
// importer. The exporter is deliberately generous to OBJ: trimmed numbers,
|
|
4
|
-
// de-duplicated normals and texture coordinates, one usemtl per material run.
|
|
5
|
-
|
|
6
|
-
import { Mesh, computeNormals } from '../geometry/mesh.js';
|
|
7
|
-
import { linearToSrgb } from '../format/colors.js';
|
|
8
|
-
import { exportableInstances, materialNames, fixed, buildExternalScene, InstanceTable } from './external-scene.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @param {import('../scene/flatten.js').RenderScene} scene
|
|
12
|
-
* @param {(entry:object, lod:number)=>Mesh} meshOf
|
|
13
|
-
* @param {{lod?: number, digits?: number, name?: string, uvs?: 'auto'|boolean}} [opts]
|
|
14
|
-
* uvs: 'auto' writes texture coordinates only for textured materials
|
|
15
|
-
* @returns {{obj: string, mtl: string, vertices: number, triangles: number}}
|
|
16
|
-
*/
|
|
17
|
-
export function exportOBJ(scene, meshOf, { lod = 0, digits = 5, name = 'scene', uvs = 'auto' } = {}) {
|
|
18
|
-
const names = materialNames(scene.materials);
|
|
19
|
-
const out = [`# ES3D export: ${name}`, `mtllib ${name}.mtl`];
|
|
20
|
-
const vnIndex = new Map();
|
|
21
|
-
const vtIndex = new Map();
|
|
22
|
-
const vnLines = [];
|
|
23
|
-
const vtLines = [];
|
|
24
|
-
const vLines = [];
|
|
25
|
-
const faces = [];
|
|
26
|
-
let vCount = 0;
|
|
27
|
-
let triangles = 0;
|
|
28
|
-
let currentMat = -1;
|
|
29
|
-
const m = new Float64Array(16);
|
|
30
|
-
for (const i of exportableInstances(scene)) {
|
|
31
|
-
const entry = scene.geometries[scene.geom[i]];
|
|
32
|
-
const mesh = meshOf(entry, Math.min(lod, entry.lods - 1));
|
|
33
|
-
if (!mesh.vertexCount) continue;
|
|
34
|
-
for (let k = 0; k < 16; k++) m[k] = scene.matrices[i * 16 + k];
|
|
35
|
-
const mi = scene.mat[i];
|
|
36
|
-
if (mi !== currentMat) {
|
|
37
|
-
faces.push(`usemtl ${names[mi]}`);
|
|
38
|
-
currentMat = mi;
|
|
39
|
-
}
|
|
40
|
-
// normal matrix = cofactor (sign-corrected)
|
|
41
|
-
const c00 = m[5] * m[10] - m[6] * m[9], c01 = m[6] * m[8] - m[4] * m[10], c02 = m[4] * m[9] - m[5] * m[8];
|
|
42
|
-
const c10 = m[9] * m[2] - m[10] * m[1], c11 = m[10] * m[0] - m[8] * m[2], c12 = m[8] * m[1] - m[9] * m[0];
|
|
43
|
-
const c20 = m[1] * m[6] - m[2] * m[5], c21 = m[2] * m[4] - m[0] * m[6], c22 = m[0] * m[5] - m[1] * m[4];
|
|
44
|
-
const det = m[0] * c00 + m[1] * c01 + m[2] * c02;
|
|
45
|
-
const sg = det < 0 ? -1 : 1;
|
|
46
|
-
const p = mesh.positions, nr = mesh.normals, uv = mesh.uvs;
|
|
47
|
-
const n = mesh.vertexCount;
|
|
48
|
-
const material = scene.materials[mi];
|
|
49
|
-
const withUV = uvs === true || (uvs === 'auto' && !!(material.map || material.normalMap));
|
|
50
|
-
const vt = new Int32Array(n);
|
|
51
|
-
const vn = new Int32Array(n);
|
|
52
|
-
for (let k = 0; k < n; k++) {
|
|
53
|
-
const x = p[k * 3], y = p[k * 3 + 1], z = p[k * 3 + 2];
|
|
54
|
-
vLines.push(`v ${fixed(m[0] * x + m[4] * y + m[8] * z + m[12], digits)} ${fixed(m[1] * x + m[5] * y + m[9] * z + m[13], digits)} ${fixed(m[2] * x + m[6] * y + m[10] * z + m[14], digits)}`);
|
|
55
|
-
if (mesh.kind === 'triangles') {
|
|
56
|
-
const a = nr[k * 3], b = nr[k * 3 + 1], c = nr[k * 3 + 2];
|
|
57
|
-
let nx = (c00 * a + c10 * b + c20 * c) * sg, ny = (c01 * a + c11 * b + c21 * c) * sg, nz = (c02 * a + c12 * b + c22 * c) * sg;
|
|
58
|
-
const l = Math.hypot(nx, ny, nz) || 1;
|
|
59
|
-
const key = `${fixed(nx / l, 4)} ${fixed(ny / l, 4)} ${fixed(nz / l, 4)}`;
|
|
60
|
-
let ni = vnIndex.get(key);
|
|
61
|
-
if (ni === undefined) {
|
|
62
|
-
ni = vnLines.length + 1;
|
|
63
|
-
vnIndex.set(key, ni);
|
|
64
|
-
vnLines.push('vn ' + key);
|
|
65
|
-
}
|
|
66
|
-
vn[k] = ni;
|
|
67
|
-
if (!withUV) continue;
|
|
68
|
-
const tkey = `${fixed(uv[k * 2], 4)} ${fixed(uv[k * 2 + 1], 4)}`;
|
|
69
|
-
let ti = vtIndex.get(tkey);
|
|
70
|
-
if (ti === undefined) {
|
|
71
|
-
ti = vtLines.length + 1;
|
|
72
|
-
vtIndex.set(tkey, ti);
|
|
73
|
-
vtLines.push('vt ' + tkey);
|
|
74
|
-
}
|
|
75
|
-
vt[k] = ti;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const idx = mesh.indices;
|
|
79
|
-
if (mesh.kind === 'lines') {
|
|
80
|
-
for (let k = 0; k < idx.length; k += 2) faces.push(`l ${idx[k] + 1 + vCount} ${idx[k + 1] + 1 + vCount}`);
|
|
81
|
-
} else {
|
|
82
|
-
for (let k = 0; k < idx.length; k += 3) {
|
|
83
|
-
let a = idx[k], b = idx[k + 1], c = idx[k + 2];
|
|
84
|
-
if (det < 0) [b, c] = [c, b];
|
|
85
|
-
if (withUV) faces.push(`f ${a + 1 + vCount}/${vt[a]}/${vn[a]} ${b + 1 + vCount}/${vt[b]}/${vn[b]} ${c + 1 + vCount}/${vt[c]}/${vn[c]}`);
|
|
86
|
-
else faces.push(`f ${a + 1 + vCount}//${vn[a]} ${b + 1 + vCount}//${vn[b]} ${c + 1 + vCount}//${vn[c]}`);
|
|
87
|
-
}
|
|
88
|
-
triangles += idx.length / 3;
|
|
89
|
-
}
|
|
90
|
-
vCount += n;
|
|
91
|
-
}
|
|
92
|
-
const parts = [out, vLines, vtLines, vnLines, faces].filter((a) => a.length).map((a) => a.join('\n'));
|
|
93
|
-
const mtl = [];
|
|
94
|
-
scene.materials.forEach((mat, i) => {
|
|
95
|
-
const kd = mat.baseColor.slice(0, 3).map((c) => fixed(linearToSrgb(c), 4)).join(' ');
|
|
96
|
-
const ke = mat.emissive.map((c) => fixed(Math.min(1, linearToSrgb(c)), 4)).join(' ');
|
|
97
|
-
mtl.push(`newmtl ${names[i]}`, `Kd ${kd}`, `Ke ${ke}`, `d ${fixed(mat.opacity, 4)}`, `Pr ${fixed(mat.roughness, 4)}`, `Pm ${fixed(mat.metallic, 4)}`, '');
|
|
98
|
-
});
|
|
99
|
-
return { obj: parts.join('\n') + '\n', mtl: mtl.join('\n'), vertices: vCount, triangles };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// parsing
|
|
104
|
-
|
|
105
|
-
const POW10 = Array.from({ length: 23 }, (_, i) => 10 ** i); // exact doubles
|
|
106
|
-
|
|
107
|
-
function parseMtl(text) {
|
|
108
|
-
const mats = new Map();
|
|
109
|
-
let cur = null;
|
|
110
|
-
for (const raw of text.split('\n')) {
|
|
111
|
-
const line = raw.trim();
|
|
112
|
-
if (!line || line[0] === '#') continue;
|
|
113
|
-
const parts = line.split(/\s+/);
|
|
114
|
-
const k = parts[0];
|
|
115
|
-
if (k === 'newmtl') {
|
|
116
|
-
cur = { name: parts.slice(1).join(' '), color: [0.8, 0.8, 0.8, 1], metallic: 0, roughness: 0.5, opacity: 1, emissive: [0, 0, 0, 1] };
|
|
117
|
-
mats.set(cur.name, cur);
|
|
118
|
-
} else if (cur) {
|
|
119
|
-
const nums = parts.slice(1).map(Number);
|
|
120
|
-
if (k === 'Kd') cur.color = [nums[0], nums[1], nums[2], 1];
|
|
121
|
-
else if (k === 'Ke') cur.emissive = [nums[0], nums[1], nums[2], 1];
|
|
122
|
-
else if (k === 'd') cur.opacity = nums[0];
|
|
123
|
-
else if (k === 'Tr') cur.opacity = 1 - nums[0];
|
|
124
|
-
else if (k === 'Pr') cur.roughness = nums[0];
|
|
125
|
-
else if (k === 'Pm') cur.metallic = nums[0];
|
|
126
|
-
else if (k === 'Ns') cur.roughness = Math.sqrt(2 / (nums[0] + 2));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return mats;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Parse OBJ text (and optional MTL text) into meshes grouped by material.
|
|
134
|
-
* Faces are scanned character by character and corners are de-duplicated
|
|
135
|
-
* through per-position linked lists, so large files parse without per-corner
|
|
136
|
-
* string allocations.
|
|
137
|
-
* @returns {{groups: {material: string, mesh: Mesh}[], materials: Map, stats: object}}
|
|
138
|
-
*/
|
|
139
|
-
export function parseOBJ(text, mtlText = '') {
|
|
140
|
-
const pos = [];
|
|
141
|
-
const nrm = [];
|
|
142
|
-
const tex = [];
|
|
143
|
-
const groups = new Map();
|
|
144
|
-
let group = null;
|
|
145
|
-
const groupList = [];
|
|
146
|
-
const useGroup = (name) => {
|
|
147
|
-
group = groups.get(name);
|
|
148
|
-
if (!group) {
|
|
149
|
-
group = { material: name, id: groupList.length, p: [], n: [], t: [], idx: [], lines: [], hasNormals: true, hasUV: true };
|
|
150
|
-
groups.set(name, group);
|
|
151
|
-
groupList.push(group);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
useGroup('default');
|
|
155
|
-
// corner de-duplication: for every position index a linked list of the
|
|
156
|
-
// (group, texcoord, normal) combinations already emitted
|
|
157
|
-
let head = new Int32Array(1024).fill(-1);
|
|
158
|
-
let cap = 1024;
|
|
159
|
-
let nextE = new Int32Array(cap), gE = new Int32Array(cap), tE = new Int32Array(cap), nE = new Int32Array(cap), outE = new Int32Array(cap);
|
|
160
|
-
let entries = 0;
|
|
161
|
-
const growEntries = () => {
|
|
162
|
-
cap *= 2;
|
|
163
|
-
const grow = (a) => {
|
|
164
|
-
const b = new Int32Array(cap);
|
|
165
|
-
b.set(a);
|
|
166
|
-
return b;
|
|
167
|
-
};
|
|
168
|
-
nextE = grow(nextE);
|
|
169
|
-
gE = grow(gE);
|
|
170
|
-
tE = grow(tE);
|
|
171
|
-
nE = grow(nE);
|
|
172
|
-
outE = grow(outE);
|
|
173
|
-
};
|
|
174
|
-
const corner = (v, t, n) => {
|
|
175
|
-
if (v < 0) v = pos.length / 3 + v + 1;
|
|
176
|
-
if (t < 0) t = tex.length / 2 + t + 1;
|
|
177
|
-
if (n < 0) n = nrm.length / 3 + n + 1;
|
|
178
|
-
if (v < 1 || v > pos.length / 3) throw new Error(`OBJ face references vertex ${v}, but only ${pos.length / 3} are defined.`);
|
|
179
|
-
if (v > head.length) {
|
|
180
|
-
let len = head.length;
|
|
181
|
-
while (len < v) len *= 2;
|
|
182
|
-
const h = new Int32Array(len).fill(-1);
|
|
183
|
-
h.set(head);
|
|
184
|
-
head = h;
|
|
185
|
-
}
|
|
186
|
-
for (let e = head[v - 1]; e >= 0; e = nextE[e]) {
|
|
187
|
-
if (gE[e] === group.id && tE[e] === t && nE[e] === n) return outE[e];
|
|
188
|
-
}
|
|
189
|
-
const idx = group.p.length / 3;
|
|
190
|
-
group.p.push(pos[(v - 1) * 3], pos[(v - 1) * 3 + 1], pos[(v - 1) * 3 + 2]);
|
|
191
|
-
if (n) group.n.push(nrm[(n - 1) * 3], nrm[(n - 1) * 3 + 1], nrm[(n - 1) * 3 + 2]);
|
|
192
|
-
else {
|
|
193
|
-
group.hasNormals = false;
|
|
194
|
-
group.n.push(0, 0, 0);
|
|
195
|
-
}
|
|
196
|
-
if (t) group.t.push(tex[(t - 1) * 2], tex[(t - 1) * 2 + 1]);
|
|
197
|
-
else {
|
|
198
|
-
group.hasUV = false;
|
|
199
|
-
group.t.push(0, 0);
|
|
200
|
-
}
|
|
201
|
-
if (entries === cap) growEntries();
|
|
202
|
-
gE[entries] = group.id;
|
|
203
|
-
tE[entries] = t;
|
|
204
|
-
nE[entries] = n;
|
|
205
|
-
outE[entries] = idx;
|
|
206
|
-
nextE[entries] = head[v - 1];
|
|
207
|
-
head[v - 1] = entries++;
|
|
208
|
-
return idx;
|
|
209
|
-
};
|
|
210
|
-
let i = 0;
|
|
211
|
-
const L = text.length;
|
|
212
|
-
// plain decimals (the common case) are converted with one exact division,
|
|
213
|
-
// which gives the same correctly rounded double as Number(); anything else
|
|
214
|
-
// (exponents, very long mantissas) goes through Number()
|
|
215
|
-
const decimal = (start, end) => {
|
|
216
|
-
let k = start;
|
|
217
|
-
let neg = false;
|
|
218
|
-
const c0 = text.charCodeAt(k);
|
|
219
|
-
if (c0 === 45 || c0 === 43) {
|
|
220
|
-
neg = c0 === 45;
|
|
221
|
-
k++;
|
|
222
|
-
}
|
|
223
|
-
let mant = 0, scale = 0, dot = false, digits = 0;
|
|
224
|
-
for (; k < end; k++) {
|
|
225
|
-
const c = text.charCodeAt(k);
|
|
226
|
-
if (c >= 48 && c <= 57) {
|
|
227
|
-
if (mant > 9e14) return +text.slice(start, end);
|
|
228
|
-
mant = mant * 10 + (c - 48);
|
|
229
|
-
digits++;
|
|
230
|
-
if (dot) scale++;
|
|
231
|
-
} else if (c === 46 && !dot) dot = true;
|
|
232
|
-
else return +text.slice(start, end);
|
|
233
|
-
}
|
|
234
|
-
if (!digits || scale > 22) return +text.slice(start, end);
|
|
235
|
-
const v = scale ? mant / POW10[scale] : mant;
|
|
236
|
-
return neg ? -v : v;
|
|
237
|
-
};
|
|
238
|
-
const readFloats = (start, end, out, count) => {
|
|
239
|
-
let k = start;
|
|
240
|
-
for (let c = 0; c < count; c++) {
|
|
241
|
-
while (k < end && (text.charCodeAt(k) === 32 || text.charCodeAt(k) === 9)) k++;
|
|
242
|
-
let e = k;
|
|
243
|
-
while (e < end && text.charCodeAt(e) !== 32 && text.charCodeAt(e) !== 9) e++;
|
|
244
|
-
out.push(k < e ? decimal(k, e) : 0);
|
|
245
|
-
k = e;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
// signed integer scanner for face records; `at` is the cursor
|
|
249
|
-
let at = 0;
|
|
250
|
-
const int = (end) => {
|
|
251
|
-
let sign = 1;
|
|
252
|
-
if (text.charCodeAt(at) === 45) {
|
|
253
|
-
sign = -1;
|
|
254
|
-
at++;
|
|
255
|
-
}
|
|
256
|
-
let v = 0;
|
|
257
|
-
let digits = 0;
|
|
258
|
-
for (let c = text.charCodeAt(at); at < end && c >= 48 && c <= 57; c = text.charCodeAt(++at)) {
|
|
259
|
-
v = v * 10 + (c - 48);
|
|
260
|
-
digits++;
|
|
261
|
-
}
|
|
262
|
-
return digits ? sign * v : 0;
|
|
263
|
-
};
|
|
264
|
-
// the corners of one face / line record, as output indices
|
|
265
|
-
const refs = [];
|
|
266
|
-
const readRefs = (start, end) => {
|
|
267
|
-
refs.length = 0;
|
|
268
|
-
at = start;
|
|
269
|
-
for (;;) {
|
|
270
|
-
while (at < end && (text.charCodeAt(at) === 32 || text.charCodeAt(at) === 9)) at++;
|
|
271
|
-
if (at >= end) break;
|
|
272
|
-
const v = int(end);
|
|
273
|
-
let t = 0, n = 0;
|
|
274
|
-
if (text.charCodeAt(at) === 47) {
|
|
275
|
-
at++;
|
|
276
|
-
t = int(end);
|
|
277
|
-
if (text.charCodeAt(at) === 47) {
|
|
278
|
-
at++;
|
|
279
|
-
n = int(end);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const c = text.charCodeAt(at);
|
|
283
|
-
if (at < end && c !== 32 && c !== 9) throw new Error(`Invalid OBJ vertex reference near "${text.slice(start, Math.min(end, start + 40))}".`);
|
|
284
|
-
refs.push(corner(v, t, n));
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
while (i < L) {
|
|
288
|
-
let e = text.indexOf('\n', i);
|
|
289
|
-
if (e < 0) e = L;
|
|
290
|
-
let s = i;
|
|
291
|
-
while (s < e && (text.charCodeAt(s) === 32 || text.charCodeAt(s) === 9)) s++;
|
|
292
|
-
const c0 = text.charCodeAt(s);
|
|
293
|
-
const c1 = text.charCodeAt(s + 1);
|
|
294
|
-
let end = e;
|
|
295
|
-
if (text.charCodeAt(end - 1) === 13) end--;
|
|
296
|
-
if (c0 === 118) {
|
|
297
|
-
// v, vn, vt
|
|
298
|
-
if (c1 === 32 || c1 === 9) readFloats(s + 2, end, pos, 3);
|
|
299
|
-
else if (c1 === 110) readFloats(s + 3, end, nrm, 3);
|
|
300
|
-
else if (c1 === 116) readFloats(s + 3, end, tex, 2);
|
|
301
|
-
} else if (c0 === 102 && (c1 === 32 || c1 === 9)) {
|
|
302
|
-
readRefs(s + 2, end);
|
|
303
|
-
for (let k = 2; k < refs.length; k++) group.idx.push(refs[0], refs[k - 1], refs[k]);
|
|
304
|
-
} else if (c0 === 108 && (c1 === 32 || c1 === 9)) {
|
|
305
|
-
readRefs(s + 2, end);
|
|
306
|
-
for (let k = 0; k + 1 < refs.length; k++) group.lines.push(refs[k], refs[k + 1]);
|
|
307
|
-
} else if (text.startsWith('usemtl', s)) {
|
|
308
|
-
useGroup(text.slice(s + 6, end).trim() || 'default');
|
|
309
|
-
}
|
|
310
|
-
i = e + 1;
|
|
311
|
-
}
|
|
312
|
-
const result = [];
|
|
313
|
-
let triangles = 0;
|
|
314
|
-
for (const g of groups.values()) {
|
|
315
|
-
if (g.idx.length) {
|
|
316
|
-
let mesh;
|
|
317
|
-
if (g.hasNormals) {
|
|
318
|
-
mesh = new Mesh({ positions: Float32Array.from(g.p), normals: Float32Array.from(g.n), uvs: Float32Array.from(g.t), indices: Uint32Array.from(g.idx) });
|
|
319
|
-
} else {
|
|
320
|
-
mesh = computeNormals(Float32Array.from(g.p), Uint32Array.from(g.idx), 30, g.hasUV ? Float32Array.from(g.t) : null);
|
|
321
|
-
}
|
|
322
|
-
result.push({ material: g.material, mesh });
|
|
323
|
-
triangles += g.idx.length / 3;
|
|
324
|
-
}
|
|
325
|
-
if (g.lines.length) {
|
|
326
|
-
result.push({ material: g.material, mesh: new Mesh({ positions: Float32Array.from(g.p), indices: Uint32Array.from(g.lines), kind: 'lines' }) });
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return { groups: result, materials: mtlText ? parseMtl(mtlText) : new Map(), stats: { vertices: pos.length / 3, triangles } };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/** Parse OBJ into a RenderScene (one identity instance per material group). */
|
|
333
|
-
export function objToScene(text, mtlText = '') {
|
|
334
|
-
const parsed = parseOBJ(text, mtlText);
|
|
335
|
-
const matIndex = new Map();
|
|
336
|
-
const materials = [];
|
|
337
|
-
const geometries = [];
|
|
338
|
-
const instances = new InstanceTable();
|
|
339
|
-
const I = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
340
|
-
for (const g of parsed.groups) {
|
|
341
|
-
let mi = matIndex.get(g.material);
|
|
342
|
-
if (mi === undefined) {
|
|
343
|
-
mi = materials.length;
|
|
344
|
-
matIndex.set(g.material, mi);
|
|
345
|
-
const m = parsed.materials.get(g.material);
|
|
346
|
-
materials.push(m ? { ...m } : { name: g.material });
|
|
347
|
-
}
|
|
348
|
-
geometries.push({ mesh: g.mesh });
|
|
349
|
-
instances.push(geometries.length - 1, mi, I);
|
|
350
|
-
}
|
|
351
|
-
return { ...buildExternalScene({ geometries, materials, instances }), parsed };
|
|
352
|
-
}
|
|
1
|
+
// Wavefront OBJ export (fully flattened triangles, the format has no
|
|
2
|
+
// instancing) and a reasonably fast OBJ parser used by the benchmark and the
|
|
3
|
+
// importer. The exporter is deliberately generous to OBJ: trimmed numbers,
|
|
4
|
+
// de-duplicated normals and texture coordinates, one usemtl per material run.
|
|
5
|
+
|
|
6
|
+
import { Mesh, computeNormals } from '../geometry/mesh.js';
|
|
7
|
+
import { linearToSrgb } from '../format/colors.js';
|
|
8
|
+
import { exportableInstances, materialNames, fixed, buildExternalScene, InstanceTable } from './external-scene.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('../scene/flatten.js').RenderScene} scene
|
|
12
|
+
* @param {(entry:object, lod:number)=>Mesh} meshOf
|
|
13
|
+
* @param {{lod?: number, digits?: number, name?: string, uvs?: 'auto'|boolean}} [opts]
|
|
14
|
+
* uvs: 'auto' writes texture coordinates only for textured materials
|
|
15
|
+
* @returns {{obj: string, mtl: string, vertices: number, triangles: number}}
|
|
16
|
+
*/
|
|
17
|
+
export function exportOBJ(scene, meshOf, { lod = 0, digits = 5, name = 'scene', uvs = 'auto' } = {}) {
|
|
18
|
+
const names = materialNames(scene.materials);
|
|
19
|
+
const out = [`# ES3D export: ${name}`, `mtllib ${name}.mtl`];
|
|
20
|
+
const vnIndex = new Map();
|
|
21
|
+
const vtIndex = new Map();
|
|
22
|
+
const vnLines = [];
|
|
23
|
+
const vtLines = [];
|
|
24
|
+
const vLines = [];
|
|
25
|
+
const faces = [];
|
|
26
|
+
let vCount = 0;
|
|
27
|
+
let triangles = 0;
|
|
28
|
+
let currentMat = -1;
|
|
29
|
+
const m = new Float64Array(16);
|
|
30
|
+
for (const i of exportableInstances(scene)) {
|
|
31
|
+
const entry = scene.geometries[scene.geom[i]];
|
|
32
|
+
const mesh = meshOf(entry, Math.min(lod, entry.lods - 1));
|
|
33
|
+
if (!mesh.vertexCount) continue;
|
|
34
|
+
for (let k = 0; k < 16; k++) m[k] = scene.matrices[i * 16 + k];
|
|
35
|
+
const mi = scene.mat[i];
|
|
36
|
+
if (mi !== currentMat) {
|
|
37
|
+
faces.push(`usemtl ${names[mi]}`);
|
|
38
|
+
currentMat = mi;
|
|
39
|
+
}
|
|
40
|
+
// normal matrix = cofactor (sign-corrected)
|
|
41
|
+
const c00 = m[5] * m[10] - m[6] * m[9], c01 = m[6] * m[8] - m[4] * m[10], c02 = m[4] * m[9] - m[5] * m[8];
|
|
42
|
+
const c10 = m[9] * m[2] - m[10] * m[1], c11 = m[10] * m[0] - m[8] * m[2], c12 = m[8] * m[1] - m[9] * m[0];
|
|
43
|
+
const c20 = m[1] * m[6] - m[2] * m[5], c21 = m[2] * m[4] - m[0] * m[6], c22 = m[0] * m[5] - m[1] * m[4];
|
|
44
|
+
const det = m[0] * c00 + m[1] * c01 + m[2] * c02;
|
|
45
|
+
const sg = det < 0 ? -1 : 1;
|
|
46
|
+
const p = mesh.positions, nr = mesh.normals, uv = mesh.uvs;
|
|
47
|
+
const n = mesh.vertexCount;
|
|
48
|
+
const material = scene.materials[mi];
|
|
49
|
+
const withUV = uvs === true || (uvs === 'auto' && !!(material.map || material.normalMap));
|
|
50
|
+
const vt = new Int32Array(n);
|
|
51
|
+
const vn = new Int32Array(n);
|
|
52
|
+
for (let k = 0; k < n; k++) {
|
|
53
|
+
const x = p[k * 3], y = p[k * 3 + 1], z = p[k * 3 + 2];
|
|
54
|
+
vLines.push(`v ${fixed(m[0] * x + m[4] * y + m[8] * z + m[12], digits)} ${fixed(m[1] * x + m[5] * y + m[9] * z + m[13], digits)} ${fixed(m[2] * x + m[6] * y + m[10] * z + m[14], digits)}`);
|
|
55
|
+
if (mesh.kind === 'triangles') {
|
|
56
|
+
const a = nr[k * 3], b = nr[k * 3 + 1], c = nr[k * 3 + 2];
|
|
57
|
+
let nx = (c00 * a + c10 * b + c20 * c) * sg, ny = (c01 * a + c11 * b + c21 * c) * sg, nz = (c02 * a + c12 * b + c22 * c) * sg;
|
|
58
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
59
|
+
const key = `${fixed(nx / l, 4)} ${fixed(ny / l, 4)} ${fixed(nz / l, 4)}`;
|
|
60
|
+
let ni = vnIndex.get(key);
|
|
61
|
+
if (ni === undefined) {
|
|
62
|
+
ni = vnLines.length + 1;
|
|
63
|
+
vnIndex.set(key, ni);
|
|
64
|
+
vnLines.push('vn ' + key);
|
|
65
|
+
}
|
|
66
|
+
vn[k] = ni;
|
|
67
|
+
if (!withUV) continue;
|
|
68
|
+
const tkey = `${fixed(uv[k * 2], 4)} ${fixed(uv[k * 2 + 1], 4)}`;
|
|
69
|
+
let ti = vtIndex.get(tkey);
|
|
70
|
+
if (ti === undefined) {
|
|
71
|
+
ti = vtLines.length + 1;
|
|
72
|
+
vtIndex.set(tkey, ti);
|
|
73
|
+
vtLines.push('vt ' + tkey);
|
|
74
|
+
}
|
|
75
|
+
vt[k] = ti;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const idx = mesh.indices;
|
|
79
|
+
if (mesh.kind === 'lines') {
|
|
80
|
+
for (let k = 0; k < idx.length; k += 2) faces.push(`l ${idx[k] + 1 + vCount} ${idx[k + 1] + 1 + vCount}`);
|
|
81
|
+
} else {
|
|
82
|
+
for (let k = 0; k < idx.length; k += 3) {
|
|
83
|
+
let a = idx[k], b = idx[k + 1], c = idx[k + 2];
|
|
84
|
+
if (det < 0) [b, c] = [c, b];
|
|
85
|
+
if (withUV) faces.push(`f ${a + 1 + vCount}/${vt[a]}/${vn[a]} ${b + 1 + vCount}/${vt[b]}/${vn[b]} ${c + 1 + vCount}/${vt[c]}/${vn[c]}`);
|
|
86
|
+
else faces.push(`f ${a + 1 + vCount}//${vn[a]} ${b + 1 + vCount}//${vn[b]} ${c + 1 + vCount}//${vn[c]}`);
|
|
87
|
+
}
|
|
88
|
+
triangles += idx.length / 3;
|
|
89
|
+
}
|
|
90
|
+
vCount += n;
|
|
91
|
+
}
|
|
92
|
+
const parts = [out, vLines, vtLines, vnLines, faces].filter((a) => a.length).map((a) => a.join('\n'));
|
|
93
|
+
const mtl = [];
|
|
94
|
+
scene.materials.forEach((mat, i) => {
|
|
95
|
+
const kd = mat.baseColor.slice(0, 3).map((c) => fixed(linearToSrgb(c), 4)).join(' ');
|
|
96
|
+
const ke = mat.emissive.map((c) => fixed(Math.min(1, linearToSrgb(c)), 4)).join(' ');
|
|
97
|
+
mtl.push(`newmtl ${names[i]}`, `Kd ${kd}`, `Ke ${ke}`, `d ${fixed(mat.opacity, 4)}`, `Pr ${fixed(mat.roughness, 4)}`, `Pm ${fixed(mat.metallic, 4)}`, '');
|
|
98
|
+
});
|
|
99
|
+
return { obj: parts.join('\n') + '\n', mtl: mtl.join('\n'), vertices: vCount, triangles };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// parsing
|
|
104
|
+
|
|
105
|
+
const POW10 = Array.from({ length: 23 }, (_, i) => 10 ** i); // exact doubles
|
|
106
|
+
|
|
107
|
+
function parseMtl(text) {
|
|
108
|
+
const mats = new Map();
|
|
109
|
+
let cur = null;
|
|
110
|
+
for (const raw of text.split('\n')) {
|
|
111
|
+
const line = raw.trim();
|
|
112
|
+
if (!line || line[0] === '#') continue;
|
|
113
|
+
const parts = line.split(/\s+/);
|
|
114
|
+
const k = parts[0];
|
|
115
|
+
if (k === 'newmtl') {
|
|
116
|
+
cur = { name: parts.slice(1).join(' '), color: [0.8, 0.8, 0.8, 1], metallic: 0, roughness: 0.5, opacity: 1, emissive: [0, 0, 0, 1] };
|
|
117
|
+
mats.set(cur.name, cur);
|
|
118
|
+
} else if (cur) {
|
|
119
|
+
const nums = parts.slice(1).map(Number);
|
|
120
|
+
if (k === 'Kd') cur.color = [nums[0], nums[1], nums[2], 1];
|
|
121
|
+
else if (k === 'Ke') cur.emissive = [nums[0], nums[1], nums[2], 1];
|
|
122
|
+
else if (k === 'd') cur.opacity = nums[0];
|
|
123
|
+
else if (k === 'Tr') cur.opacity = 1 - nums[0];
|
|
124
|
+
else if (k === 'Pr') cur.roughness = nums[0];
|
|
125
|
+
else if (k === 'Pm') cur.metallic = nums[0];
|
|
126
|
+
else if (k === 'Ns') cur.roughness = Math.sqrt(2 / (nums[0] + 2));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return mats;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse OBJ text (and optional MTL text) into meshes grouped by material.
|
|
134
|
+
* Faces are scanned character by character and corners are de-duplicated
|
|
135
|
+
* through per-position linked lists, so large files parse without per-corner
|
|
136
|
+
* string allocations.
|
|
137
|
+
* @returns {{groups: {material: string, mesh: Mesh}[], materials: Map, stats: object}}
|
|
138
|
+
*/
|
|
139
|
+
export function parseOBJ(text, mtlText = '') {
|
|
140
|
+
const pos = [];
|
|
141
|
+
const nrm = [];
|
|
142
|
+
const tex = [];
|
|
143
|
+
const groups = new Map();
|
|
144
|
+
let group = null;
|
|
145
|
+
const groupList = [];
|
|
146
|
+
const useGroup = (name) => {
|
|
147
|
+
group = groups.get(name);
|
|
148
|
+
if (!group) {
|
|
149
|
+
group = { material: name, id: groupList.length, p: [], n: [], t: [], idx: [], lines: [], hasNormals: true, hasUV: true };
|
|
150
|
+
groups.set(name, group);
|
|
151
|
+
groupList.push(group);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
useGroup('default');
|
|
155
|
+
// corner de-duplication: for every position index a linked list of the
|
|
156
|
+
// (group, texcoord, normal) combinations already emitted
|
|
157
|
+
let head = new Int32Array(1024).fill(-1);
|
|
158
|
+
let cap = 1024;
|
|
159
|
+
let nextE = new Int32Array(cap), gE = new Int32Array(cap), tE = new Int32Array(cap), nE = new Int32Array(cap), outE = new Int32Array(cap);
|
|
160
|
+
let entries = 0;
|
|
161
|
+
const growEntries = () => {
|
|
162
|
+
cap *= 2;
|
|
163
|
+
const grow = (a) => {
|
|
164
|
+
const b = new Int32Array(cap);
|
|
165
|
+
b.set(a);
|
|
166
|
+
return b;
|
|
167
|
+
};
|
|
168
|
+
nextE = grow(nextE);
|
|
169
|
+
gE = grow(gE);
|
|
170
|
+
tE = grow(tE);
|
|
171
|
+
nE = grow(nE);
|
|
172
|
+
outE = grow(outE);
|
|
173
|
+
};
|
|
174
|
+
const corner = (v, t, n) => {
|
|
175
|
+
if (v < 0) v = pos.length / 3 + v + 1;
|
|
176
|
+
if (t < 0) t = tex.length / 2 + t + 1;
|
|
177
|
+
if (n < 0) n = nrm.length / 3 + n + 1;
|
|
178
|
+
if (v < 1 || v > pos.length / 3) throw new Error(`OBJ face references vertex ${v}, but only ${pos.length / 3} are defined.`);
|
|
179
|
+
if (v > head.length) {
|
|
180
|
+
let len = head.length;
|
|
181
|
+
while (len < v) len *= 2;
|
|
182
|
+
const h = new Int32Array(len).fill(-1);
|
|
183
|
+
h.set(head);
|
|
184
|
+
head = h;
|
|
185
|
+
}
|
|
186
|
+
for (let e = head[v - 1]; e >= 0; e = nextE[e]) {
|
|
187
|
+
if (gE[e] === group.id && tE[e] === t && nE[e] === n) return outE[e];
|
|
188
|
+
}
|
|
189
|
+
const idx = group.p.length / 3;
|
|
190
|
+
group.p.push(pos[(v - 1) * 3], pos[(v - 1) * 3 + 1], pos[(v - 1) * 3 + 2]);
|
|
191
|
+
if (n) group.n.push(nrm[(n - 1) * 3], nrm[(n - 1) * 3 + 1], nrm[(n - 1) * 3 + 2]);
|
|
192
|
+
else {
|
|
193
|
+
group.hasNormals = false;
|
|
194
|
+
group.n.push(0, 0, 0);
|
|
195
|
+
}
|
|
196
|
+
if (t) group.t.push(tex[(t - 1) * 2], tex[(t - 1) * 2 + 1]);
|
|
197
|
+
else {
|
|
198
|
+
group.hasUV = false;
|
|
199
|
+
group.t.push(0, 0);
|
|
200
|
+
}
|
|
201
|
+
if (entries === cap) growEntries();
|
|
202
|
+
gE[entries] = group.id;
|
|
203
|
+
tE[entries] = t;
|
|
204
|
+
nE[entries] = n;
|
|
205
|
+
outE[entries] = idx;
|
|
206
|
+
nextE[entries] = head[v - 1];
|
|
207
|
+
head[v - 1] = entries++;
|
|
208
|
+
return idx;
|
|
209
|
+
};
|
|
210
|
+
let i = 0;
|
|
211
|
+
const L = text.length;
|
|
212
|
+
// plain decimals (the common case) are converted with one exact division,
|
|
213
|
+
// which gives the same correctly rounded double as Number(); anything else
|
|
214
|
+
// (exponents, very long mantissas) goes through Number()
|
|
215
|
+
const decimal = (start, end) => {
|
|
216
|
+
let k = start;
|
|
217
|
+
let neg = false;
|
|
218
|
+
const c0 = text.charCodeAt(k);
|
|
219
|
+
if (c0 === 45 || c0 === 43) {
|
|
220
|
+
neg = c0 === 45;
|
|
221
|
+
k++;
|
|
222
|
+
}
|
|
223
|
+
let mant = 0, scale = 0, dot = false, digits = 0;
|
|
224
|
+
for (; k < end; k++) {
|
|
225
|
+
const c = text.charCodeAt(k);
|
|
226
|
+
if (c >= 48 && c <= 57) {
|
|
227
|
+
if (mant > 9e14) return +text.slice(start, end);
|
|
228
|
+
mant = mant * 10 + (c - 48);
|
|
229
|
+
digits++;
|
|
230
|
+
if (dot) scale++;
|
|
231
|
+
} else if (c === 46 && !dot) dot = true;
|
|
232
|
+
else return +text.slice(start, end);
|
|
233
|
+
}
|
|
234
|
+
if (!digits || scale > 22) return +text.slice(start, end);
|
|
235
|
+
const v = scale ? mant / POW10[scale] : mant;
|
|
236
|
+
return neg ? -v : v;
|
|
237
|
+
};
|
|
238
|
+
const readFloats = (start, end, out, count) => {
|
|
239
|
+
let k = start;
|
|
240
|
+
for (let c = 0; c < count; c++) {
|
|
241
|
+
while (k < end && (text.charCodeAt(k) === 32 || text.charCodeAt(k) === 9)) k++;
|
|
242
|
+
let e = k;
|
|
243
|
+
while (e < end && text.charCodeAt(e) !== 32 && text.charCodeAt(e) !== 9) e++;
|
|
244
|
+
out.push(k < e ? decimal(k, e) : 0);
|
|
245
|
+
k = e;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
// signed integer scanner for face records; `at` is the cursor
|
|
249
|
+
let at = 0;
|
|
250
|
+
const int = (end) => {
|
|
251
|
+
let sign = 1;
|
|
252
|
+
if (text.charCodeAt(at) === 45) {
|
|
253
|
+
sign = -1;
|
|
254
|
+
at++;
|
|
255
|
+
}
|
|
256
|
+
let v = 0;
|
|
257
|
+
let digits = 0;
|
|
258
|
+
for (let c = text.charCodeAt(at); at < end && c >= 48 && c <= 57; c = text.charCodeAt(++at)) {
|
|
259
|
+
v = v * 10 + (c - 48);
|
|
260
|
+
digits++;
|
|
261
|
+
}
|
|
262
|
+
return digits ? sign * v : 0;
|
|
263
|
+
};
|
|
264
|
+
// the corners of one face / line record, as output indices
|
|
265
|
+
const refs = [];
|
|
266
|
+
const readRefs = (start, end) => {
|
|
267
|
+
refs.length = 0;
|
|
268
|
+
at = start;
|
|
269
|
+
for (;;) {
|
|
270
|
+
while (at < end && (text.charCodeAt(at) === 32 || text.charCodeAt(at) === 9)) at++;
|
|
271
|
+
if (at >= end) break;
|
|
272
|
+
const v = int(end);
|
|
273
|
+
let t = 0, n = 0;
|
|
274
|
+
if (text.charCodeAt(at) === 47) {
|
|
275
|
+
at++;
|
|
276
|
+
t = int(end);
|
|
277
|
+
if (text.charCodeAt(at) === 47) {
|
|
278
|
+
at++;
|
|
279
|
+
n = int(end);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const c = text.charCodeAt(at);
|
|
283
|
+
if (at < end && c !== 32 && c !== 9) throw new Error(`Invalid OBJ vertex reference near "${text.slice(start, Math.min(end, start + 40))}".`);
|
|
284
|
+
refs.push(corner(v, t, n));
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
while (i < L) {
|
|
288
|
+
let e = text.indexOf('\n', i);
|
|
289
|
+
if (e < 0) e = L;
|
|
290
|
+
let s = i;
|
|
291
|
+
while (s < e && (text.charCodeAt(s) === 32 || text.charCodeAt(s) === 9)) s++;
|
|
292
|
+
const c0 = text.charCodeAt(s);
|
|
293
|
+
const c1 = text.charCodeAt(s + 1);
|
|
294
|
+
let end = e;
|
|
295
|
+
if (text.charCodeAt(end - 1) === 13) end--;
|
|
296
|
+
if (c0 === 118) {
|
|
297
|
+
// v, vn, vt
|
|
298
|
+
if (c1 === 32 || c1 === 9) readFloats(s + 2, end, pos, 3);
|
|
299
|
+
else if (c1 === 110) readFloats(s + 3, end, nrm, 3);
|
|
300
|
+
else if (c1 === 116) readFloats(s + 3, end, tex, 2);
|
|
301
|
+
} else if (c0 === 102 && (c1 === 32 || c1 === 9)) {
|
|
302
|
+
readRefs(s + 2, end);
|
|
303
|
+
for (let k = 2; k < refs.length; k++) group.idx.push(refs[0], refs[k - 1], refs[k]);
|
|
304
|
+
} else if (c0 === 108 && (c1 === 32 || c1 === 9)) {
|
|
305
|
+
readRefs(s + 2, end);
|
|
306
|
+
for (let k = 0; k + 1 < refs.length; k++) group.lines.push(refs[k], refs[k + 1]);
|
|
307
|
+
} else if (text.startsWith('usemtl', s)) {
|
|
308
|
+
useGroup(text.slice(s + 6, end).trim() || 'default');
|
|
309
|
+
}
|
|
310
|
+
i = e + 1;
|
|
311
|
+
}
|
|
312
|
+
const result = [];
|
|
313
|
+
let triangles = 0;
|
|
314
|
+
for (const g of groups.values()) {
|
|
315
|
+
if (g.idx.length) {
|
|
316
|
+
let mesh;
|
|
317
|
+
if (g.hasNormals) {
|
|
318
|
+
mesh = new Mesh({ positions: Float32Array.from(g.p), normals: Float32Array.from(g.n), uvs: Float32Array.from(g.t), indices: Uint32Array.from(g.idx) });
|
|
319
|
+
} else {
|
|
320
|
+
mesh = computeNormals(Float32Array.from(g.p), Uint32Array.from(g.idx), 30, g.hasUV ? Float32Array.from(g.t) : null);
|
|
321
|
+
}
|
|
322
|
+
result.push({ material: g.material, mesh });
|
|
323
|
+
triangles += g.idx.length / 3;
|
|
324
|
+
}
|
|
325
|
+
if (g.lines.length) {
|
|
326
|
+
result.push({ material: g.material, mesh: new Mesh({ positions: Float32Array.from(g.p), indices: Uint32Array.from(g.lines), kind: 'lines' }) });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { groups: result, materials: mtlText ? parseMtl(mtlText) : new Map(), stats: { vertices: pos.length / 3, triangles } };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Parse OBJ into a RenderScene (one identity instance per material group). */
|
|
333
|
+
export function objToScene(text, mtlText = '') {
|
|
334
|
+
const parsed = parseOBJ(text, mtlText);
|
|
335
|
+
const matIndex = new Map();
|
|
336
|
+
const materials = [];
|
|
337
|
+
const geometries = [];
|
|
338
|
+
const instances = new InstanceTable();
|
|
339
|
+
const I = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
340
|
+
for (const g of parsed.groups) {
|
|
341
|
+
let mi = matIndex.get(g.material);
|
|
342
|
+
if (mi === undefined) {
|
|
343
|
+
mi = materials.length;
|
|
344
|
+
matIndex.set(g.material, mi);
|
|
345
|
+
const m = parsed.materials.get(g.material);
|
|
346
|
+
materials.push(m ? { ...m } : { name: g.material });
|
|
347
|
+
}
|
|
348
|
+
geometries.push({ mesh: g.mesh });
|
|
349
|
+
instances.push(geometries.length - 1, mi, I);
|
|
350
|
+
}
|
|
351
|
+
return { ...buildExternalScene({ geometries, materials, instances }), parsed };
|
|
352
|
+
}
|