@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/geometry/mesh.js
CHANGED
|
@@ -1,458 +1,458 @@
|
|
|
1
|
-
// Mesh data structure and building helpers.
|
|
2
|
-
//
|
|
3
|
-
// A Mesh is an indexed triangle (or line) list with interleavable attributes:
|
|
4
|
-
// positions (xyz), normals (xyz), uvs (uv). Generators produce meshes in a
|
|
5
|
-
// canonical local space; the renderer packs them into shared GPU buffers.
|
|
6
|
-
|
|
7
|
-
import { computeNormalsWasm } from '../wasm/kernels.js';
|
|
8
|
-
|
|
9
|
-
export class Mesh {
|
|
10
|
-
/**
|
|
11
|
-
* @param {{positions: Float32Array, normals?: Float32Array, uvs?: Float32Array, indices: Uint32Array, kind?: 'triangles'|'lines'}} o
|
|
12
|
-
*/
|
|
13
|
-
constructor(o) {
|
|
14
|
-
this.positions = o.positions;
|
|
15
|
-
const n = this.positions.length / 3;
|
|
16
|
-
this.normals = o.normals || new Float32Array(n * 3);
|
|
17
|
-
this.uvs = o.uvs || new Float32Array(n * 2);
|
|
18
|
-
this.indices = o.indices;
|
|
19
|
-
this.kind = o.kind || 'triangles';
|
|
20
|
-
this._bounds = null;
|
|
21
|
-
this._edges = null;
|
|
22
|
-
this._normalLines = null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get vertexCount() {
|
|
26
|
-
return this.positions.length / 3;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
get primitiveCount() {
|
|
30
|
-
return this.kind === 'lines' ? this.indices.length / 2 : this.indices.length / 3;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
get byteSize() {
|
|
34
|
-
return this.positions.byteLength + this.normals.byteLength + this.uvs.byteLength + this.indices.byteLength;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get bounds() {
|
|
38
|
-
if (!this._bounds) {
|
|
39
|
-
const p = this.positions;
|
|
40
|
-
const min = [Infinity, Infinity, Infinity];
|
|
41
|
-
const max = [-Infinity, -Infinity, -Infinity];
|
|
42
|
-
for (let i = 0; i < p.length; i += 3) {
|
|
43
|
-
for (let k = 0; k < 3; k++) {
|
|
44
|
-
const v = p[i + k];
|
|
45
|
-
if (v < min[k]) min[k] = v;
|
|
46
|
-
if (v > max[k]) max[k] = v;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (p.length === 0) {
|
|
50
|
-
min.fill(0);
|
|
51
|
-
max.fill(0);
|
|
52
|
-
}
|
|
53
|
-
this._bounds = { min, max };
|
|
54
|
-
}
|
|
55
|
-
return this._bounds;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Unique edges as a line index list (for wireframe display). */
|
|
59
|
-
edges() {
|
|
60
|
-
if (this._edges) return this._edges;
|
|
61
|
-
if (this.kind === 'lines') return (this._edges = this.indices);
|
|
62
|
-
// weld positions so that seams do not show as edges twice
|
|
63
|
-
const weld = weldMap(this.positions);
|
|
64
|
-
const seen = new Set();
|
|
65
|
-
const out = [];
|
|
66
|
-
const idx = this.indices;
|
|
67
|
-
const nv = this.vertexCount;
|
|
68
|
-
for (let i = 0; i < idx.length; i += 3) {
|
|
69
|
-
for (let k = 0; k < 3; k++) {
|
|
70
|
-
const a = idx[i + k];
|
|
71
|
-
const b = idx[i + ((k + 1) % 3)];
|
|
72
|
-
const wa = weld[a], wb = weld[b];
|
|
73
|
-
if (wa === wb) continue;
|
|
74
|
-
const key = wa < wb ? wa * nv + wb : wb * nv + wa;
|
|
75
|
-
if (seen.has(key)) continue;
|
|
76
|
-
seen.add(key);
|
|
77
|
-
out.push(a, b);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
this._edges = Uint32Array.from(out);
|
|
81
|
-
return this._edges;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Line vertices for normal display: [p, n, flag] where flag 0 = base, 1 = tip. */
|
|
85
|
-
normalLines() {
|
|
86
|
-
if (this._normalLines) return this._normalLines;
|
|
87
|
-
const n = this.vertexCount;
|
|
88
|
-
const positions = new Float32Array(n * 6);
|
|
89
|
-
const normals = new Float32Array(n * 6);
|
|
90
|
-
const uvs = new Float32Array(n * 4);
|
|
91
|
-
const indices = new Uint32Array(n * 2);
|
|
92
|
-
for (let i = 0; i < n; i++) {
|
|
93
|
-
for (let k = 0; k < 3; k++) {
|
|
94
|
-
positions[i * 6 + k] = this.positions[i * 3 + k];
|
|
95
|
-
positions[i * 6 + 3 + k] = this.positions[i * 3 + k];
|
|
96
|
-
normals[i * 6 + k] = this.normals[i * 3 + k];
|
|
97
|
-
normals[i * 6 + 3 + k] = this.normals[i * 3 + k];
|
|
98
|
-
}
|
|
99
|
-
uvs[i * 4 + 2] = 1; // tip flag in u
|
|
100
|
-
indices[i * 2] = i * 2;
|
|
101
|
-
indices[i * 2 + 1] = i * 2 + 1;
|
|
102
|
-
}
|
|
103
|
-
this._normalLines = new Mesh({ positions, normals, uvs, indices, kind: 'lines' });
|
|
104
|
-
return this._normalLines;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Map each vertex to the first vertex with the same position (quantized to 1e-6). */
|
|
109
|
-
export function weldMap(positions) {
|
|
110
|
-
const n = positions.length / 3;
|
|
111
|
-
const map = new Uint32Array(n);
|
|
112
|
-
// open addressing hash table over quantized coordinates (no string keys)
|
|
113
|
-
let size = 16;
|
|
114
|
-
while (size < n * 2) size *= 2;
|
|
115
|
-
const mask = size - 1;
|
|
116
|
-
const table = new Int32Array(size).fill(-1);
|
|
117
|
-
const q = new Float64Array(n * 3);
|
|
118
|
-
for (let i = 0; i < n * 3; i++) q[i] = Math.round(positions[i] * 1e6);
|
|
119
|
-
for (let i = 0; i < n; i++) {
|
|
120
|
-
const x = q[i * 3], y = q[i * 3 + 1], z = q[i * 3 + 2];
|
|
121
|
-
let h = (Math.imul(x | 0, 73856093) ^ Math.imul(y | 0, 19349663) ^ Math.imul(z | 0, 83492791)) & mask;
|
|
122
|
-
for (;;) {
|
|
123
|
-
const f = table[h];
|
|
124
|
-
if (f < 0) {
|
|
125
|
-
table[h] = i;
|
|
126
|
-
map[i] = i;
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
if (q[f * 3] === x && q[f * 3 + 1] === y && q[f * 3 + 2] === z) {
|
|
130
|
-
map[i] = f;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
h = (h + 1) & mask;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return map;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Growable mesh builder. */
|
|
140
|
-
export class MeshBuilder {
|
|
141
|
-
/**
|
|
142
|
-
* @param {'triangles'|'lines'} kind
|
|
143
|
-
* @param {number} vertexCapacity vertices to allocate for; a generator that
|
|
144
|
-
* knows its exact counts passes them, so nothing grows or is copied
|
|
145
|
-
* @param {number} indexCapacity
|
|
146
|
-
*/
|
|
147
|
-
constructor(kind = 'triangles', vertexCapacity = 256, indexCapacity = 768) {
|
|
148
|
-
this.kind = kind;
|
|
149
|
-
vertexCapacity = Math.max(1, vertexCapacity);
|
|
150
|
-
indexCapacity = Math.max(1, indexCapacity);
|
|
151
|
-
this.p = new Float32Array(vertexCapacity * 3);
|
|
152
|
-
this.n = new Float32Array(vertexCapacity * 3);
|
|
153
|
-
this.t = new Float32Array(vertexCapacity * 2);
|
|
154
|
-
this.i = new Uint32Array(indexCapacity);
|
|
155
|
-
this.vc = 0;
|
|
156
|
-
this.ic = 0;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
growV(extra) {
|
|
160
|
-
const need = this.vc + extra;
|
|
161
|
-
if (need * 3 <= this.p.length) return;
|
|
162
|
-
let cap = this.p.length / 3;
|
|
163
|
-
while (cap < need) cap *= 2;
|
|
164
|
-
const p = new Float32Array(cap * 3);
|
|
165
|
-
p.set(this.p);
|
|
166
|
-
const n = new Float32Array(cap * 3);
|
|
167
|
-
n.set(this.n);
|
|
168
|
-
const t = new Float32Array(cap * 2);
|
|
169
|
-
t.set(this.t);
|
|
170
|
-
this.p = p;
|
|
171
|
-
this.n = n;
|
|
172
|
-
this.t = t;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
growI(extra) {
|
|
176
|
-
const need = this.ic + extra;
|
|
177
|
-
if (need <= this.i.length) return;
|
|
178
|
-
let cap = this.i.length;
|
|
179
|
-
while (cap < need) cap *= 2;
|
|
180
|
-
const i = new Uint32Array(cap);
|
|
181
|
-
i.set(this.i);
|
|
182
|
-
this.i = i;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
vertex(px, py, pz, nx = 0, ny = 0, nz = 0, u = 0, v = 0) {
|
|
186
|
-
if (this.vc * 3 >= this.p.length) this.growV(1);
|
|
187
|
-
const k = this.vc;
|
|
188
|
-
this.p[k * 3] = px;
|
|
189
|
-
this.p[k * 3 + 1] = py;
|
|
190
|
-
this.p[k * 3 + 2] = pz;
|
|
191
|
-
this.n[k * 3] = nx;
|
|
192
|
-
this.n[k * 3 + 1] = ny;
|
|
193
|
-
this.n[k * 3 + 2] = nz;
|
|
194
|
-
this.t[k * 2] = u;
|
|
195
|
-
this.t[k * 2 + 1] = v;
|
|
196
|
-
this.vc++;
|
|
197
|
-
return k;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
tri(a, b, c) {
|
|
201
|
-
if (this.ic + 3 > this.i.length) this.growI(3);
|
|
202
|
-
this.i[this.ic++] = a;
|
|
203
|
-
this.i[this.ic++] = b;
|
|
204
|
-
this.i[this.ic++] = c;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Quad a-b-c-d in counter-clockwise order (as seen from the front). */
|
|
208
|
-
quad(a, b, c, d) {
|
|
209
|
-
this.tri(a, b, c);
|
|
210
|
-
this.tri(a, c, d);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
line(a, b) {
|
|
214
|
-
if (this.ic + 2 > this.i.length) this.growI(2);
|
|
215
|
-
this.i[this.ic++] = a;
|
|
216
|
-
this.i[this.ic++] = b;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
get vertexCount() {
|
|
220
|
-
return this.vc;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
build() {
|
|
224
|
-
// a builder sized exactly (generators that count first) hands over its arrays
|
|
225
|
-
const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
|
|
226
|
-
return new Mesh({
|
|
227
|
-
positions: exact(this.p, this.vc * 3),
|
|
228
|
-
normals: exact(this.n, this.vc * 3),
|
|
229
|
-
uvs: exact(this.t, this.vc * 2),
|
|
230
|
-
indices: exact(this.i, this.ic),
|
|
231
|
-
kind: this.kind,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Every generator writes through a builder, and a builder lives only while
|
|
237
|
-
// one mesh is made. When a full collection finds no builder alive, V8 drops
|
|
238
|
-
// the hidden class of the instances and discards all optimized code that
|
|
239
|
-
// relied on it, so the next scene would run the generators unoptimized again.
|
|
240
|
-
// This one never dies, and neither does the class.
|
|
241
|
-
MeshBuilder.shapeKeeper = new MeshBuilder('triangles', 1, 1);
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Compute vertex normals for an indexed triangle soup with a crease angle:
|
|
245
|
-
* faces meeting at an angle larger than the crease get split normals.
|
|
246
|
-
* Returns a new mesh (vertices may be duplicated at creases).
|
|
247
|
-
*/
|
|
248
|
-
export function computeNormals(positions, indices, creaseDeg = 30, uvs = null) {
|
|
249
|
-
const crease = Math.min(Math.max(creaseDeg, 0), 180);
|
|
250
|
-
const cosCrease = Math.cos((crease * Math.PI) / 180);
|
|
251
|
-
if (crease === crease) {
|
|
252
|
-
const w = computeNormalsWasm(positions, indices, crease, cosCrease, uvs);
|
|
253
|
-
if (w) return new Mesh({ ...w, kind: 'triangles' });
|
|
254
|
-
}
|
|
255
|
-
const nt = indices.length / 3;
|
|
256
|
-
const faceN = new Float64Array(nt * 3); // area weighted (not normalized)
|
|
257
|
-
const faceU = new Float64Array(nt * 3); // unit length
|
|
258
|
-
for (let t = 0; t < nt; t++) {
|
|
259
|
-
const a = indices[t * 3] * 3, b = indices[t * 3 + 1] * 3, c = indices[t * 3 + 2] * 3;
|
|
260
|
-
const e1x = positions[b] - positions[a], e1y = positions[b + 1] - positions[a + 1], e1z = positions[b + 2] - positions[a + 2];
|
|
261
|
-
const e2x = positions[c] - positions[a], e2y = positions[c + 1] - positions[a + 1], e2z = positions[c + 2] - positions[a + 2];
|
|
262
|
-
const x = e1y * e2z - e1z * e2y, y = e1z * e2x - e1x * e2z, z = e1x * e2y - e1y * e2x;
|
|
263
|
-
faceN[t * 3] = x;
|
|
264
|
-
faceN[t * 3 + 1] = y;
|
|
265
|
-
faceN[t * 3 + 2] = z;
|
|
266
|
-
const l = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
267
|
-
faceU[t * 3] = x / l;
|
|
268
|
-
faceU[t * 3 + 1] = y / l;
|
|
269
|
-
faceU[t * 3 + 2] = z / l;
|
|
270
|
-
}
|
|
271
|
-
const weld = weldMap(positions);
|
|
272
|
-
const nv = positions.length / 3;
|
|
273
|
-
// faces incident to each welded vertex (CSR layout), needed only to test the crease angle
|
|
274
|
-
let counts = null, inc = null;
|
|
275
|
-
if (crease > 0 && crease < 180) {
|
|
276
|
-
counts = new Uint32Array(nv + 1);
|
|
277
|
-
for (let i = 0; i < indices.length; i++) counts[weld[indices[i]] + 1]++;
|
|
278
|
-
for (let i = 0; i < nv; i++) counts[i + 1] += counts[i];
|
|
279
|
-
inc = new Uint32Array(indices.length);
|
|
280
|
-
const fill = counts.slice(0, nv);
|
|
281
|
-
for (let i = 0; i < indices.length; i++) inc[fill[weld[indices[i]]]++] = (i / 3) | 0;
|
|
282
|
-
}
|
|
283
|
-
// without creases every corner of a welded vertex gets the same normal:
|
|
284
|
-
// sum the faces, then normalize once per welded vertex
|
|
285
|
-
let smoothN = null;
|
|
286
|
-
if (crease >= 180) {
|
|
287
|
-
smoothN = new Float64Array(nv * 3);
|
|
288
|
-
for (let i = 0; i < indices.length; i++) {
|
|
289
|
-
const w = weld[indices[i]] * 3, f = ((i / 3) | 0) * 3;
|
|
290
|
-
smoothN[w] += faceN[f];
|
|
291
|
-
smoothN[w + 1] += faceN[f + 1];
|
|
292
|
-
smoothN[w + 2] += faceN[f + 2];
|
|
293
|
-
}
|
|
294
|
-
for (let w = 0; w < nv * 3; w += 3) {
|
|
295
|
-
const x = smoothN[w], y = smoothN[w + 1], z = smoothN[w + 2];
|
|
296
|
-
const l = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
297
|
-
smoothN[w] = x / l;
|
|
298
|
-
smoothN[w + 1] = y / l;
|
|
299
|
-
smoothN[w + 2] = z / l;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// output vertices: as many as the input until creases split some
|
|
303
|
-
let cap = Math.max(1, nv);
|
|
304
|
-
let outP = new Float32Array(cap * 3), outN = new Float32Array(cap * 3), outT = new Float32Array(cap * 2);
|
|
305
|
-
const outI = new Uint32Array(indices.length);
|
|
306
|
-
let vc = 0;
|
|
307
|
-
let smoothOk = smoothN !== null;
|
|
308
|
-
for (let k = 0; smoothOk && k < nv * 3; k++) if (smoothN[k] !== smoothN[k]) smoothOk = false;
|
|
309
|
-
if (smoothOk) {
|
|
310
|
-
// one normal per welded vertex, so every input vertex becomes exactly one
|
|
311
|
-
// output vertex, in the order the triangles first use it
|
|
312
|
-
const out = new Int32Array(nv).fill(-1);
|
|
313
|
-
for (let i = 0; i < indices.length; i++) {
|
|
314
|
-
const vi = indices[i];
|
|
315
|
-
let idx = out[vi];
|
|
316
|
-
if (idx < 0) {
|
|
317
|
-
idx = out[vi] = vc++;
|
|
318
|
-
const w = weld[vi] * 3;
|
|
319
|
-
outP[idx * 3] = positions[vi * 3];
|
|
320
|
-
outP[idx * 3 + 1] = positions[vi * 3 + 1];
|
|
321
|
-
outP[idx * 3 + 2] = positions[vi * 3 + 2];
|
|
322
|
-
outN[idx * 3] = smoothN[w];
|
|
323
|
-
outN[idx * 3 + 1] = smoothN[w + 1];
|
|
324
|
-
outN[idx * 3 + 2] = smoothN[w + 2];
|
|
325
|
-
if (uvs) {
|
|
326
|
-
outT[idx * 2] = uvs[vi * 2];
|
|
327
|
-
outT[idx * 2 + 1] = uvs[vi * 2 + 1];
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
outI[i] = idx;
|
|
331
|
-
}
|
|
332
|
-
} else {
|
|
333
|
-
// output vertices already emitted for each input vertex, as a linked list
|
|
334
|
-
// keyed by the normal quantized to 1e-4 (corners with equal normals share)
|
|
335
|
-
const head = new Int32Array(nv).fill(-1);
|
|
336
|
-
let next = new Int32Array(cap);
|
|
337
|
-
let qn = new Int32Array(cap * 3);
|
|
338
|
-
for (let i = 0; i < indices.length; i++) {
|
|
339
|
-
const t = (i / 3) | 0;
|
|
340
|
-
const vi = indices[i];
|
|
341
|
-
const w = weld[vi];
|
|
342
|
-
let nx = 0, ny = 0, nz = 0;
|
|
343
|
-
if (crease <= 0) {
|
|
344
|
-
nx = faceU[t * 3];
|
|
345
|
-
ny = faceU[t * 3 + 1];
|
|
346
|
-
nz = faceU[t * 3 + 2];
|
|
347
|
-
} else if (smoothN) {
|
|
348
|
-
nx = smoothN[w * 3];
|
|
349
|
-
ny = smoothN[w * 3 + 1];
|
|
350
|
-
nz = smoothN[w * 3 + 2];
|
|
351
|
-
} else {
|
|
352
|
-
// faces within the crease angle of this corner's face
|
|
353
|
-
const fx = faceU[t * 3], fy = faceU[t * 3 + 1], fz = faceU[t * 3 + 2];
|
|
354
|
-
for (let k = counts[w]; k < counts[w + 1]; k++) {
|
|
355
|
-
const f = inc[k];
|
|
356
|
-
if (f === t || faceU[f * 3] * fx + faceU[f * 3 + 1] * fy + faceU[f * 3 + 2] * fz >= cosCrease) {
|
|
357
|
-
nx += faceN[f * 3];
|
|
358
|
-
ny += faceN[f * 3 + 1];
|
|
359
|
-
nz += faceN[f * 3 + 2];
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
const l = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
363
|
-
nx /= l;
|
|
364
|
-
ny /= l;
|
|
365
|
-
nz /= l;
|
|
366
|
-
}
|
|
367
|
-
const qx = Math.round(nx * 1e4), qy = Math.round(ny * 1e4), qz = Math.round(nz * 1e4);
|
|
368
|
-
let idx = head[vi];
|
|
369
|
-
while (idx >= 0 && !(qn[idx * 3] === qx && qn[idx * 3 + 1] === qy && qn[idx * 3 + 2] === qz)) idx = next[idx];
|
|
370
|
-
if (idx < 0) {
|
|
371
|
-
idx = vc++;
|
|
372
|
-
if (idx >= cap) {
|
|
373
|
-
cap *= 2;
|
|
374
|
-
const grow = (a, size) => {
|
|
375
|
-
const g = new a.constructor(cap * size);
|
|
376
|
-
g.set(a);
|
|
377
|
-
return g;
|
|
378
|
-
};
|
|
379
|
-
outP = grow(outP, 3);
|
|
380
|
-
outN = grow(outN, 3);
|
|
381
|
-
outT = grow(outT, 2);
|
|
382
|
-
next = grow(next, 1);
|
|
383
|
-
qn = grow(qn, 3);
|
|
384
|
-
}
|
|
385
|
-
outP[idx * 3] = positions[vi * 3];
|
|
386
|
-
outP[idx * 3 + 1] = positions[vi * 3 + 1];
|
|
387
|
-
outP[idx * 3 + 2] = positions[vi * 3 + 2];
|
|
388
|
-
outN[idx * 3] = nx;
|
|
389
|
-
outN[idx * 3 + 1] = ny;
|
|
390
|
-
outN[idx * 3 + 2] = nz;
|
|
391
|
-
if (uvs) {
|
|
392
|
-
outT[idx * 2] = uvs[vi * 2];
|
|
393
|
-
outT[idx * 2 + 1] = uvs[vi * 2 + 1];
|
|
394
|
-
}
|
|
395
|
-
next[idx] = head[vi];
|
|
396
|
-
head[vi] = idx;
|
|
397
|
-
qn[idx * 3] = qx;
|
|
398
|
-
qn[idx * 3 + 1] = qy;
|
|
399
|
-
qn[idx * 3 + 2] = qz;
|
|
400
|
-
}
|
|
401
|
-
outI[i] = idx;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
|
|
405
|
-
return new Mesh({ positions: exact(outP, vc * 3), normals: exact(outN, vc * 3), uvs: exact(outT, vc * 2), indices: outI, kind: 'triangles' });
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/** Merge meshes transformed by matrices (used by exporters). */
|
|
409
|
-
export function mergeTransformed(items) {
|
|
410
|
-
let vc = 0;
|
|
411
|
-
let ic = 0;
|
|
412
|
-
for (const { mesh } of items) {
|
|
413
|
-
vc += mesh.vertexCount;
|
|
414
|
-
ic += mesh.indices.length;
|
|
415
|
-
}
|
|
416
|
-
const positions = new Float32Array(vc * 3);
|
|
417
|
-
const normals = new Float32Array(vc * 3);
|
|
418
|
-
const uvs = new Float32Array(vc * 2);
|
|
419
|
-
const indices = new Uint32Array(ic);
|
|
420
|
-
let vo = 0;
|
|
421
|
-
let io = 0;
|
|
422
|
-
for (const { mesh, matrix: m } of items) {
|
|
423
|
-
const p = mesh.positions, n = mesh.normals;
|
|
424
|
-
// normal matrix = cofactor of the upper 3x3 (with determinant sign)
|
|
425
|
-
const c0 = [m[5] * m[10] - m[6] * m[9], m[6] * m[8] - m[4] * m[10], m[4] * m[9] - m[5] * m[8]];
|
|
426
|
-
const c1 = [m[9] * m[2] - m[10] * m[1], m[10] * m[0] - m[8] * m[2], m[8] * m[1] - m[9] * m[0]];
|
|
427
|
-
const c2 = [m[1] * m[6] - m[2] * m[5], m[2] * m[4] - m[0] * m[6], m[0] * m[5] - m[1] * m[4]];
|
|
428
|
-
const det = m[0] * c0[0] + m[1] * c0[1] + m[2] * c0[2];
|
|
429
|
-
const sgn = det < 0 ? -1 : 1;
|
|
430
|
-
for (let i = 0; i < mesh.vertexCount; i++) {
|
|
431
|
-
const x = p[i * 3], y = p[i * 3 + 1], z = p[i * 3 + 2];
|
|
432
|
-
positions[(vo + i) * 3] = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
433
|
-
positions[(vo + i) * 3 + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
434
|
-
positions[(vo + i) * 3 + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
435
|
-
const nx = n[i * 3], ny = n[i * 3 + 1], nz = n[i * 3 + 2];
|
|
436
|
-
let ox = (c0[0] * nx + c1[0] * ny + c2[0] * nz) * sgn;
|
|
437
|
-
let oy = (c0[1] * nx + c1[1] * ny + c2[1] * nz) * sgn;
|
|
438
|
-
let oz = (c0[2] * nx + c1[2] * ny + c2[2] * nz) * sgn;
|
|
439
|
-
const l = Math.hypot(ox, oy, oz) || 1;
|
|
440
|
-
normals[(vo + i) * 3] = ox / l;
|
|
441
|
-
normals[(vo + i) * 3 + 1] = oy / l;
|
|
442
|
-
normals[(vo + i) * 3 + 2] = oz / l;
|
|
443
|
-
uvs[(vo + i) * 2] = mesh.uvs[i * 2];
|
|
444
|
-
uvs[(vo + i) * 2 + 1] = mesh.uvs[i * 2 + 1];
|
|
445
|
-
}
|
|
446
|
-
const flip = det < 0 && mesh.kind === 'triangles';
|
|
447
|
-
for (let i = 0; i < mesh.indices.length; i += 3) {
|
|
448
|
-
if (mesh.kind === 'lines') break;
|
|
449
|
-
indices[io + i] = mesh.indices[i] + vo;
|
|
450
|
-
indices[io + i + 1] = mesh.indices[flip ? i + 2 : i + 1] + vo;
|
|
451
|
-
indices[io + i + 2] = mesh.indices[flip ? i + 1 : i + 2] + vo;
|
|
452
|
-
}
|
|
453
|
-
if (mesh.kind === 'lines') for (let i = 0; i < mesh.indices.length; i++) indices[io + i] = mesh.indices[i] + vo;
|
|
454
|
-
vo += mesh.vertexCount;
|
|
455
|
-
io += mesh.indices.length;
|
|
456
|
-
}
|
|
457
|
-
return new Mesh({ positions, normals, uvs, indices, kind: items.length ? items[0].mesh.kind : 'triangles' });
|
|
458
|
-
}
|
|
1
|
+
// Mesh data structure and building helpers.
|
|
2
|
+
//
|
|
3
|
+
// A Mesh is an indexed triangle (or line) list with interleavable attributes:
|
|
4
|
+
// positions (xyz), normals (xyz), uvs (uv). Generators produce meshes in a
|
|
5
|
+
// canonical local space; the renderer packs them into shared GPU buffers.
|
|
6
|
+
|
|
7
|
+
import { computeNormalsWasm } from '../wasm/kernels.js';
|
|
8
|
+
|
|
9
|
+
export class Mesh {
|
|
10
|
+
/**
|
|
11
|
+
* @param {{positions: Float32Array, normals?: Float32Array, uvs?: Float32Array, indices: Uint32Array, kind?: 'triangles'|'lines'}} o
|
|
12
|
+
*/
|
|
13
|
+
constructor(o) {
|
|
14
|
+
this.positions = o.positions;
|
|
15
|
+
const n = this.positions.length / 3;
|
|
16
|
+
this.normals = o.normals || new Float32Array(n * 3);
|
|
17
|
+
this.uvs = o.uvs || new Float32Array(n * 2);
|
|
18
|
+
this.indices = o.indices;
|
|
19
|
+
this.kind = o.kind || 'triangles';
|
|
20
|
+
this._bounds = null;
|
|
21
|
+
this._edges = null;
|
|
22
|
+
this._normalLines = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get vertexCount() {
|
|
26
|
+
return this.positions.length / 3;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get primitiveCount() {
|
|
30
|
+
return this.kind === 'lines' ? this.indices.length / 2 : this.indices.length / 3;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get byteSize() {
|
|
34
|
+
return this.positions.byteLength + this.normals.byteLength + this.uvs.byteLength + this.indices.byteLength;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get bounds() {
|
|
38
|
+
if (!this._bounds) {
|
|
39
|
+
const p = this.positions;
|
|
40
|
+
const min = [Infinity, Infinity, Infinity];
|
|
41
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
42
|
+
for (let i = 0; i < p.length; i += 3) {
|
|
43
|
+
for (let k = 0; k < 3; k++) {
|
|
44
|
+
const v = p[i + k];
|
|
45
|
+
if (v < min[k]) min[k] = v;
|
|
46
|
+
if (v > max[k]) max[k] = v;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (p.length === 0) {
|
|
50
|
+
min.fill(0);
|
|
51
|
+
max.fill(0);
|
|
52
|
+
}
|
|
53
|
+
this._bounds = { min, max };
|
|
54
|
+
}
|
|
55
|
+
return this._bounds;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Unique edges as a line index list (for wireframe display). */
|
|
59
|
+
edges() {
|
|
60
|
+
if (this._edges) return this._edges;
|
|
61
|
+
if (this.kind === 'lines') return (this._edges = this.indices);
|
|
62
|
+
// weld positions so that seams do not show as edges twice
|
|
63
|
+
const weld = weldMap(this.positions);
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const out = [];
|
|
66
|
+
const idx = this.indices;
|
|
67
|
+
const nv = this.vertexCount;
|
|
68
|
+
for (let i = 0; i < idx.length; i += 3) {
|
|
69
|
+
for (let k = 0; k < 3; k++) {
|
|
70
|
+
const a = idx[i + k];
|
|
71
|
+
const b = idx[i + ((k + 1) % 3)];
|
|
72
|
+
const wa = weld[a], wb = weld[b];
|
|
73
|
+
if (wa === wb) continue;
|
|
74
|
+
const key = wa < wb ? wa * nv + wb : wb * nv + wa;
|
|
75
|
+
if (seen.has(key)) continue;
|
|
76
|
+
seen.add(key);
|
|
77
|
+
out.push(a, b);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this._edges = Uint32Array.from(out);
|
|
81
|
+
return this._edges;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Line vertices for normal display: [p, n, flag] where flag 0 = base, 1 = tip. */
|
|
85
|
+
normalLines() {
|
|
86
|
+
if (this._normalLines) return this._normalLines;
|
|
87
|
+
const n = this.vertexCount;
|
|
88
|
+
const positions = new Float32Array(n * 6);
|
|
89
|
+
const normals = new Float32Array(n * 6);
|
|
90
|
+
const uvs = new Float32Array(n * 4);
|
|
91
|
+
const indices = new Uint32Array(n * 2);
|
|
92
|
+
for (let i = 0; i < n; i++) {
|
|
93
|
+
for (let k = 0; k < 3; k++) {
|
|
94
|
+
positions[i * 6 + k] = this.positions[i * 3 + k];
|
|
95
|
+
positions[i * 6 + 3 + k] = this.positions[i * 3 + k];
|
|
96
|
+
normals[i * 6 + k] = this.normals[i * 3 + k];
|
|
97
|
+
normals[i * 6 + 3 + k] = this.normals[i * 3 + k];
|
|
98
|
+
}
|
|
99
|
+
uvs[i * 4 + 2] = 1; // tip flag in u
|
|
100
|
+
indices[i * 2] = i * 2;
|
|
101
|
+
indices[i * 2 + 1] = i * 2 + 1;
|
|
102
|
+
}
|
|
103
|
+
this._normalLines = new Mesh({ positions, normals, uvs, indices, kind: 'lines' });
|
|
104
|
+
return this._normalLines;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Map each vertex to the first vertex with the same position (quantized to 1e-6). */
|
|
109
|
+
export function weldMap(positions) {
|
|
110
|
+
const n = positions.length / 3;
|
|
111
|
+
const map = new Uint32Array(n);
|
|
112
|
+
// open addressing hash table over quantized coordinates (no string keys)
|
|
113
|
+
let size = 16;
|
|
114
|
+
while (size < n * 2) size *= 2;
|
|
115
|
+
const mask = size - 1;
|
|
116
|
+
const table = new Int32Array(size).fill(-1);
|
|
117
|
+
const q = new Float64Array(n * 3);
|
|
118
|
+
for (let i = 0; i < n * 3; i++) q[i] = Math.round(positions[i] * 1e6);
|
|
119
|
+
for (let i = 0; i < n; i++) {
|
|
120
|
+
const x = q[i * 3], y = q[i * 3 + 1], z = q[i * 3 + 2];
|
|
121
|
+
let h = (Math.imul(x | 0, 73856093) ^ Math.imul(y | 0, 19349663) ^ Math.imul(z | 0, 83492791)) & mask;
|
|
122
|
+
for (;;) {
|
|
123
|
+
const f = table[h];
|
|
124
|
+
if (f < 0) {
|
|
125
|
+
table[h] = i;
|
|
126
|
+
map[i] = i;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (q[f * 3] === x && q[f * 3 + 1] === y && q[f * 3 + 2] === z) {
|
|
130
|
+
map[i] = f;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
h = (h + 1) & mask;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return map;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Growable mesh builder. */
|
|
140
|
+
export class MeshBuilder {
|
|
141
|
+
/**
|
|
142
|
+
* @param {'triangles'|'lines'} kind
|
|
143
|
+
* @param {number} vertexCapacity vertices to allocate for; a generator that
|
|
144
|
+
* knows its exact counts passes them, so nothing grows or is copied
|
|
145
|
+
* @param {number} indexCapacity
|
|
146
|
+
*/
|
|
147
|
+
constructor(kind = 'triangles', vertexCapacity = 256, indexCapacity = 768) {
|
|
148
|
+
this.kind = kind;
|
|
149
|
+
vertexCapacity = Math.max(1, vertexCapacity);
|
|
150
|
+
indexCapacity = Math.max(1, indexCapacity);
|
|
151
|
+
this.p = new Float32Array(vertexCapacity * 3);
|
|
152
|
+
this.n = new Float32Array(vertexCapacity * 3);
|
|
153
|
+
this.t = new Float32Array(vertexCapacity * 2);
|
|
154
|
+
this.i = new Uint32Array(indexCapacity);
|
|
155
|
+
this.vc = 0;
|
|
156
|
+
this.ic = 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
growV(extra) {
|
|
160
|
+
const need = this.vc + extra;
|
|
161
|
+
if (need * 3 <= this.p.length) return;
|
|
162
|
+
let cap = this.p.length / 3;
|
|
163
|
+
while (cap < need) cap *= 2;
|
|
164
|
+
const p = new Float32Array(cap * 3);
|
|
165
|
+
p.set(this.p);
|
|
166
|
+
const n = new Float32Array(cap * 3);
|
|
167
|
+
n.set(this.n);
|
|
168
|
+
const t = new Float32Array(cap * 2);
|
|
169
|
+
t.set(this.t);
|
|
170
|
+
this.p = p;
|
|
171
|
+
this.n = n;
|
|
172
|
+
this.t = t;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
growI(extra) {
|
|
176
|
+
const need = this.ic + extra;
|
|
177
|
+
if (need <= this.i.length) return;
|
|
178
|
+
let cap = this.i.length;
|
|
179
|
+
while (cap < need) cap *= 2;
|
|
180
|
+
const i = new Uint32Array(cap);
|
|
181
|
+
i.set(this.i);
|
|
182
|
+
this.i = i;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
vertex(px, py, pz, nx = 0, ny = 0, nz = 0, u = 0, v = 0) {
|
|
186
|
+
if (this.vc * 3 >= this.p.length) this.growV(1);
|
|
187
|
+
const k = this.vc;
|
|
188
|
+
this.p[k * 3] = px;
|
|
189
|
+
this.p[k * 3 + 1] = py;
|
|
190
|
+
this.p[k * 3 + 2] = pz;
|
|
191
|
+
this.n[k * 3] = nx;
|
|
192
|
+
this.n[k * 3 + 1] = ny;
|
|
193
|
+
this.n[k * 3 + 2] = nz;
|
|
194
|
+
this.t[k * 2] = u;
|
|
195
|
+
this.t[k * 2 + 1] = v;
|
|
196
|
+
this.vc++;
|
|
197
|
+
return k;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
tri(a, b, c) {
|
|
201
|
+
if (this.ic + 3 > this.i.length) this.growI(3);
|
|
202
|
+
this.i[this.ic++] = a;
|
|
203
|
+
this.i[this.ic++] = b;
|
|
204
|
+
this.i[this.ic++] = c;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Quad a-b-c-d in counter-clockwise order (as seen from the front). */
|
|
208
|
+
quad(a, b, c, d) {
|
|
209
|
+
this.tri(a, b, c);
|
|
210
|
+
this.tri(a, c, d);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
line(a, b) {
|
|
214
|
+
if (this.ic + 2 > this.i.length) this.growI(2);
|
|
215
|
+
this.i[this.ic++] = a;
|
|
216
|
+
this.i[this.ic++] = b;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get vertexCount() {
|
|
220
|
+
return this.vc;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
build() {
|
|
224
|
+
// a builder sized exactly (generators that count first) hands over its arrays
|
|
225
|
+
const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
|
|
226
|
+
return new Mesh({
|
|
227
|
+
positions: exact(this.p, this.vc * 3),
|
|
228
|
+
normals: exact(this.n, this.vc * 3),
|
|
229
|
+
uvs: exact(this.t, this.vc * 2),
|
|
230
|
+
indices: exact(this.i, this.ic),
|
|
231
|
+
kind: this.kind,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Every generator writes through a builder, and a builder lives only while
|
|
237
|
+
// one mesh is made. When a full collection finds no builder alive, V8 drops
|
|
238
|
+
// the hidden class of the instances and discards all optimized code that
|
|
239
|
+
// relied on it, so the next scene would run the generators unoptimized again.
|
|
240
|
+
// This one never dies, and neither does the class.
|
|
241
|
+
MeshBuilder.shapeKeeper = new MeshBuilder('triangles', 1, 1);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Compute vertex normals for an indexed triangle soup with a crease angle:
|
|
245
|
+
* faces meeting at an angle larger than the crease get split normals.
|
|
246
|
+
* Returns a new mesh (vertices may be duplicated at creases).
|
|
247
|
+
*/
|
|
248
|
+
export function computeNormals(positions, indices, creaseDeg = 30, uvs = null) {
|
|
249
|
+
const crease = Math.min(Math.max(creaseDeg, 0), 180);
|
|
250
|
+
const cosCrease = Math.cos((crease * Math.PI) / 180);
|
|
251
|
+
if (crease === crease) {
|
|
252
|
+
const w = computeNormalsWasm(positions, indices, crease, cosCrease, uvs);
|
|
253
|
+
if (w) return new Mesh({ ...w, kind: 'triangles' });
|
|
254
|
+
}
|
|
255
|
+
const nt = indices.length / 3;
|
|
256
|
+
const faceN = new Float64Array(nt * 3); // area weighted (not normalized)
|
|
257
|
+
const faceU = new Float64Array(nt * 3); // unit length
|
|
258
|
+
for (let t = 0; t < nt; t++) {
|
|
259
|
+
const a = indices[t * 3] * 3, b = indices[t * 3 + 1] * 3, c = indices[t * 3 + 2] * 3;
|
|
260
|
+
const e1x = positions[b] - positions[a], e1y = positions[b + 1] - positions[a + 1], e1z = positions[b + 2] - positions[a + 2];
|
|
261
|
+
const e2x = positions[c] - positions[a], e2y = positions[c + 1] - positions[a + 1], e2z = positions[c + 2] - positions[a + 2];
|
|
262
|
+
const x = e1y * e2z - e1z * e2y, y = e1z * e2x - e1x * e2z, z = e1x * e2y - e1y * e2x;
|
|
263
|
+
faceN[t * 3] = x;
|
|
264
|
+
faceN[t * 3 + 1] = y;
|
|
265
|
+
faceN[t * 3 + 2] = z;
|
|
266
|
+
const l = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
267
|
+
faceU[t * 3] = x / l;
|
|
268
|
+
faceU[t * 3 + 1] = y / l;
|
|
269
|
+
faceU[t * 3 + 2] = z / l;
|
|
270
|
+
}
|
|
271
|
+
const weld = weldMap(positions);
|
|
272
|
+
const nv = positions.length / 3;
|
|
273
|
+
// faces incident to each welded vertex (CSR layout), needed only to test the crease angle
|
|
274
|
+
let counts = null, inc = null;
|
|
275
|
+
if (crease > 0 && crease < 180) {
|
|
276
|
+
counts = new Uint32Array(nv + 1);
|
|
277
|
+
for (let i = 0; i < indices.length; i++) counts[weld[indices[i]] + 1]++;
|
|
278
|
+
for (let i = 0; i < nv; i++) counts[i + 1] += counts[i];
|
|
279
|
+
inc = new Uint32Array(indices.length);
|
|
280
|
+
const fill = counts.slice(0, nv);
|
|
281
|
+
for (let i = 0; i < indices.length; i++) inc[fill[weld[indices[i]]]++] = (i / 3) | 0;
|
|
282
|
+
}
|
|
283
|
+
// without creases every corner of a welded vertex gets the same normal:
|
|
284
|
+
// sum the faces, then normalize once per welded vertex
|
|
285
|
+
let smoothN = null;
|
|
286
|
+
if (crease >= 180) {
|
|
287
|
+
smoothN = new Float64Array(nv * 3);
|
|
288
|
+
for (let i = 0; i < indices.length; i++) {
|
|
289
|
+
const w = weld[indices[i]] * 3, f = ((i / 3) | 0) * 3;
|
|
290
|
+
smoothN[w] += faceN[f];
|
|
291
|
+
smoothN[w + 1] += faceN[f + 1];
|
|
292
|
+
smoothN[w + 2] += faceN[f + 2];
|
|
293
|
+
}
|
|
294
|
+
for (let w = 0; w < nv * 3; w += 3) {
|
|
295
|
+
const x = smoothN[w], y = smoothN[w + 1], z = smoothN[w + 2];
|
|
296
|
+
const l = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
297
|
+
smoothN[w] = x / l;
|
|
298
|
+
smoothN[w + 1] = y / l;
|
|
299
|
+
smoothN[w + 2] = z / l;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// output vertices: as many as the input until creases split some
|
|
303
|
+
let cap = Math.max(1, nv);
|
|
304
|
+
let outP = new Float32Array(cap * 3), outN = new Float32Array(cap * 3), outT = new Float32Array(cap * 2);
|
|
305
|
+
const outI = new Uint32Array(indices.length);
|
|
306
|
+
let vc = 0;
|
|
307
|
+
let smoothOk = smoothN !== null;
|
|
308
|
+
for (let k = 0; smoothOk && k < nv * 3; k++) if (smoothN[k] !== smoothN[k]) smoothOk = false;
|
|
309
|
+
if (smoothOk) {
|
|
310
|
+
// one normal per welded vertex, so every input vertex becomes exactly one
|
|
311
|
+
// output vertex, in the order the triangles first use it
|
|
312
|
+
const out = new Int32Array(nv).fill(-1);
|
|
313
|
+
for (let i = 0; i < indices.length; i++) {
|
|
314
|
+
const vi = indices[i];
|
|
315
|
+
let idx = out[vi];
|
|
316
|
+
if (idx < 0) {
|
|
317
|
+
idx = out[vi] = vc++;
|
|
318
|
+
const w = weld[vi] * 3;
|
|
319
|
+
outP[idx * 3] = positions[vi * 3];
|
|
320
|
+
outP[idx * 3 + 1] = positions[vi * 3 + 1];
|
|
321
|
+
outP[idx * 3 + 2] = positions[vi * 3 + 2];
|
|
322
|
+
outN[idx * 3] = smoothN[w];
|
|
323
|
+
outN[idx * 3 + 1] = smoothN[w + 1];
|
|
324
|
+
outN[idx * 3 + 2] = smoothN[w + 2];
|
|
325
|
+
if (uvs) {
|
|
326
|
+
outT[idx * 2] = uvs[vi * 2];
|
|
327
|
+
outT[idx * 2 + 1] = uvs[vi * 2 + 1];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
outI[i] = idx;
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
// output vertices already emitted for each input vertex, as a linked list
|
|
334
|
+
// keyed by the normal quantized to 1e-4 (corners with equal normals share)
|
|
335
|
+
const head = new Int32Array(nv).fill(-1);
|
|
336
|
+
let next = new Int32Array(cap);
|
|
337
|
+
let qn = new Int32Array(cap * 3);
|
|
338
|
+
for (let i = 0; i < indices.length; i++) {
|
|
339
|
+
const t = (i / 3) | 0;
|
|
340
|
+
const vi = indices[i];
|
|
341
|
+
const w = weld[vi];
|
|
342
|
+
let nx = 0, ny = 0, nz = 0;
|
|
343
|
+
if (crease <= 0) {
|
|
344
|
+
nx = faceU[t * 3];
|
|
345
|
+
ny = faceU[t * 3 + 1];
|
|
346
|
+
nz = faceU[t * 3 + 2];
|
|
347
|
+
} else if (smoothN) {
|
|
348
|
+
nx = smoothN[w * 3];
|
|
349
|
+
ny = smoothN[w * 3 + 1];
|
|
350
|
+
nz = smoothN[w * 3 + 2];
|
|
351
|
+
} else {
|
|
352
|
+
// faces within the crease angle of this corner's face
|
|
353
|
+
const fx = faceU[t * 3], fy = faceU[t * 3 + 1], fz = faceU[t * 3 + 2];
|
|
354
|
+
for (let k = counts[w]; k < counts[w + 1]; k++) {
|
|
355
|
+
const f = inc[k];
|
|
356
|
+
if (f === t || faceU[f * 3] * fx + faceU[f * 3 + 1] * fy + faceU[f * 3 + 2] * fz >= cosCrease) {
|
|
357
|
+
nx += faceN[f * 3];
|
|
358
|
+
ny += faceN[f * 3 + 1];
|
|
359
|
+
nz += faceN[f * 3 + 2];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const l = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
363
|
+
nx /= l;
|
|
364
|
+
ny /= l;
|
|
365
|
+
nz /= l;
|
|
366
|
+
}
|
|
367
|
+
const qx = Math.round(nx * 1e4), qy = Math.round(ny * 1e4), qz = Math.round(nz * 1e4);
|
|
368
|
+
let idx = head[vi];
|
|
369
|
+
while (idx >= 0 && !(qn[idx * 3] === qx && qn[idx * 3 + 1] === qy && qn[idx * 3 + 2] === qz)) idx = next[idx];
|
|
370
|
+
if (idx < 0) {
|
|
371
|
+
idx = vc++;
|
|
372
|
+
if (idx >= cap) {
|
|
373
|
+
cap *= 2;
|
|
374
|
+
const grow = (a, size) => {
|
|
375
|
+
const g = new a.constructor(cap * size);
|
|
376
|
+
g.set(a);
|
|
377
|
+
return g;
|
|
378
|
+
};
|
|
379
|
+
outP = grow(outP, 3);
|
|
380
|
+
outN = grow(outN, 3);
|
|
381
|
+
outT = grow(outT, 2);
|
|
382
|
+
next = grow(next, 1);
|
|
383
|
+
qn = grow(qn, 3);
|
|
384
|
+
}
|
|
385
|
+
outP[idx * 3] = positions[vi * 3];
|
|
386
|
+
outP[idx * 3 + 1] = positions[vi * 3 + 1];
|
|
387
|
+
outP[idx * 3 + 2] = positions[vi * 3 + 2];
|
|
388
|
+
outN[idx * 3] = nx;
|
|
389
|
+
outN[idx * 3 + 1] = ny;
|
|
390
|
+
outN[idx * 3 + 2] = nz;
|
|
391
|
+
if (uvs) {
|
|
392
|
+
outT[idx * 2] = uvs[vi * 2];
|
|
393
|
+
outT[idx * 2 + 1] = uvs[vi * 2 + 1];
|
|
394
|
+
}
|
|
395
|
+
next[idx] = head[vi];
|
|
396
|
+
head[vi] = idx;
|
|
397
|
+
qn[idx * 3] = qx;
|
|
398
|
+
qn[idx * 3 + 1] = qy;
|
|
399
|
+
qn[idx * 3 + 2] = qz;
|
|
400
|
+
}
|
|
401
|
+
outI[i] = idx;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const exact = (a, n) => (a.length === n ? a : a.slice(0, n));
|
|
405
|
+
return new Mesh({ positions: exact(outP, vc * 3), normals: exact(outN, vc * 3), uvs: exact(outT, vc * 2), indices: outI, kind: 'triangles' });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Merge meshes transformed by matrices (used by exporters). */
|
|
409
|
+
export function mergeTransformed(items) {
|
|
410
|
+
let vc = 0;
|
|
411
|
+
let ic = 0;
|
|
412
|
+
for (const { mesh } of items) {
|
|
413
|
+
vc += mesh.vertexCount;
|
|
414
|
+
ic += mesh.indices.length;
|
|
415
|
+
}
|
|
416
|
+
const positions = new Float32Array(vc * 3);
|
|
417
|
+
const normals = new Float32Array(vc * 3);
|
|
418
|
+
const uvs = new Float32Array(vc * 2);
|
|
419
|
+
const indices = new Uint32Array(ic);
|
|
420
|
+
let vo = 0;
|
|
421
|
+
let io = 0;
|
|
422
|
+
for (const { mesh, matrix: m } of items) {
|
|
423
|
+
const p = mesh.positions, n = mesh.normals;
|
|
424
|
+
// normal matrix = cofactor of the upper 3x3 (with determinant sign)
|
|
425
|
+
const c0 = [m[5] * m[10] - m[6] * m[9], m[6] * m[8] - m[4] * m[10], m[4] * m[9] - m[5] * m[8]];
|
|
426
|
+
const c1 = [m[9] * m[2] - m[10] * m[1], m[10] * m[0] - m[8] * m[2], m[8] * m[1] - m[9] * m[0]];
|
|
427
|
+
const c2 = [m[1] * m[6] - m[2] * m[5], m[2] * m[4] - m[0] * m[6], m[0] * m[5] - m[1] * m[4]];
|
|
428
|
+
const det = m[0] * c0[0] + m[1] * c0[1] + m[2] * c0[2];
|
|
429
|
+
const sgn = det < 0 ? -1 : 1;
|
|
430
|
+
for (let i = 0; i < mesh.vertexCount; i++) {
|
|
431
|
+
const x = p[i * 3], y = p[i * 3 + 1], z = p[i * 3 + 2];
|
|
432
|
+
positions[(vo + i) * 3] = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
433
|
+
positions[(vo + i) * 3 + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
434
|
+
positions[(vo + i) * 3 + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
435
|
+
const nx = n[i * 3], ny = n[i * 3 + 1], nz = n[i * 3 + 2];
|
|
436
|
+
let ox = (c0[0] * nx + c1[0] * ny + c2[0] * nz) * sgn;
|
|
437
|
+
let oy = (c0[1] * nx + c1[1] * ny + c2[1] * nz) * sgn;
|
|
438
|
+
let oz = (c0[2] * nx + c1[2] * ny + c2[2] * nz) * sgn;
|
|
439
|
+
const l = Math.hypot(ox, oy, oz) || 1;
|
|
440
|
+
normals[(vo + i) * 3] = ox / l;
|
|
441
|
+
normals[(vo + i) * 3 + 1] = oy / l;
|
|
442
|
+
normals[(vo + i) * 3 + 2] = oz / l;
|
|
443
|
+
uvs[(vo + i) * 2] = mesh.uvs[i * 2];
|
|
444
|
+
uvs[(vo + i) * 2 + 1] = mesh.uvs[i * 2 + 1];
|
|
445
|
+
}
|
|
446
|
+
const flip = det < 0 && mesh.kind === 'triangles';
|
|
447
|
+
for (let i = 0; i < mesh.indices.length; i += 3) {
|
|
448
|
+
if (mesh.kind === 'lines') break;
|
|
449
|
+
indices[io + i] = mesh.indices[i] + vo;
|
|
450
|
+
indices[io + i + 1] = mesh.indices[flip ? i + 2 : i + 1] + vo;
|
|
451
|
+
indices[io + i + 2] = mesh.indices[flip ? i + 1 : i + 2] + vo;
|
|
452
|
+
}
|
|
453
|
+
if (mesh.kind === 'lines') for (let i = 0; i < mesh.indices.length; i++) indices[io + i] = mesh.indices[i] + vo;
|
|
454
|
+
vo += mesh.vertexCount;
|
|
455
|
+
io += mesh.indices.length;
|
|
456
|
+
}
|
|
457
|
+
return new Mesh({ positions, normals, uvs, indices, kind: items.length ? items[0].mesh.kind : 'triangles' });
|
|
458
|
+
}
|