@aethervtt/shard-mesh 0.1.0 → 0.2.0
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/builder.d.ts +146 -0
- package/dist/builder.js +1109 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +27 -1
- package/dist/simplify.d.ts +34 -0
- package/dist/simplify.js +140 -0
- package/dist/tree.d.ts +81 -0
- package/dist/tree.js +211 -0
- package/dist/trig.d.ts +15 -0
- package/dist/trig.js +36 -0
- package/package.json +4 -3
package/dist/builder.js
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { sinCos } from "./trig.js";
|
|
3
|
+
class MeshBuilder {
|
|
4
|
+
/** xyz per vertex. */
|
|
5
|
+
positions = new Float64Array(64 * 3);
|
|
6
|
+
/** xyz per vertex (zero until normals are computed or a shape writes them). */
|
|
7
|
+
normalData = new Float64Array(64 * 3);
|
|
8
|
+
/** uv per vertex. */
|
|
9
|
+
uvData = new Float64Array(64 * 2);
|
|
10
|
+
/** rgba per vertex, or undefined until something sets a color. */
|
|
11
|
+
colorData;
|
|
12
|
+
/** Second uv set per vertex, or undefined until something sets one. */
|
|
13
|
+
uv1Data;
|
|
14
|
+
/** xyzw per vertex, after `tangents()`. */
|
|
15
|
+
tangentData;
|
|
16
|
+
indices = new Uint32Array(64 * 3);
|
|
17
|
+
vertexCount = 0;
|
|
18
|
+
/** Index count (three per triangle). */
|
|
19
|
+
indexCount = 0;
|
|
20
|
+
/** Whether `normals` hold real normals for every vertex. */
|
|
21
|
+
hasNormals = true;
|
|
22
|
+
static create() {
|
|
23
|
+
return new MeshBuilder();
|
|
24
|
+
}
|
|
25
|
+
/** A builder holding a copy of `mesh`. */
|
|
26
|
+
static from(mesh) {
|
|
27
|
+
return new MeshBuilder().merge(mesh);
|
|
28
|
+
}
|
|
29
|
+
get triangleCount() {
|
|
30
|
+
return this.indexCount / 3;
|
|
31
|
+
}
|
|
32
|
+
// --- low level ---------------------------------------------------------------------------------
|
|
33
|
+
reserve(vertices, indices) {
|
|
34
|
+
const v = this.vertexCount + vertices;
|
|
35
|
+
if (v * 3 > this.positions.length) {
|
|
36
|
+
const cap = Math.max(v, this.positions.length / 3) * 2;
|
|
37
|
+
this.positions = grow(this.positions, cap * 3);
|
|
38
|
+
this.normalData = grow(this.normalData, cap * 3);
|
|
39
|
+
this.uvData = grow(this.uvData, cap * 2);
|
|
40
|
+
if (this.colorData) this.colorData = grow(this.colorData, cap * 4);
|
|
41
|
+
if (this.uv1Data) this.uv1Data = grow(this.uv1Data, cap * 2);
|
|
42
|
+
if (this.tangentData) this.tangentData = grow(this.tangentData, cap * 4);
|
|
43
|
+
}
|
|
44
|
+
const i = this.indexCount + indices;
|
|
45
|
+
if (i > this.indices.length) {
|
|
46
|
+
const next = new Uint32Array(Math.max(i, this.indices.length * 2));
|
|
47
|
+
next.set(this.indices.subarray(0, this.indexCount));
|
|
48
|
+
this.indices = next;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Appends a vertex and returns its index. A zero normal means "compute it in `finish`". */
|
|
52
|
+
vertex(x, y, z, nx = 0, ny = 0, nz = 0, u = 0, v = 0) {
|
|
53
|
+
this.reserve(1, 0);
|
|
54
|
+
const i = this.vertexCount++;
|
|
55
|
+
const p = this.positions;
|
|
56
|
+
p[i * 3] = x;
|
|
57
|
+
p[i * 3 + 1] = y;
|
|
58
|
+
p[i * 3 + 2] = z;
|
|
59
|
+
const n = this.normalData;
|
|
60
|
+
n[i * 3] = nx;
|
|
61
|
+
n[i * 3 + 1] = ny;
|
|
62
|
+
n[i * 3 + 2] = nz;
|
|
63
|
+
if (nx === 0 && ny === 0 && nz === 0) this.hasNormals = false;
|
|
64
|
+
this.uvData[i * 2] = u;
|
|
65
|
+
this.uvData[i * 2 + 1] = v;
|
|
66
|
+
if (this.colorData) this.colorData.set(WHITE, i * 4);
|
|
67
|
+
if (this.uv1Data) {
|
|
68
|
+
this.uv1Data[i * 2] = 0;
|
|
69
|
+
this.uv1Data[i * 2 + 1] = 0;
|
|
70
|
+
}
|
|
71
|
+
return i;
|
|
72
|
+
}
|
|
73
|
+
triangle(a, b, c) {
|
|
74
|
+
this.reserve(0, 3);
|
|
75
|
+
const t = this.indices;
|
|
76
|
+
t[this.indexCount++] = a;
|
|
77
|
+
t[this.indexCount++] = b;
|
|
78
|
+
t[this.indexCount++] = c;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
/** Two triangles, a-b-c and a-c-d (counter-clockwise front faces). */
|
|
82
|
+
quad(a, b, c, d) {
|
|
83
|
+
return this.triangle(a, b, c).triangle(a, c, d);
|
|
84
|
+
}
|
|
85
|
+
/** Sets one vertex's color (linear rgba). Other vertices default to white. */
|
|
86
|
+
color(vertex, r, g, b, a = 1) {
|
|
87
|
+
const c = this.ensureColors();
|
|
88
|
+
c[vertex * 4] = r;
|
|
89
|
+
c[vertex * 4 + 1] = g;
|
|
90
|
+
c[vertex * 4 + 2] = b;
|
|
91
|
+
c[vertex * 4 + 3] = a;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
ensureColors() {
|
|
95
|
+
if (!this.colorData) {
|
|
96
|
+
this.colorData = new Float64Array(this.positions.length / 3 * 4);
|
|
97
|
+
for (let i = 0; i < this.vertexCount; i++) this.colorData.set(WHITE, i * 4);
|
|
98
|
+
}
|
|
99
|
+
return this.colorData;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Sets one vertex's second uv set. Engine generators (0045) put a part code and a shade there
|
|
103
|
+
* (0 bark or base, 1 leaf or tip; a per-vertex variation), which vegetation materials tint by.
|
|
104
|
+
*/
|
|
105
|
+
uv1(vertex, u, v) {
|
|
106
|
+
this.uv1Data ??= new Float64Array(this.positions.length / 3 * 2);
|
|
107
|
+
this.uv1Data[vertex * 2] = u;
|
|
108
|
+
this.uv1Data[vertex * 2 + 1] = v;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
/** Sets the second uv set of every vertex from `first` on. */
|
|
112
|
+
uv1All(u, v, first = 0) {
|
|
113
|
+
for (let i = first; i < this.vertexCount; i++) this.uv1(i, u, v);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
/** Colors every vertex from `first` on (default: all of them). */
|
|
117
|
+
colorAll(r, g, b, a = 1, first = 0) {
|
|
118
|
+
for (let i = first; i < this.vertexCount; i++) this.color(i, r, g, b, a);
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
// --- shapes --------------------------------------------------------------------------------------
|
|
122
|
+
/** A sphere of `radius` subdivided `detail` times from an icosahedron (10 · 4^detail + 2 vertices). */
|
|
123
|
+
icosphere(detail, radius = 1) {
|
|
124
|
+
if (!Number.isInteger(detail) || detail < 0 || detail > 8) {
|
|
125
|
+
throw new ShardError("mesh/bad-shape", `icosphere detail must be 0 to 8, got ${detail}`, {
|
|
126
|
+
hint: "Detail 6 is already 40 962 vertices."
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const first = this.vertexCount;
|
|
130
|
+
const phi = (1 + Math.sqrt(5)) / 2;
|
|
131
|
+
const base = [
|
|
132
|
+
-1,
|
|
133
|
+
phi,
|
|
134
|
+
0,
|
|
135
|
+
1,
|
|
136
|
+
phi,
|
|
137
|
+
0,
|
|
138
|
+
-1,
|
|
139
|
+
-phi,
|
|
140
|
+
0,
|
|
141
|
+
1,
|
|
142
|
+
-phi,
|
|
143
|
+
0,
|
|
144
|
+
0,
|
|
145
|
+
-1,
|
|
146
|
+
phi,
|
|
147
|
+
0,
|
|
148
|
+
1,
|
|
149
|
+
phi,
|
|
150
|
+
0,
|
|
151
|
+
-1,
|
|
152
|
+
-phi,
|
|
153
|
+
0,
|
|
154
|
+
1,
|
|
155
|
+
-phi,
|
|
156
|
+
phi,
|
|
157
|
+
0,
|
|
158
|
+
-1,
|
|
159
|
+
phi,
|
|
160
|
+
0,
|
|
161
|
+
1,
|
|
162
|
+
-phi,
|
|
163
|
+
0,
|
|
164
|
+
-1,
|
|
165
|
+
-phi,
|
|
166
|
+
0,
|
|
167
|
+
1
|
|
168
|
+
];
|
|
169
|
+
let faces = [
|
|
170
|
+
0,
|
|
171
|
+
11,
|
|
172
|
+
5,
|
|
173
|
+
0,
|
|
174
|
+
5,
|
|
175
|
+
1,
|
|
176
|
+
0,
|
|
177
|
+
1,
|
|
178
|
+
7,
|
|
179
|
+
0,
|
|
180
|
+
7,
|
|
181
|
+
10,
|
|
182
|
+
0,
|
|
183
|
+
10,
|
|
184
|
+
11,
|
|
185
|
+
1,
|
|
186
|
+
5,
|
|
187
|
+
9,
|
|
188
|
+
5,
|
|
189
|
+
11,
|
|
190
|
+
4,
|
|
191
|
+
11,
|
|
192
|
+
10,
|
|
193
|
+
2,
|
|
194
|
+
10,
|
|
195
|
+
7,
|
|
196
|
+
6,
|
|
197
|
+
7,
|
|
198
|
+
1,
|
|
199
|
+
8,
|
|
200
|
+
3,
|
|
201
|
+
9,
|
|
202
|
+
4,
|
|
203
|
+
3,
|
|
204
|
+
4,
|
|
205
|
+
2,
|
|
206
|
+
3,
|
|
207
|
+
2,
|
|
208
|
+
6,
|
|
209
|
+
3,
|
|
210
|
+
6,
|
|
211
|
+
8,
|
|
212
|
+
3,
|
|
213
|
+
8,
|
|
214
|
+
9,
|
|
215
|
+
4,
|
|
216
|
+
9,
|
|
217
|
+
5,
|
|
218
|
+
2,
|
|
219
|
+
4,
|
|
220
|
+
11,
|
|
221
|
+
6,
|
|
222
|
+
2,
|
|
223
|
+
10,
|
|
224
|
+
8,
|
|
225
|
+
6,
|
|
226
|
+
7,
|
|
227
|
+
9,
|
|
228
|
+
8,
|
|
229
|
+
1
|
|
230
|
+
];
|
|
231
|
+
const total = 10 * 4 ** detail + 2;
|
|
232
|
+
const unit = new Float64Array(total * 3);
|
|
233
|
+
let count = 0;
|
|
234
|
+
const add = (x, y, z) => {
|
|
235
|
+
const inv = 1 / Math.sqrt(x * x + y * y + z * z);
|
|
236
|
+
unit[count * 3] = x * inv;
|
|
237
|
+
unit[count * 3 + 1] = y * inv;
|
|
238
|
+
unit[count * 3 + 2] = z * inv;
|
|
239
|
+
return count++;
|
|
240
|
+
};
|
|
241
|
+
for (let i = 0; i < 12; i++) add(base[i * 3], base[i * 3 + 1], base[i * 3 + 2]);
|
|
242
|
+
for (let level = 0; level < detail; level++) {
|
|
243
|
+
const midpoints = /* @__PURE__ */ new Map();
|
|
244
|
+
const mid = (a, b) => {
|
|
245
|
+
const key = a < b ? a * total + b : b * total + a;
|
|
246
|
+
let m = midpoints.get(key);
|
|
247
|
+
if (m === void 0) {
|
|
248
|
+
m = add(
|
|
249
|
+
unit[a * 3] + unit[b * 3],
|
|
250
|
+
unit[a * 3 + 1] + unit[b * 3 + 1],
|
|
251
|
+
unit[a * 3 + 2] + unit[b * 3 + 2]
|
|
252
|
+
);
|
|
253
|
+
midpoints.set(key, m);
|
|
254
|
+
}
|
|
255
|
+
return m;
|
|
256
|
+
};
|
|
257
|
+
const next = [];
|
|
258
|
+
for (let f = 0; f < faces.length; f += 3) {
|
|
259
|
+
const a = faces[f];
|
|
260
|
+
const b = faces[f + 1];
|
|
261
|
+
const c = faces[f + 2];
|
|
262
|
+
const ab = mid(a, b);
|
|
263
|
+
const bc = mid(b, c);
|
|
264
|
+
const ca = mid(c, a);
|
|
265
|
+
next.push(a, ab, ca, b, bc, ab, c, ca, bc, ab, bc, ca);
|
|
266
|
+
}
|
|
267
|
+
faces = next;
|
|
268
|
+
}
|
|
269
|
+
this.reserve(count, faces.length);
|
|
270
|
+
for (let i = 0; i < count; i++) {
|
|
271
|
+
const x = unit[i * 3];
|
|
272
|
+
const y = unit[i * 3 + 1];
|
|
273
|
+
const z = unit[i * 3 + 2];
|
|
274
|
+
this.vertex(x * radius, y * radius, z * radius, x, y, z, 0.5 + x * 0.5, 0.5 - y * 0.5);
|
|
275
|
+
}
|
|
276
|
+
for (let f = 0; f < faces.length; f += 3)
|
|
277
|
+
this.triangle(first + faces[f], first + faces[f + 1], first + faces[f + 2]);
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
/** An axis-aligned box centered on the origin, with flat faces. */
|
|
281
|
+
box(options = {}) {
|
|
282
|
+
const h = [(options.x ?? 1) / 2, (options.y ?? 1) / 2, (options.z ?? 1) / 2];
|
|
283
|
+
const faces = [
|
|
284
|
+
[0, 1, 2, -1, 1, 1],
|
|
285
|
+
[0, -1, 2, 1, 1, 1],
|
|
286
|
+
[1, 1, 0, 1, 2, -1],
|
|
287
|
+
[1, -1, 0, 1, 2, 1],
|
|
288
|
+
[2, 1, 0, 1, 1, 1],
|
|
289
|
+
[2, -1, 0, -1, 1, 1]
|
|
290
|
+
];
|
|
291
|
+
const p = [0, 0, 0];
|
|
292
|
+
const n = [0, 0, 0];
|
|
293
|
+
for (const [axis, sign, ua, us, va, vs] of faces) {
|
|
294
|
+
const base = this.vertexCount;
|
|
295
|
+
n[0] = n[1] = n[2] = 0;
|
|
296
|
+
n[axis] = sign;
|
|
297
|
+
for (let k = 0; k < 4; k++) {
|
|
298
|
+
const su = k === 1 || k === 2 ? 1 : -1;
|
|
299
|
+
const sv = k >= 2 ? 1 : -1;
|
|
300
|
+
p[axis] = sign * h[axis];
|
|
301
|
+
p[ua] = su * us * h[ua];
|
|
302
|
+
p[va] = sv * vs * h[va];
|
|
303
|
+
this.vertex(p[0], p[1], p[2], n[0], n[1], n[2], (su + 1) / 2, (1 - sv) / 2);
|
|
304
|
+
}
|
|
305
|
+
this.quad(base, base + 1, base + 2, base + 3);
|
|
306
|
+
}
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* A surface of revolution around +Y: `profile` is [radius, y] pairs from bottom to top, swept
|
|
311
|
+
* through `sides` steps. A radius of 0 closes that end to a point.
|
|
312
|
+
*/
|
|
313
|
+
lathe(profile, options = {}) {
|
|
314
|
+
const sides = options.sides ?? 12;
|
|
315
|
+
const rows = profile.length / 2;
|
|
316
|
+
if (rows < 2 || sides < 3) {
|
|
317
|
+
throw new ShardError("mesh/bad-shape", "lathe needs at least 2 profile points and 3 sides", {
|
|
318
|
+
hint: "profile: [r0, y0, r1, y1, \u2026], bottom to top."
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const first = this.vertexCount;
|
|
322
|
+
const ring = sides + 1;
|
|
323
|
+
for (let j = 0; j < rows; j++) {
|
|
324
|
+
const r = profile[j * 2];
|
|
325
|
+
const y = profile[j * 2 + 1];
|
|
326
|
+
const a = Math.max(0, j - 1);
|
|
327
|
+
const b = Math.min(rows - 1, j + 1);
|
|
328
|
+
const dr = profile[b * 2] - profile[a * 2];
|
|
329
|
+
const dy = profile[b * 2 + 1] - profile[a * 2 + 1];
|
|
330
|
+
const len = Math.sqrt(dr * dr + dy * dy) || 1;
|
|
331
|
+
const nr = dy / len;
|
|
332
|
+
const ny = -dr / len;
|
|
333
|
+
for (let i = 0; i <= sides; i++) {
|
|
334
|
+
sinCos(i / sides * TAU, sc);
|
|
335
|
+
const s = sc[0];
|
|
336
|
+
const c = sc[1];
|
|
337
|
+
this.vertex(s * r, y, c * r, s * nr, ny, c * nr, i / sides, 1 - j / (rows - 1));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (let j = 0; j < rows - 1; j++) {
|
|
341
|
+
for (let i = 0; i < sides; i++) {
|
|
342
|
+
const a = first + j * ring + i;
|
|
343
|
+
const b = a + ring;
|
|
344
|
+
this.quad(a, a + 1, b + 1, b);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (options.caps ?? true) {
|
|
348
|
+
this.cap(profile[0], profile[1], sides, -1);
|
|
349
|
+
this.cap(profile[(rows - 1) * 2], profile[(rows - 1) * 2 + 1], sides, 1);
|
|
350
|
+
}
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
cap(r, y, sides, dir) {
|
|
354
|
+
if (!(r > 0)) return;
|
|
355
|
+
const center = this.vertex(0, y, 0, 0, dir, 0, 0.5, 0.5);
|
|
356
|
+
for (let i = 0; i <= sides; i++) {
|
|
357
|
+
sinCos(i / sides * TAU, sc);
|
|
358
|
+
this.vertex(sc[0] * r, y, sc[1] * r, 0, dir, 0, 0.5 + sc[0] / 2, 0.5 - sc[1] / 2);
|
|
359
|
+
}
|
|
360
|
+
for (let i = 0; i < sides; i++) {
|
|
361
|
+
const a = center + 1 + i;
|
|
362
|
+
if (dir > 0) this.triangle(center, a, a + 1);
|
|
363
|
+
else this.triangle(center, a + 1, a);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** A cylinder (or a cone, with `radiusTop` 0) on +Y, centered on the origin. */
|
|
367
|
+
cylinder(options = {}) {
|
|
368
|
+
const r = options.radius ?? 0.5;
|
|
369
|
+
const h = (options.height ?? 1) / 2;
|
|
370
|
+
return this.lathe([r, -h, options.radiusTop ?? r, h], {
|
|
371
|
+
sides: options.sides ?? 16,
|
|
372
|
+
caps: options.caps
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A tube along a curve (xyz points): rings of `sides` vertices, oriented by parallel transport
|
|
377
|
+
* so they don't twist. `radius` is one number or one per point (0 at an end makes a point).
|
|
378
|
+
*/
|
|
379
|
+
tube(curve, radius, options = {}) {
|
|
380
|
+
const count = curve.length / 3;
|
|
381
|
+
if (count < 2) {
|
|
382
|
+
throw new ShardError("mesh/bad-shape", "tube needs a curve of at least 2 points", {
|
|
383
|
+
hint: "curve: [x0, y0, z0, x1, y1, z1, \u2026]."
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const sides = options.sides ?? 8;
|
|
387
|
+
const ring = sides + 1;
|
|
388
|
+
const first = this.vertexCount;
|
|
389
|
+
const radiusAt = (i) => typeof radius === "number" ? radius : radius[i];
|
|
390
|
+
const t = new Float64Array(count * 3);
|
|
391
|
+
for (let i = 0; i < count; i++) {
|
|
392
|
+
const a = Math.max(0, i - 1);
|
|
393
|
+
const b = Math.min(count - 1, i + 1);
|
|
394
|
+
let x = curve[b * 3] - curve[a * 3];
|
|
395
|
+
let y = curve[b * 3 + 1] - curve[a * 3 + 1];
|
|
396
|
+
let z = curve[b * 3 + 2] - curve[a * 3 + 2];
|
|
397
|
+
const len = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
398
|
+
x /= len;
|
|
399
|
+
y /= len;
|
|
400
|
+
z /= len;
|
|
401
|
+
t[i * 3] = x;
|
|
402
|
+
t[i * 3 + 1] = y;
|
|
403
|
+
t[i * 3 + 2] = z;
|
|
404
|
+
}
|
|
405
|
+
const nrm = new Float64Array(3);
|
|
406
|
+
perpendicular(t[0], t[1], t[2], nrm);
|
|
407
|
+
let length = options.v0 ?? 0;
|
|
408
|
+
const vScale = options.vScale ?? 1;
|
|
409
|
+
for (let i = 0; i < count; i++) {
|
|
410
|
+
const tx = t[i * 3];
|
|
411
|
+
const ty = t[i * 3 + 1];
|
|
412
|
+
const tz = t[i * 3 + 2];
|
|
413
|
+
if (i > 0) {
|
|
414
|
+
const d = nrm[0] * tx + nrm[1] * ty + nrm[2] * tz;
|
|
415
|
+
let nx = nrm[0] - tx * d;
|
|
416
|
+
let ny = nrm[1] - ty * d;
|
|
417
|
+
let nz = nrm[2] - tz * d;
|
|
418
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
419
|
+
if (len < 1e-9) perpendicular(tx, ty, tz, nrm);
|
|
420
|
+
else {
|
|
421
|
+
nx /= len;
|
|
422
|
+
ny /= len;
|
|
423
|
+
nz /= len;
|
|
424
|
+
nrm[0] = nx;
|
|
425
|
+
nrm[1] = ny;
|
|
426
|
+
nrm[2] = nz;
|
|
427
|
+
}
|
|
428
|
+
const dx = curve[i * 3] - curve[i * 3 - 3];
|
|
429
|
+
const dy = curve[i * 3 + 1] - curve[i * 3 - 2];
|
|
430
|
+
const dz = curve[i * 3 + 2] - curve[i * 3 - 1];
|
|
431
|
+
length += Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
432
|
+
}
|
|
433
|
+
const bx = ty * nrm[2] - tz * nrm[1];
|
|
434
|
+
const by = tz * nrm[0] - tx * nrm[2];
|
|
435
|
+
const bz = tx * nrm[1] - ty * nrm[0];
|
|
436
|
+
const r = radiusAt(i);
|
|
437
|
+
const cx = curve[i * 3];
|
|
438
|
+
const cy = curve[i * 3 + 1];
|
|
439
|
+
const cz = curve[i * 3 + 2];
|
|
440
|
+
for (let s = 0; s <= sides; s++) {
|
|
441
|
+
sinCos(s / sides * TAU, sc);
|
|
442
|
+
const ox = nrm[0] * sc[1] + bx * sc[0];
|
|
443
|
+
const oy = nrm[1] * sc[1] + by * sc[0];
|
|
444
|
+
const oz = nrm[2] * sc[1] + bz * sc[0];
|
|
445
|
+
this.vertex(cx + ox * r, cy + oy * r, cz + oz * r, ox, oy, oz, s / sides, length * vScale);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (let i = 0; i < count - 1; i++) {
|
|
449
|
+
for (let s = 0; s < sides; s++) {
|
|
450
|
+
const a = first + i * ring + s;
|
|
451
|
+
const b = a + ring;
|
|
452
|
+
this.quad(a, a + 1, b + 1, b);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (options.caps) {
|
|
456
|
+
for (const end of [0, count - 1]) {
|
|
457
|
+
const r = radiusAt(end);
|
|
458
|
+
if (!(r > 0)) continue;
|
|
459
|
+
const dir = end === 0 ? -1 : 1;
|
|
460
|
+
const nx = t[end * 3] * dir;
|
|
461
|
+
const ny = t[end * 3 + 1] * dir;
|
|
462
|
+
const nz = t[end * 3 + 2] * dir;
|
|
463
|
+
const center = this.vertex(
|
|
464
|
+
curve[end * 3],
|
|
465
|
+
curve[end * 3 + 1],
|
|
466
|
+
curve[end * 3 + 2],
|
|
467
|
+
nx,
|
|
468
|
+
ny,
|
|
469
|
+
nz,
|
|
470
|
+
0.5,
|
|
471
|
+
0.5
|
|
472
|
+
);
|
|
473
|
+
const row = first + end * ring;
|
|
474
|
+
for (let s = 0; s < sides; s++) {
|
|
475
|
+
if (dir > 0) this.triangle(center, row + s, row + s + 1);
|
|
476
|
+
else this.triangle(center, row + s + 1, row + s);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return this;
|
|
481
|
+
}
|
|
482
|
+
// --- deformation ---------------------------------------------------------------------------------
|
|
483
|
+
/**
|
|
484
|
+
* Moves every vertex along its normal by `fn(position, normal, index)` metres (normals are
|
|
485
|
+
* computed first where missing). Synchronous: for generator code, which runs off the main thread.
|
|
486
|
+
*/
|
|
487
|
+
displace(fn, first = 0) {
|
|
488
|
+
this.ensureNormals();
|
|
489
|
+
const p = this.positions;
|
|
490
|
+
const n = this.normalData;
|
|
491
|
+
const pos = new Float64Array(3);
|
|
492
|
+
const nrm = new Float64Array(3);
|
|
493
|
+
for (let i = first; i < this.vertexCount; i++) {
|
|
494
|
+
pos[0] = p[i * 3];
|
|
495
|
+
pos[1] = p[i * 3 + 1];
|
|
496
|
+
pos[2] = p[i * 3 + 2];
|
|
497
|
+
nrm[0] = n[i * 3];
|
|
498
|
+
nrm[1] = n[i * 3 + 1];
|
|
499
|
+
nrm[2] = n[i * 3 + 2];
|
|
500
|
+
const d = fn(pos, nrm, i);
|
|
501
|
+
p[i * 3] = pos[0] + nrm[0] * d;
|
|
502
|
+
p[i * 3 + 1] = pos[1] + nrm[1] * d;
|
|
503
|
+
p[i * 3 + 2] = pos[2] + nrm[2] * d;
|
|
504
|
+
}
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Moves every vertex along its normal by `amount × noise(position)`, sampling all positions in one
|
|
509
|
+
* batch: `sample(points, out)` is a batch noise call, such as a generator's
|
|
510
|
+
* `(p, out) => ctx.noise.sample(graph, seed, p, out)`.
|
|
511
|
+
*/
|
|
512
|
+
displaceNoise(sample, amount, first = 0) {
|
|
513
|
+
this.ensureNormals();
|
|
514
|
+
const count = this.vertexCount - first;
|
|
515
|
+
const pts = new Float32Array(count * 3);
|
|
516
|
+
for (let i = 0; i < count * 3; i++) pts[i] = this.positions[first * 3 + i];
|
|
517
|
+
const out = new Float32Array(count);
|
|
518
|
+
sample(pts, out);
|
|
519
|
+
const p = this.positions;
|
|
520
|
+
const n = this.normalData;
|
|
521
|
+
for (let k = 0; k < count; k++) {
|
|
522
|
+
const i = first + k;
|
|
523
|
+
const d = out[k] * amount;
|
|
524
|
+
p[i * 3] = p[i * 3] + n[i * 3] * d;
|
|
525
|
+
p[i * 3 + 1] = p[i * 3 + 1] + n[i * 3 + 1] * d;
|
|
526
|
+
p[i * 3 + 2] = p[i * 3 + 2] + n[i * 3 + 2] * d;
|
|
527
|
+
}
|
|
528
|
+
return this;
|
|
529
|
+
}
|
|
530
|
+
/** Scales positions per axis about the origin (normals follow the inverse transpose). */
|
|
531
|
+
scale(x, y = x, z = x, first = 0) {
|
|
532
|
+
const p = this.positions;
|
|
533
|
+
const n = this.normalData;
|
|
534
|
+
for (let i = first; i < this.vertexCount; i++) {
|
|
535
|
+
p[i * 3] = p[i * 3] * x;
|
|
536
|
+
p[i * 3 + 1] = p[i * 3 + 1] * y;
|
|
537
|
+
p[i * 3 + 2] = p[i * 3 + 2] * z;
|
|
538
|
+
normalizeAt(n, i, n[i * 3] / x, n[i * 3 + 1] / y, n[i * 3 + 2] / z);
|
|
539
|
+
}
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
/** Moves vertices from `first` on. */
|
|
543
|
+
translate(x, y, z, first = 0) {
|
|
544
|
+
const p = this.positions;
|
|
545
|
+
for (let i = first; i < this.vertexCount; i++) {
|
|
546
|
+
p[i * 3] = p[i * 3] + x;
|
|
547
|
+
p[i * 3 + 1] = p[i * 3 + 1] + y;
|
|
548
|
+
p[i * 3 + 2] = p[i * 3 + 2] + z;
|
|
549
|
+
}
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Transforms vertices from `first` on by a column-major 4×4 affine matrix (normals by its
|
|
554
|
+
* inverse transpose, renormalized).
|
|
555
|
+
*/
|
|
556
|
+
transform(m, first = 0) {
|
|
557
|
+
const inv = normalMatrix(m);
|
|
558
|
+
const p = this.positions;
|
|
559
|
+
const n = this.normalData;
|
|
560
|
+
for (let i = first; i < this.vertexCount; i++) {
|
|
561
|
+
const x = p[i * 3];
|
|
562
|
+
const y = p[i * 3 + 1];
|
|
563
|
+
const z = p[i * 3 + 2];
|
|
564
|
+
p[i * 3] = m[0] * x + m[4] * y + m[8] * z + m[12];
|
|
565
|
+
p[i * 3 + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
|
|
566
|
+
p[i * 3 + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
|
|
567
|
+
const nx = n[i * 3];
|
|
568
|
+
const ny = n[i * 3 + 1];
|
|
569
|
+
const nz = n[i * 3 + 2];
|
|
570
|
+
if (nx === 0 && ny === 0 && nz === 0) continue;
|
|
571
|
+
normalizeAt(
|
|
572
|
+
n,
|
|
573
|
+
i,
|
|
574
|
+
inv[0] * nx + inv[3] * ny + inv[6] * nz,
|
|
575
|
+
inv[1] * nx + inv[4] * ny + inv[7] * nz,
|
|
576
|
+
inv[2] * nx + inv[5] * ny + inv[8] * nz
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
return this;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Extrudes a set of triangles (indices of triangles, i.e. index / 3) by `distance` along their
|
|
583
|
+
* average normal per vertex: the set moves out, and its boundary gets side walls.
|
|
584
|
+
*/
|
|
585
|
+
extrude(faceSet, distance) {
|
|
586
|
+
const t = this.indices;
|
|
587
|
+
const p = this.positions;
|
|
588
|
+
const acc = /* @__PURE__ */ new Map();
|
|
589
|
+
const inSet = /* @__PURE__ */ new Set();
|
|
590
|
+
for (let k = 0; k < faceSet.length; k++) {
|
|
591
|
+
const f = faceSet[k];
|
|
592
|
+
if (!(f >= 0 && f < this.triangleCount)) {
|
|
593
|
+
throw new ShardError("mesh/bad-shape", `extrude: triangle ${f} doesn't exist`, {
|
|
594
|
+
hint: `Triangles are 0 to ${this.triangleCount - 1} (index / 3).`
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
inSet.add(f);
|
|
598
|
+
const a = t[f * 3];
|
|
599
|
+
const b = t[f * 3 + 1];
|
|
600
|
+
const c = t[f * 3 + 2];
|
|
601
|
+
const ux = p[b * 3] - p[a * 3];
|
|
602
|
+
const uy = p[b * 3 + 1] - p[a * 3 + 1];
|
|
603
|
+
const uz = p[b * 3 + 2] - p[a * 3 + 2];
|
|
604
|
+
const vx = p[c * 3] - p[a * 3];
|
|
605
|
+
const vy = p[c * 3 + 1] - p[a * 3 + 1];
|
|
606
|
+
const vz = p[c * 3 + 2] - p[a * 3 + 2];
|
|
607
|
+
const nx = uy * vz - uz * vy;
|
|
608
|
+
const ny = uz * vx - ux * vz;
|
|
609
|
+
const nz = ux * vy - uy * vx;
|
|
610
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
611
|
+
for (const v of [a, b, c]) {
|
|
612
|
+
let s = acc.get(v);
|
|
613
|
+
if (!s) {
|
|
614
|
+
s = [0, 0, 0];
|
|
615
|
+
acc.set(v, s);
|
|
616
|
+
}
|
|
617
|
+
s[0] += nx / len;
|
|
618
|
+
s[1] += ny / len;
|
|
619
|
+
s[2] += nz / len;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const edges = /* @__PURE__ */ new Map();
|
|
623
|
+
for (const f of inSet) {
|
|
624
|
+
for (let e = 0; e < 3; e++) {
|
|
625
|
+
const a = t[f * 3 + e];
|
|
626
|
+
const b = t[f * 3 + (e + 1) % 3];
|
|
627
|
+
const reverse = `${b}:${a}`;
|
|
628
|
+
if (edges.has(reverse)) edges.delete(reverse);
|
|
629
|
+
else edges.set(`${a}:${b}`, [a, b]);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const moved = /* @__PURE__ */ new Map();
|
|
633
|
+
const vertices = [...acc.keys()].sort((a, b) => a - b);
|
|
634
|
+
for (const v of vertices) {
|
|
635
|
+
const s = acc.get(v);
|
|
636
|
+
const len = Math.sqrt(s[0] * s[0] + s[1] * s[1] + s[2] * s[2]) || 1;
|
|
637
|
+
const nv = this.copyVertex(v);
|
|
638
|
+
const q = this.positions;
|
|
639
|
+
q[nv * 3] = q[nv * 3] + s[0] / len * distance;
|
|
640
|
+
q[nv * 3 + 1] = q[nv * 3 + 1] + s[1] / len * distance;
|
|
641
|
+
q[nv * 3 + 2] = q[nv * 3 + 2] + s[2] / len * distance;
|
|
642
|
+
moved.set(v, nv);
|
|
643
|
+
}
|
|
644
|
+
const idx = this.indices;
|
|
645
|
+
for (const f of inSet) {
|
|
646
|
+
for (let e = 0; e < 3; e++) idx[f * 3 + e] = moved.get(idx[f * 3 + e]);
|
|
647
|
+
}
|
|
648
|
+
const walls = [...edges.values()].sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
649
|
+
for (const [a, b] of walls) {
|
|
650
|
+
const a2 = moved.get(a);
|
|
651
|
+
const b2 = moved.get(b);
|
|
652
|
+
const w = [this.copyVertex(a), this.copyVertex(b), this.copyVertex(b2), this.copyVertex(a2)];
|
|
653
|
+
for (const v of w) this.normalData.fill(0, v * 3, v * 3 + 3);
|
|
654
|
+
this.hasNormals = false;
|
|
655
|
+
this.quad(w[0], w[1], w[2], w[3]);
|
|
656
|
+
}
|
|
657
|
+
return this;
|
|
658
|
+
}
|
|
659
|
+
copyVertex(v) {
|
|
660
|
+
const p = this.positions;
|
|
661
|
+
const n = this.normalData;
|
|
662
|
+
const i = this.vertex(
|
|
663
|
+
p[v * 3],
|
|
664
|
+
p[v * 3 + 1],
|
|
665
|
+
p[v * 3 + 2],
|
|
666
|
+
n[v * 3],
|
|
667
|
+
n[v * 3 + 1],
|
|
668
|
+
n[v * 3 + 2],
|
|
669
|
+
this.uvData[v * 2],
|
|
670
|
+
this.uvData[v * 2 + 1]
|
|
671
|
+
);
|
|
672
|
+
if (this.colorData) this.colorData.copyWithin(i * 4, v * 4, v * 4 + 4);
|
|
673
|
+
if (this.uv1Data) this.uv1Data.copyWithin(i * 2, v * 2, v * 2 + 2);
|
|
674
|
+
return i;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Appends another mesh (a builder or MeshData), optionally transformed by a column-major 4×4
|
|
678
|
+
* affine matrix.
|
|
679
|
+
*/
|
|
680
|
+
merge(other, transform) {
|
|
681
|
+
const first = this.vertexCount;
|
|
682
|
+
const src = other instanceof MeshBuilder ? other : void 0;
|
|
683
|
+
const data = other instanceof MeshBuilder ? void 0 : other;
|
|
684
|
+
const count = src ? src.vertexCount : data.positions.length / 3;
|
|
685
|
+
const indexCount = src ? src.indexCount : data.indices?.length ?? count;
|
|
686
|
+
this.reserve(count, indexCount);
|
|
687
|
+
const pos = src ? src.positions : data.positions;
|
|
688
|
+
const nrm = src ? src.normalData : data.normals;
|
|
689
|
+
const uv = src ? src.uvData : data.uvs;
|
|
690
|
+
const col = src ? src.colorData : data.colors;
|
|
691
|
+
const uv1 = src ? src.uv1Data : data.uvs1;
|
|
692
|
+
if (col) this.ensureColors();
|
|
693
|
+
for (let i = 0; i < count; i++) {
|
|
694
|
+
this.vertex(
|
|
695
|
+
pos[i * 3],
|
|
696
|
+
pos[i * 3 + 1],
|
|
697
|
+
pos[i * 3 + 2],
|
|
698
|
+
nrm ? nrm[i * 3] : 0,
|
|
699
|
+
nrm ? nrm[i * 3 + 1] : 0,
|
|
700
|
+
nrm ? nrm[i * 3 + 2] : 0,
|
|
701
|
+
uv ? uv[i * 2] : 0,
|
|
702
|
+
uv ? uv[i * 2 + 1] : 0
|
|
703
|
+
);
|
|
704
|
+
if (col) this.color(first + i, col[i * 4], col[i * 4 + 1], col[i * 4 + 2], col[i * 4 + 3]);
|
|
705
|
+
if (uv1) this.uv1(first + i, uv1[i * 2], uv1[i * 2 + 1]);
|
|
706
|
+
}
|
|
707
|
+
const tri = src ? src.indices : data.indices;
|
|
708
|
+
for (let k = 0; k < indexCount; k++) {
|
|
709
|
+
this.indices[this.indexCount++] = first + (tri ? tri[k] : k);
|
|
710
|
+
}
|
|
711
|
+
if (transform) this.transform(transform, first);
|
|
712
|
+
return this;
|
|
713
|
+
}
|
|
714
|
+
// --- cleanup and attributes -----------------------------------------------------------------------
|
|
715
|
+
/**
|
|
716
|
+
* Merges vertices closer than `epsilon` (positions only; the first vertex's attributes win) and
|
|
717
|
+
* drops triangles that collapse.
|
|
718
|
+
*/
|
|
719
|
+
weld(epsilon = 1e-6) {
|
|
720
|
+
const p = this.positions;
|
|
721
|
+
const n = this.vertexCount;
|
|
722
|
+
const inv = 1 / Math.max(epsilon, 1e-12);
|
|
723
|
+
const cells = /* @__PURE__ */ new Map();
|
|
724
|
+
const remap = new Int32Array(n);
|
|
725
|
+
let next = 0;
|
|
726
|
+
const keep = new Int32Array(n);
|
|
727
|
+
for (let i = 0; i < n; i++) {
|
|
728
|
+
const x = p[i * 3];
|
|
729
|
+
const y = p[i * 3 + 1];
|
|
730
|
+
const z = p[i * 3 + 2];
|
|
731
|
+
const cx = Math.floor(x * inv);
|
|
732
|
+
const cy = Math.floor(y * inv);
|
|
733
|
+
const cz = Math.floor(z * inv);
|
|
734
|
+
let found = -1;
|
|
735
|
+
for (let dz = -1; dz <= 1 && found < 0; dz++) {
|
|
736
|
+
for (let dy = -1; dy <= 1 && found < 0; dy++) {
|
|
737
|
+
for (let dx = -1; dx <= 1 && found < 0; dx++) {
|
|
738
|
+
const list2 = cells.get(`${cx + dx},${cy + dy},${cz + dz}`);
|
|
739
|
+
if (!list2) continue;
|
|
740
|
+
for (const j of list2) {
|
|
741
|
+
const ex = p[keep[j] * 3] - x;
|
|
742
|
+
const ey = p[keep[j] * 3 + 1] - y;
|
|
743
|
+
const ez = p[keep[j] * 3 + 2] - z;
|
|
744
|
+
if (ex * ex + ey * ey + ez * ez <= epsilon * epsilon) {
|
|
745
|
+
found = j;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (found >= 0) {
|
|
753
|
+
remap[i] = found;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
keep[next] = i;
|
|
757
|
+
remap[i] = next;
|
|
758
|
+
const key = `${cx},${cy},${cz}`;
|
|
759
|
+
const list = cells.get(key);
|
|
760
|
+
if (list) list.push(next);
|
|
761
|
+
else cells.set(key, [next]);
|
|
762
|
+
next++;
|
|
763
|
+
}
|
|
764
|
+
this.compact(keep, next, remap);
|
|
765
|
+
return this;
|
|
766
|
+
}
|
|
767
|
+
/** Keeps vertices `keep[0..count)` (in that order) and rewrites indices through `remap`. */
|
|
768
|
+
compact(keep, count, remap) {
|
|
769
|
+
const pick = (src, width) => {
|
|
770
|
+
const out = new Float64Array(Math.max(count, 1) * width);
|
|
771
|
+
for (let i = 0; i < count; i++)
|
|
772
|
+
for (let k = 0; k < width; k++) out[i * width + k] = src[keep[i] * width + k];
|
|
773
|
+
return out;
|
|
774
|
+
};
|
|
775
|
+
this.positions = pick(this.positions, 3);
|
|
776
|
+
this.normalData = pick(this.normalData, 3);
|
|
777
|
+
this.uvData = pick(this.uvData, 2);
|
|
778
|
+
if (this.colorData) this.colorData = pick(this.colorData, 4);
|
|
779
|
+
if (this.uv1Data) this.uv1Data = pick(this.uv1Data, 2);
|
|
780
|
+
if (this.tangentData) this.tangentData = pick(this.tangentData, 4);
|
|
781
|
+
this.vertexCount = count;
|
|
782
|
+
const t = this.indices;
|
|
783
|
+
let w = 0;
|
|
784
|
+
for (let k = 0; k < this.indexCount; k += 3) {
|
|
785
|
+
const a = remap[t[k]];
|
|
786
|
+
const b = remap[t[k + 1]];
|
|
787
|
+
const c = remap[t[k + 2]];
|
|
788
|
+
if (a === b || b === c || c === a) continue;
|
|
789
|
+
t[w++] = a;
|
|
790
|
+
t[w++] = b;
|
|
791
|
+
t[w++] = c;
|
|
792
|
+
}
|
|
793
|
+
this.indexCount = w;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Recomputes normals from the triangles (area-weighted). Faces meeting at more than `angle`
|
|
797
|
+
* degrees keep a hard edge (their shared vertices are split); `angle: 180` smooths everything.
|
|
798
|
+
*/
|
|
799
|
+
normals(options = {}) {
|
|
800
|
+
return this.computeNormals(options.angle ?? 180);
|
|
801
|
+
}
|
|
802
|
+
computeNormals(angle) {
|
|
803
|
+
const p = this.positions;
|
|
804
|
+
const t = this.indices;
|
|
805
|
+
const tris = this.triangleCount;
|
|
806
|
+
const fn = new Float64Array(tris * 3);
|
|
807
|
+
for (let f = 0; f < tris; f++) {
|
|
808
|
+
const a = t[f * 3] * 3;
|
|
809
|
+
const b = t[f * 3 + 1] * 3;
|
|
810
|
+
const c = t[f * 3 + 2] * 3;
|
|
811
|
+
const ux = p[b] - p[a];
|
|
812
|
+
const uy = p[b + 1] - p[a + 1];
|
|
813
|
+
const uz = p[b + 2] - p[a + 2];
|
|
814
|
+
const vx = p[c] - p[a];
|
|
815
|
+
const vy = p[c + 1] - p[a + 1];
|
|
816
|
+
const vz = p[c + 2] - p[a + 2];
|
|
817
|
+
fn[f * 3] = uy * vz - uz * vy;
|
|
818
|
+
fn[f * 3 + 1] = uz * vx - ux * vz;
|
|
819
|
+
fn[f * 3 + 2] = ux * vy - uy * vx;
|
|
820
|
+
}
|
|
821
|
+
if (angle >= 180) {
|
|
822
|
+
const acc = new Float64Array(this.vertexCount * 3);
|
|
823
|
+
for (let f = 0; f < tris; f++) {
|
|
824
|
+
for (let e = 0; e < 3; e++) {
|
|
825
|
+
const v = t[f * 3 + e] * 3;
|
|
826
|
+
acc[v] = acc[v] + fn[f * 3];
|
|
827
|
+
acc[v + 1] = acc[v + 1] + fn[f * 3 + 1];
|
|
828
|
+
acc[v + 2] = acc[v + 2] + fn[f * 3 + 2];
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
for (let i = 0; i < this.vertexCount; i++)
|
|
832
|
+
normalizeAt(this.normalData, i, acc[i * 3], acc[i * 3 + 1], acc[i * 3 + 2], 1);
|
|
833
|
+
this.hasNormals = true;
|
|
834
|
+
return this;
|
|
835
|
+
}
|
|
836
|
+
sinCos(angle * DEG, sc);
|
|
837
|
+
const cosLimit = sc[1];
|
|
838
|
+
const unit = new Float64Array(tris * 3);
|
|
839
|
+
for (let f = 0; f < tris; f++) {
|
|
840
|
+
const x = fn[f * 3];
|
|
841
|
+
const y = fn[f * 3 + 1];
|
|
842
|
+
const z = fn[f * 3 + 2];
|
|
843
|
+
const len = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
844
|
+
unit[f * 3] = x / len;
|
|
845
|
+
unit[f * 3 + 1] = y / len;
|
|
846
|
+
unit[f * 3 + 2] = z / len;
|
|
847
|
+
}
|
|
848
|
+
const start = new Int32Array(this.vertexCount + 1);
|
|
849
|
+
for (let k = 0; k < this.indexCount; k++) start[t[k] + 1]++;
|
|
850
|
+
for (let v = 0; v < this.vertexCount; v++) start[v + 1] += start[v];
|
|
851
|
+
const fill = start.slice();
|
|
852
|
+
const faces = new Int32Array(this.indexCount);
|
|
853
|
+
for (let k = 0; k < this.indexCount; k++) faces[fill[t[k]]++] = k / 3 | 0;
|
|
854
|
+
const original = this.vertexCount;
|
|
855
|
+
const corner = new Float64Array(3);
|
|
856
|
+
for (let v = 0; v < original; v++) {
|
|
857
|
+
const made = [];
|
|
858
|
+
const holders = [];
|
|
859
|
+
for (let s = start[v]; s < start[v + 1]; s++) {
|
|
860
|
+
const f = faces[s];
|
|
861
|
+
corner[0] = corner[1] = corner[2] = 0;
|
|
862
|
+
for (let s2 = start[v]; s2 < start[v + 1]; s2++) {
|
|
863
|
+
const g = faces[s2];
|
|
864
|
+
const dot = unit[f * 3] * unit[g * 3] + unit[f * 3 + 1] * unit[g * 3 + 1] + unit[f * 3 + 2] * unit[g * 3 + 2];
|
|
865
|
+
if (g !== f && dot < cosLimit) continue;
|
|
866
|
+
corner[0] += fn[g * 3];
|
|
867
|
+
corner[1] += fn[g * 3 + 1];
|
|
868
|
+
corner[2] += fn[g * 3 + 2];
|
|
869
|
+
}
|
|
870
|
+
const len = Math.sqrt(corner[0] ** 2 + corner[1] ** 2 + corner[2] ** 2) || 1;
|
|
871
|
+
const nx = corner[0] / len;
|
|
872
|
+
const ny = corner[1] / len;
|
|
873
|
+
const nz = corner[2] / len;
|
|
874
|
+
let holder = -1;
|
|
875
|
+
for (let m = 0; m < holders.length; m++) {
|
|
876
|
+
const dx = made[m * 3] - nx;
|
|
877
|
+
const dy = made[m * 3 + 1] - ny;
|
|
878
|
+
const dz = made[m * 3 + 2] - nz;
|
|
879
|
+
if (dx * dx + dy * dy + dz * dz < 1e-12) {
|
|
880
|
+
holder = holders[m];
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (holder < 0) {
|
|
885
|
+
holder = holders.length === 0 ? v : this.copyVertex(v);
|
|
886
|
+
holders.push(holder);
|
|
887
|
+
made.push(nx, ny, nz);
|
|
888
|
+
this.normalData[holder * 3] = nx;
|
|
889
|
+
this.normalData[holder * 3 + 1] = ny;
|
|
890
|
+
this.normalData[holder * 3 + 2] = nz;
|
|
891
|
+
}
|
|
892
|
+
for (let e = 0; e < 3; e++)
|
|
893
|
+
if (this.indices[f * 3 + e] === v) this.indices[f * 3 + e] = holder;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
this.hasNormals = true;
|
|
897
|
+
return this;
|
|
898
|
+
}
|
|
899
|
+
ensureNormals() {
|
|
900
|
+
if (!this.hasNormals) this.computeNormals(180);
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Per-vertex tangents (xyzw, w the handedness) from the UV gradient (Lengyel's method, averaged
|
|
904
|
+
* per vertex), or any direction perpendicular to the normal where there's no gradient.
|
|
905
|
+
*/
|
|
906
|
+
tangents() {
|
|
907
|
+
this.ensureNormals();
|
|
908
|
+
const count = this.vertexCount;
|
|
909
|
+
const p = this.positions;
|
|
910
|
+
const n = this.normalData;
|
|
911
|
+
const uv = this.uvData;
|
|
912
|
+
const tri = this.indices;
|
|
913
|
+
const tan = new Float64Array(count * 3);
|
|
914
|
+
const bit = new Float64Array(count * 3);
|
|
915
|
+
for (let k = 0; k < this.indexCount; k += 3) {
|
|
916
|
+
const a = tri[k];
|
|
917
|
+
const b = tri[k + 1];
|
|
918
|
+
const c = tri[k + 2];
|
|
919
|
+
const e1x = p[b * 3] - p[a * 3];
|
|
920
|
+
const e1y = p[b * 3 + 1] - p[a * 3 + 1];
|
|
921
|
+
const e1z = p[b * 3 + 2] - p[a * 3 + 2];
|
|
922
|
+
const e2x = p[c * 3] - p[a * 3];
|
|
923
|
+
const e2y = p[c * 3 + 1] - p[a * 3 + 1];
|
|
924
|
+
const e2z = p[c * 3 + 2] - p[a * 3 + 2];
|
|
925
|
+
const du1 = uv[b * 2] - uv[a * 2];
|
|
926
|
+
const dv1 = uv[b * 2 + 1] - uv[a * 2 + 1];
|
|
927
|
+
const du2 = uv[c * 2] - uv[a * 2];
|
|
928
|
+
const dv2 = uv[c * 2 + 1] - uv[a * 2 + 1];
|
|
929
|
+
const det = du1 * dv2 - du2 * dv1;
|
|
930
|
+
if (det === 0) continue;
|
|
931
|
+
const r = 1 / det;
|
|
932
|
+
const tx = (e1x * dv2 - e2x * dv1) * r;
|
|
933
|
+
const ty = (e1y * dv2 - e2y * dv1) * r;
|
|
934
|
+
const tz = (e1z * dv2 - e2z * dv1) * r;
|
|
935
|
+
const bx = (e2x * du1 - e1x * du2) * r;
|
|
936
|
+
const by = (e2y * du1 - e1y * du2) * r;
|
|
937
|
+
const bz = (e2z * du1 - e1z * du2) * r;
|
|
938
|
+
for (const v of [a, b, c]) {
|
|
939
|
+
tan[v * 3] = tan[v * 3] + tx;
|
|
940
|
+
tan[v * 3 + 1] = tan[v * 3 + 1] + ty;
|
|
941
|
+
tan[v * 3 + 2] = tan[v * 3 + 2] + tz;
|
|
942
|
+
bit[v * 3] = bit[v * 3] + bx;
|
|
943
|
+
bit[v * 3 + 1] = bit[v * 3 + 1] + by;
|
|
944
|
+
bit[v * 3 + 2] = bit[v * 3 + 2] + bz;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const out = new Float64Array(this.positions.length / 3 * 4);
|
|
948
|
+
for (let v = 0; v < count; v++) {
|
|
949
|
+
const nx = n[v * 3];
|
|
950
|
+
const ny = n[v * 3 + 1];
|
|
951
|
+
const nz = n[v * 3 + 2];
|
|
952
|
+
let tx = tan[v * 3];
|
|
953
|
+
let ty = tan[v * 3 + 1];
|
|
954
|
+
let tz = tan[v * 3 + 2];
|
|
955
|
+
if (tx === 0 && ty === 0 && tz === 0) {
|
|
956
|
+
const ax = Math.abs(nx);
|
|
957
|
+
const ay = Math.abs(ny);
|
|
958
|
+
const az = Math.abs(nz);
|
|
959
|
+
if (ax <= ay && ax <= az) tx = 1;
|
|
960
|
+
else if (ay <= az) ty = 1;
|
|
961
|
+
else tz = 1;
|
|
962
|
+
}
|
|
963
|
+
const d = nx * tx + ny * ty + nz * tz;
|
|
964
|
+
tx -= nx * d;
|
|
965
|
+
ty -= ny * d;
|
|
966
|
+
tz -= nz * d;
|
|
967
|
+
const len = Math.sqrt(tx * tx + ty * ty + tz * tz) || 1;
|
|
968
|
+
tx /= len;
|
|
969
|
+
ty /= len;
|
|
970
|
+
tz /= len;
|
|
971
|
+
const cx = ny * tz - nz * ty;
|
|
972
|
+
const cy = nz * tx - nx * tz;
|
|
973
|
+
const cz = nx * ty - ny * tx;
|
|
974
|
+
const w = cx * bit[v * 3] + cy * bit[v * 3 + 1] + cz * bit[v * 3 + 2] < 0 ? -1 : 1;
|
|
975
|
+
out[v * 4] = tx;
|
|
976
|
+
out[v * 4 + 1] = ty;
|
|
977
|
+
out[v * 4 + 2] = tz;
|
|
978
|
+
out[v * 4 + 3] = w;
|
|
979
|
+
}
|
|
980
|
+
this.tangentData = out;
|
|
981
|
+
return this;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* UVs projected along each vertex's dominant normal axis, `scale` metres per repeat: cheap
|
|
985
|
+
* texturing for rocks and crystals, with seams where the axis changes.
|
|
986
|
+
*/
|
|
987
|
+
uvsTriplanar(scale = 1) {
|
|
988
|
+
this.ensureNormals();
|
|
989
|
+
const p = this.positions;
|
|
990
|
+
const n = this.normalData;
|
|
991
|
+
const inv = 1 / scale;
|
|
992
|
+
for (let i = 0; i < this.vertexCount; i++) {
|
|
993
|
+
const ax = Math.abs(n[i * 3]);
|
|
994
|
+
const ay = Math.abs(n[i * 3 + 1]);
|
|
995
|
+
const az = Math.abs(n[i * 3 + 2]);
|
|
996
|
+
const x = p[i * 3] * inv;
|
|
997
|
+
const y = p[i * 3 + 1] * inv;
|
|
998
|
+
const z = p[i * 3 + 2] * inv;
|
|
999
|
+
let u;
|
|
1000
|
+
let v;
|
|
1001
|
+
if (ax >= ay && ax >= az) {
|
|
1002
|
+
u = n[i * 3] > 0 ? -z : z;
|
|
1003
|
+
v = -y;
|
|
1004
|
+
} else if (ay >= az) {
|
|
1005
|
+
u = x;
|
|
1006
|
+
v = n[i * 3 + 1] > 0 ? z : -z;
|
|
1007
|
+
} else {
|
|
1008
|
+
u = n[i * 3 + 2] > 0 ? x : -x;
|
|
1009
|
+
v = -y;
|
|
1010
|
+
}
|
|
1011
|
+
this.uvData[i * 2] = u;
|
|
1012
|
+
this.uvData[i * 2 + 1] = v;
|
|
1013
|
+
}
|
|
1014
|
+
return this;
|
|
1015
|
+
}
|
|
1016
|
+
/** The mesh as `MeshData` (f32 attributes; u16 indices when they fit). Normals are filled in. */
|
|
1017
|
+
finish() {
|
|
1018
|
+
if (this.vertexCount === 0) {
|
|
1019
|
+
throw new ShardError("mesh/bad-shape", "The builder has no vertices", {
|
|
1020
|
+
hint: "Append a shape (icosphere, box, lathe, tube, \u2026) before finish()."
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
this.ensureNormals();
|
|
1024
|
+
const n = this.vertexCount;
|
|
1025
|
+
const out = {
|
|
1026
|
+
positions: Float32Array.from(this.positions.subarray(0, n * 3)),
|
|
1027
|
+
normals: Float32Array.from(this.normalData.subarray(0, n * 3)),
|
|
1028
|
+
uvs: Float32Array.from(this.uvData.subarray(0, n * 2)),
|
|
1029
|
+
indices: n > 65535 ? this.indices.slice(0, this.indexCount) : Uint16Array.from(this.indices.subarray(0, this.indexCount))
|
|
1030
|
+
};
|
|
1031
|
+
if (this.colorData) out.colors = Float32Array.from(this.colorData.subarray(0, n * 4));
|
|
1032
|
+
if (this.uv1Data) out.uvs1 = Float32Array.from(this.uv1Data.subarray(0, n * 2));
|
|
1033
|
+
if (this.tangentData) out.tangents = Float32Array.from(this.tangentData.subarray(0, n * 4));
|
|
1034
|
+
return out;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const TAU = Math.PI * 2;
|
|
1038
|
+
const DEG = Math.PI / 180;
|
|
1039
|
+
const WHITE = [1, 1, 1, 1];
|
|
1040
|
+
const sc = new Float64Array(2);
|
|
1041
|
+
function grow(a, length) {
|
|
1042
|
+
const out = new Float64Array(length);
|
|
1043
|
+
out.set(a);
|
|
1044
|
+
return out;
|
|
1045
|
+
}
|
|
1046
|
+
function normalizeAt(n, i, x, y, z, fallbackY = 0) {
|
|
1047
|
+
const len = Math.sqrt(x * x + y * y + z * z);
|
|
1048
|
+
if (len > 0) {
|
|
1049
|
+
n[i * 3] = x / len;
|
|
1050
|
+
n[i * 3 + 1] = y / len;
|
|
1051
|
+
n[i * 3 + 2] = z / len;
|
|
1052
|
+
} else {
|
|
1053
|
+
n[i * 3] = 0;
|
|
1054
|
+
n[i * 3 + 1] = fallbackY;
|
|
1055
|
+
n[i * 3 + 2] = 0;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function perpendicular(x, y, z, out) {
|
|
1059
|
+
const ax = Math.abs(x);
|
|
1060
|
+
const ay = Math.abs(y);
|
|
1061
|
+
const az = Math.abs(z);
|
|
1062
|
+
let px;
|
|
1063
|
+
let py;
|
|
1064
|
+
let pz;
|
|
1065
|
+
if (ax <= ay && ax <= az) {
|
|
1066
|
+
px = 0;
|
|
1067
|
+
py = z;
|
|
1068
|
+
pz = -y;
|
|
1069
|
+
} else if (ay <= az) {
|
|
1070
|
+
px = -z;
|
|
1071
|
+
py = 0;
|
|
1072
|
+
pz = x;
|
|
1073
|
+
} else {
|
|
1074
|
+
px = y;
|
|
1075
|
+
py = -x;
|
|
1076
|
+
pz = 0;
|
|
1077
|
+
}
|
|
1078
|
+
const len = Math.sqrt(px * px + py * py + pz * pz) || 1;
|
|
1079
|
+
out[0] = px / len;
|
|
1080
|
+
out[1] = py / len;
|
|
1081
|
+
out[2] = pz / len;
|
|
1082
|
+
return out;
|
|
1083
|
+
}
|
|
1084
|
+
function normalMatrix(m) {
|
|
1085
|
+
const a = m[0];
|
|
1086
|
+
const b = m[1];
|
|
1087
|
+
const c = m[2];
|
|
1088
|
+
const d = m[4];
|
|
1089
|
+
const e = m[5];
|
|
1090
|
+
const f = m[6];
|
|
1091
|
+
const g = m[8];
|
|
1092
|
+
const h = m[9];
|
|
1093
|
+
const i = m[10];
|
|
1094
|
+
const out = new Float64Array(9);
|
|
1095
|
+
out[0] = e * i - f * h;
|
|
1096
|
+
out[1] = f * g - d * i;
|
|
1097
|
+
out[2] = d * h - e * g;
|
|
1098
|
+
out[3] = c * h - b * i;
|
|
1099
|
+
out[4] = a * i - c * g;
|
|
1100
|
+
out[5] = b * g - a * h;
|
|
1101
|
+
out[6] = b * f - c * e;
|
|
1102
|
+
out[7] = c * d - a * f;
|
|
1103
|
+
out[8] = a * e - b * d;
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
export {
|
|
1107
|
+
MeshBuilder,
|
|
1108
|
+
perpendicular
|
|
1109
|
+
};
|