@driftengine/assets 3.62.0 → 4.0.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/NOTICE +20 -0
- package/dist/bake/cluster.d.ts +49 -0
- package/dist/bake/cluster.js +277 -0
- package/dist/bake/clusterLod.d.ts +40 -0
- package/dist/bake/clusterLod.js +162 -0
- package/dist/bake/dtexChunk.d.ts +52 -0
- package/dist/bake/dtexChunk.js +103 -0
- package/dist/bake/hlod.d.ts +129 -0
- package/dist/bake/hlod.js +403 -0
- package/dist/bake/latent.d.ts +84 -0
- package/dist/bake/latent.js +339 -0
- package/dist/bake/sdf.d.ts +45 -0
- package/dist/bake/sdf.js +370 -0
- package/dist/drftLoader.d.ts +16 -0
- package/dist/drftLoader.js +23 -4
- package/dist/dtexTiles.d.ts +34 -0
- package/dist/dtexTiles.js +37 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +35 -0
- package/package.json +4 -3
- package/src/bake/cluster.ts +361 -0
- package/src/bake/clusterLod.ts +228 -0
- package/src/bake/dtexChunk.ts +156 -0
- package/src/bake/hlod.ts +487 -0
- package/src/bake/latent.ts +427 -0
- package/src/bake/sdf.ts +468 -0
- package/src/drftLoader.ts +41 -3
- package/src/dtexTiles.ts +61 -0
- package/src/index.ts +40 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splitting a mesh into clusters: the unit a GPU-driven frame culls, chooses detail for and draws.
|
|
3
|
+
*
|
|
4
|
+
* **A cluster is the unit of three separate decisions**, which is why its size is a parameter
|
|
5
|
+
* rather than a constant. Smaller clusters cull more precisely and pay more per-cluster overhead
|
|
6
|
+
* on every one of them; 128 triangles is the default because it is one workgroup of 128
|
|
7
|
+
* invocations doing one triangle each.
|
|
8
|
+
*
|
|
9
|
+
* **Growth is by shared vertices, and ties break on the lowest index.** That tie-break is not a
|
|
10
|
+
* detail: a baker whose output depends on iteration order produces a different container from the
|
|
11
|
+
* same source, which breaks every content hash downstream. The determinism test in this module's
|
|
12
|
+
* suite is what holds it.
|
|
13
|
+
*
|
|
14
|
+
* Each cluster carries a bounding sphere and a normal cone, because the runtime culls against both
|
|
15
|
+
* and computing them here costs nothing that the bake was not already paying.
|
|
16
|
+
*/
|
|
17
|
+
import type { MeshData } from '@driftengine/drft';
|
|
18
|
+
|
|
19
|
+
export interface ClusterSet {
|
|
20
|
+
/** First triangle of each cluster, as an index into `indices` divided by three. */
|
|
21
|
+
triangleOffsets: Uint32Array;
|
|
22
|
+
/** How many triangles each cluster holds. */
|
|
23
|
+
triangleCounts: Uint32Array;
|
|
24
|
+
/** The mesh's indices, reordered so each cluster's triangles are contiguous. */
|
|
25
|
+
indices: Uint32Array;
|
|
26
|
+
/** Three floats per cluster. */
|
|
27
|
+
boundsCentre: Float32Array;
|
|
28
|
+
/** One float per cluster. */
|
|
29
|
+
boundsRadius: Float32Array;
|
|
30
|
+
/** Three floats per cluster: the average face normal, normalised. */
|
|
31
|
+
coneAxis: Float32Array;
|
|
32
|
+
/** One float per cluster: the minimum dot product of any face normal against the axis. */
|
|
33
|
+
coneCutoff: Float32Array;
|
|
34
|
+
count: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Vertices shared between two triangles, each given as three vertex indices. */
|
|
38
|
+
function shared(a: Uint32Array, ai: number, b: Uint32Array, bi: number): number {
|
|
39
|
+
let count = 0;
|
|
40
|
+
for (let i = 0; i < 3; i += 1) {
|
|
41
|
+
const v = a[ai * 3 + i] as number;
|
|
42
|
+
for (let j = 0; j < 3; j += 1) if (v === (b[bi * 3 + j] as number)) count += 1;
|
|
43
|
+
}
|
|
44
|
+
return count;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function faceNormal(positions: Float32Array, indices: Uint32Array, tri: number, out: Float32Array) {
|
|
48
|
+
const a = (indices[tri * 3] as number) * 3;
|
|
49
|
+
const b = (indices[tri * 3 + 1] as number) * 3;
|
|
50
|
+
const c = (indices[tri * 3 + 2] as number) * 3;
|
|
51
|
+
const ux = (positions[b] as number) - (positions[a] as number);
|
|
52
|
+
const uy = (positions[b + 1] as number) - (positions[a + 1] as number);
|
|
53
|
+
const uz = (positions[b + 2] as number) - (positions[a + 2] as number);
|
|
54
|
+
const vx = (positions[c] as number) - (positions[a] as number);
|
|
55
|
+
const vy = (positions[c + 1] as number) - (positions[a + 1] as number);
|
|
56
|
+
const vz = (positions[c + 2] as number) - (positions[a + 2] as number);
|
|
57
|
+
let nx = uy * vz - uz * vy;
|
|
58
|
+
let ny = uz * vx - ux * vz;
|
|
59
|
+
let nz = ux * vy - uy * vx;
|
|
60
|
+
const length = Math.hypot(nx, ny, nz);
|
|
61
|
+
if (length > 0) {
|
|
62
|
+
nx /= length;
|
|
63
|
+
ny /= length;
|
|
64
|
+
nz /= length;
|
|
65
|
+
}
|
|
66
|
+
out[0] = nx;
|
|
67
|
+
out[1] = ny;
|
|
68
|
+
out[2] = nz;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every triangle touching each vertex, as a compressed row: offsets, then the triangles.
|
|
73
|
+
*
|
|
74
|
+
* **This is what makes the growth linear rather than cubic**, and the difference is not an
|
|
75
|
+
* optimisation. Growth picks the unassigned triangle sharing the most vertices with the cluster so
|
|
76
|
+
* far, and a triangle sharing none can never be picked — the loop stops when the best score is
|
|
77
|
+
* zero. So the only triangles worth considering are the ones reachable through a vertex, and this
|
|
78
|
+
* is the map that says which those are. Scanning the whole mesh to discover that answer is what
|
|
79
|
+
* the first version here did: 128 triangles took 16 ms, 2,048 took 1.9 s, 4,608 took 9.7 s, and a
|
|
80
|
+
* million-triangle asset extrapolated to days.
|
|
81
|
+
*
|
|
82
|
+
* Two passes and no arrays of arrays, because a million-triangle mesh is a million small
|
|
83
|
+
* allocations otherwise.
|
|
84
|
+
*/
|
|
85
|
+
function vertexTriangles(
|
|
86
|
+
indices: Uint32Array,
|
|
87
|
+
vertices: number,
|
|
88
|
+
): { starts: Uint32Array; list: Uint32Array } {
|
|
89
|
+
const triangles = Math.floor(indices.length / 3);
|
|
90
|
+
const starts = new Uint32Array(vertices + 1);
|
|
91
|
+
for (let i = 0; i < triangles * 3; i += 1) starts[(indices[i] as number) + 1] += 1;
|
|
92
|
+
for (let v = 0; v < vertices; v += 1) {
|
|
93
|
+
starts[v + 1] = (starts[v + 1] as number) + (starts[v] as number);
|
|
94
|
+
}
|
|
95
|
+
const cursor = Uint32Array.from(starts.subarray(0, vertices));
|
|
96
|
+
const list = new Uint32Array(triangles * 3);
|
|
97
|
+
for (let t = 0; t < triangles; t += 1) {
|
|
98
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
99
|
+
const v = indices[t * 3 + corner] as number;
|
|
100
|
+
list[cursor[v] as number] = t;
|
|
101
|
+
cursor[v] = (cursor[v] as number) + 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { starts, list };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ClusterOptions {
|
|
108
|
+
/**
|
|
109
|
+
* What makes two triangles neighbours while a cluster grows. **`'index'` by default**, which is
|
|
110
|
+
* what every baked container was hashed with and must go on producing.
|
|
111
|
+
*
|
|
112
|
+
* **`'position'` counts two corners as shared when they stand at the same point**, whatever their
|
|
113
|
+
* indices. A mesh that splits its vertices at every seam — a voxel mesher, whose faces each carry
|
|
114
|
+
* their own uv and occlusion, or a box built face by face — shares no index between two faces,
|
|
115
|
+
* so growth by index stops at every face and each quad becomes a cluster of two triangles: a
|
|
116
|
+
* median voxel chunk was 1,012 clusters for 2,024 triangles. The indices written out are still
|
|
117
|
+
* the mesh's own, because welding decides who is a neighbour and nothing else.
|
|
118
|
+
*/
|
|
119
|
+
readonly share?: 'index' | 'position';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Each index replaced by the first vertex standing where it stands.
|
|
124
|
+
*
|
|
125
|
+
* **The first, in vertex order**, so the answer depends on the mesh and not on a hash table's
|
|
126
|
+
* iteration — which is the reproducibility the tie-break above is written for. Keyed on the
|
|
127
|
+
* numbers rather than their bits, so a corner at `-0` meets one at `0`.
|
|
128
|
+
*/
|
|
129
|
+
function canonicalIndices(
|
|
130
|
+
indices: Uint32Array,
|
|
131
|
+
positions: Float32Array,
|
|
132
|
+
vertices: number,
|
|
133
|
+
): Uint32Array {
|
|
134
|
+
const first = new Map<string, number>();
|
|
135
|
+
const canonical = new Uint32Array(vertices);
|
|
136
|
+
for (let v = 0; v < vertices; v += 1) {
|
|
137
|
+
const key = `${positions[v * 3]},${positions[v * 3 + 1]},${positions[v * 3 + 2]}`;
|
|
138
|
+
const known = first.get(key);
|
|
139
|
+
if (known === undefined) {
|
|
140
|
+
first.set(key, v);
|
|
141
|
+
canonical[v] = v;
|
|
142
|
+
} else {
|
|
143
|
+
canonical[v] = known;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return Uint32Array.from(indices, (index) => canonical[index] as number);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildClusters(
|
|
150
|
+
mesh: MeshData,
|
|
151
|
+
target: number,
|
|
152
|
+
options: ClusterOptions = {},
|
|
153
|
+
): ClusterSet {
|
|
154
|
+
const source = mesh.indices as Uint32Array;
|
|
155
|
+
const triangles = Math.floor(source.length / 3);
|
|
156
|
+
const positions = mesh.positions;
|
|
157
|
+
const vertices = Math.floor(positions.length / 3);
|
|
158
|
+
|
|
159
|
+
const assigned = new Uint8Array(triangles);
|
|
160
|
+
const indices = new Uint32Array(source.length);
|
|
161
|
+
const offsets: number[] = [];
|
|
162
|
+
const counts: number[] = [];
|
|
163
|
+
/*
|
|
164
|
+
* **Who neighbours whom**, which is the source itself unless the caller asked for positions.
|
|
165
|
+
* Growth and scoring read this; what is written out reads `source`.
|
|
166
|
+
*/
|
|
167
|
+
const linked =
|
|
168
|
+
options.share === 'position' ? canonicalIndices(source, positions, vertices) : source;
|
|
169
|
+
const adjacency = vertexTriangles(linked, vertices);
|
|
170
|
+
|
|
171
|
+
const member: number[] = [];
|
|
172
|
+
/*
|
|
173
|
+
* The frontier: unassigned triangles that share at least one vertex with a member, and the best
|
|
174
|
+
* score each has against any member so far. `score` is indexed by triangle and is only read for
|
|
175
|
+
* triangles in `frontier`, so it never has to be cleared between clusters.
|
|
176
|
+
*/
|
|
177
|
+
const frontier: number[] = [];
|
|
178
|
+
const score = new Uint8Array(triangles);
|
|
179
|
+
const queued = new Uint8Array(triangles);
|
|
180
|
+
|
|
181
|
+
let written = 0;
|
|
182
|
+
let placed = 0;
|
|
183
|
+
/* The lowest unassigned triangle seeds the next cluster, so the walk is reproducible. Monotone,
|
|
184
|
+
because every triangle below it is assigned — scanning from zero each time is a second
|
|
185
|
+
quadratic term hiding behind the first. */
|
|
186
|
+
let nextSeed = 0;
|
|
187
|
+
|
|
188
|
+
while (placed < triangles) {
|
|
189
|
+
while (nextSeed < triangles && assigned[nextSeed] === 1) nextSeed += 1;
|
|
190
|
+
if (nextSeed >= triangles) break;
|
|
191
|
+
const seed = nextSeed;
|
|
192
|
+
|
|
193
|
+
member.length = 0;
|
|
194
|
+
frontier.length = 0;
|
|
195
|
+
member.push(seed);
|
|
196
|
+
assigned[seed] = 1;
|
|
197
|
+
placed += 1;
|
|
198
|
+
offer(seed);
|
|
199
|
+
|
|
200
|
+
while (member.length < target) {
|
|
201
|
+
/*
|
|
202
|
+
* The best score, ties broken by the lowest triangle index — the same answer the whole-mesh
|
|
203
|
+
* scan gave, because it ran in index order and compared strictly greater. The frontier is
|
|
204
|
+
* not in index order, so the tie-break is written out rather than inherited from the loop.
|
|
205
|
+
*/
|
|
206
|
+
let best = -1;
|
|
207
|
+
let bestScore = 0;
|
|
208
|
+
let live = 0;
|
|
209
|
+
for (let i = 0; i < frontier.length; i += 1) {
|
|
210
|
+
const t = frontier[i] as number;
|
|
211
|
+
if (assigned[t] === 1) {
|
|
212
|
+
queued[t] = 0;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
/* Compacted in place as it is scanned, so a consumed frontier entry is not walked again. */
|
|
216
|
+
frontier[live] = t;
|
|
217
|
+
live += 1;
|
|
218
|
+
const s = score[t] as number;
|
|
219
|
+
if (s > bestScore || (s === bestScore && best >= 0 && t < best)) {
|
|
220
|
+
bestScore = s;
|
|
221
|
+
best = t;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
frontier.length = live;
|
|
225
|
+
if (best === -1 || bestScore <= 0) break;
|
|
226
|
+
member.push(best);
|
|
227
|
+
assigned[best] = 1;
|
|
228
|
+
placed += 1;
|
|
229
|
+
offer(best);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* **`queued` is per cluster and the frontier is where it is recorded.** Leaving a triangle
|
|
234
|
+
* marked from a cluster that never took it makes it invisible to every cluster afterwards —
|
|
235
|
+
* `offer` would update its score and never put it on the new frontier — and what that looks
|
|
236
|
+
* like is growth stopping early everywhere: 25,088 triangles came out as 11,860 clusters
|
|
237
|
+
* rather than 196.
|
|
238
|
+
*
|
|
239
|
+
* **The frontier alone is enough and a second loop over the members is not.** Every member
|
|
240
|
+
* except the seed was offered before it was chosen, so it is in this array; the seed was
|
|
241
|
+
* already assigned when its own `offer` ran and was never marked. A loop over the members
|
|
242
|
+
* stood here and a perturbation survived it.
|
|
243
|
+
*/
|
|
244
|
+
for (const t of frontier) queued[t] = 0;
|
|
245
|
+
|
|
246
|
+
offsets.push(written / 3);
|
|
247
|
+
counts.push(member.length);
|
|
248
|
+
for (const t of member) {
|
|
249
|
+
indices[written] = source[t * 3] as number;
|
|
250
|
+
indices[written + 1] = source[t * 3 + 1] as number;
|
|
251
|
+
indices[written + 2] = source[t * 3 + 2] as number;
|
|
252
|
+
written += 3;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Raise every neighbour of a new member to the score it now has against the cluster. */
|
|
257
|
+
function offer(added: number): void {
|
|
258
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
259
|
+
const v = linked[added * 3 + corner] as number;
|
|
260
|
+
const from = adjacency.starts[v] as number;
|
|
261
|
+
const to = adjacency.starts[v + 1] as number;
|
|
262
|
+
for (let i = from; i < to; i += 1) {
|
|
263
|
+
const t = adjacency.list[i] as number;
|
|
264
|
+
/* Already in a cluster. The frontier scan refuses it again, so deleting this changes
|
|
265
|
+
no answer — it is a cost guard, and the cost is 105 ms against 89 ms over a mesh of
|
|
266
|
+
100,352 triangles, which is an eighteenth of the bake. A perturbation survives it. */
|
|
267
|
+
if (assigned[t] === 1) continue;
|
|
268
|
+
const shares = shared(linked, added, linked, t);
|
|
269
|
+
if (queued[t] === 0) {
|
|
270
|
+
queued[t] = 1;
|
|
271
|
+
score[t] = shares;
|
|
272
|
+
frontier.push(t);
|
|
273
|
+
} else if (shares > (score[t] as number)) {
|
|
274
|
+
score[t] = shares;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const count = offsets.length;
|
|
281
|
+
const set: ClusterSet = {
|
|
282
|
+
triangleOffsets: Uint32Array.from(offsets),
|
|
283
|
+
triangleCounts: Uint32Array.from(counts),
|
|
284
|
+
indices,
|
|
285
|
+
boundsCentre: new Float32Array(count * 3),
|
|
286
|
+
boundsRadius: new Float32Array(count),
|
|
287
|
+
coneAxis: new Float32Array(count * 3),
|
|
288
|
+
coneCutoff: new Float32Array(count),
|
|
289
|
+
count,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const normal = new Float32Array(3);
|
|
293
|
+
for (let c = 0; c < count; c += 1) {
|
|
294
|
+
const at = set.triangleOffsets[c] as number;
|
|
295
|
+
const n = set.triangleCounts[c] as number;
|
|
296
|
+
|
|
297
|
+
let cx = 0;
|
|
298
|
+
let cy = 0;
|
|
299
|
+
let cz = 0;
|
|
300
|
+
let vertices = 0;
|
|
301
|
+
for (let i = at * 3; i < (at + n) * 3; i += 1) {
|
|
302
|
+
const v = (indices[i] as number) * 3;
|
|
303
|
+
cx += positions[v] as number;
|
|
304
|
+
cy += positions[v + 1] as number;
|
|
305
|
+
cz += positions[v + 2] as number;
|
|
306
|
+
vertices += 1;
|
|
307
|
+
}
|
|
308
|
+
cx /= vertices;
|
|
309
|
+
cy /= vertices;
|
|
310
|
+
cz /= vertices;
|
|
311
|
+
|
|
312
|
+
let radius = 0;
|
|
313
|
+
for (let i = at * 3; i < (at + n) * 3; i += 1) {
|
|
314
|
+
const v = (indices[i] as number) * 3;
|
|
315
|
+
radius = Math.max(
|
|
316
|
+
radius,
|
|
317
|
+
Math.hypot(
|
|
318
|
+
(positions[v] as number) - cx,
|
|
319
|
+
(positions[v + 1] as number) - cy,
|
|
320
|
+
(positions[v + 2] as number) - cz,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let ax = 0;
|
|
326
|
+
let ay = 0;
|
|
327
|
+
let az = 0;
|
|
328
|
+
for (let t = at; t < at + n; t += 1) {
|
|
329
|
+
faceNormal(positions, indices, t, normal);
|
|
330
|
+
ax += normal[0] as number;
|
|
331
|
+
ay += normal[1] as number;
|
|
332
|
+
az += normal[2] as number;
|
|
333
|
+
}
|
|
334
|
+
const axisLength = Math.hypot(ax, ay, az);
|
|
335
|
+
if (axisLength > 0) {
|
|
336
|
+
ax /= axisLength;
|
|
337
|
+
ay /= axisLength;
|
|
338
|
+
az /= axisLength;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let cutoff = 1;
|
|
342
|
+
for (let t = at; t < at + n; t += 1) {
|
|
343
|
+
faceNormal(positions, indices, t, normal);
|
|
344
|
+
cutoff = Math.min(
|
|
345
|
+
cutoff,
|
|
346
|
+
(normal[0] as number) * ax + (normal[1] as number) * ay + (normal[2] as number) * az,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
set.boundsCentre[c * 3] = cx;
|
|
351
|
+
set.boundsCentre[c * 3 + 1] = cy;
|
|
352
|
+
set.boundsCentre[c * 3 + 2] = cz;
|
|
353
|
+
set.boundsRadius[c] = radius;
|
|
354
|
+
set.coneAxis[c * 3] = ax;
|
|
355
|
+
set.coneAxis[c * 3 + 1] = ay;
|
|
356
|
+
set.coneAxis[c * 3 + 2] = az;
|
|
357
|
+
set.coneCutoff[c] = cutoff;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return set;
|
|
361
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The level-of-detail graph over a mesh's clusters, and the property the whole pipeline rests on.
|
|
3
|
+
*
|
|
4
|
+
* **A cluster's own error must never exceed its parent's.** Given that, a cut chosen independently
|
|
5
|
+
* per cluster — draw this one if its projected error is under the threshold, otherwise ask its
|
|
6
|
+
* parent — is guaranteed crack-free, because two adjacent clusters can never choose levels whose
|
|
7
|
+
* shared edge was simplified differently. Without it the pipeline draws holes, and the holes appear
|
|
8
|
+
* at viewing distances nobody tested. The property holds here **by construction**: each level's
|
|
9
|
+
* error is forced up to at least its children's maximum.
|
|
10
|
+
*
|
|
11
|
+
* **Simplification is vertex clustering on a grid, not edge collapse, and that is a deliberate
|
|
12
|
+
* first implementation.** Grid snapping is deterministic without a tie-break argument, needs no
|
|
13
|
+
* dependency, and — the part that matters — has an error bound that is *known in advance* rather
|
|
14
|
+
* than measured after the fact: no vertex moves further than half a cell diagonal. A quadric edge
|
|
15
|
+
* collapse would give a better-looking result at the same triangle count and would have to
|
|
16
|
+
* measure its error to keep the invariant above. The graph shape, the error discipline and every
|
|
17
|
+
* consumer of both are identical either way, so the better simplifier is a drop-in replacement
|
|
18
|
+
* for `simplifyGroup` and nothing else.
|
|
19
|
+
*/
|
|
20
|
+
import type { MeshData } from '@driftengine/drft';
|
|
21
|
+
import { buildClusters } from './cluster.ts';
|
|
22
|
+
import type { ClusterSet } from './cluster.ts';
|
|
23
|
+
|
|
24
|
+
export interface ClusterLevel {
|
|
25
|
+
/** The clusters at this level. Level 0 is the source mesh's. */
|
|
26
|
+
set: ClusterSet;
|
|
27
|
+
/** Positions the level's indices address. Level 0 shares the source mesh's. */
|
|
28
|
+
positions: Float32Array;
|
|
29
|
+
/** Geometric error each cluster at this level carries. Zero at level 0. */
|
|
30
|
+
ownError: Float32Array;
|
|
31
|
+
/** The error of the group that replaces this cluster one level up, or Infinity at the root. */
|
|
32
|
+
parentError: Float32Array;
|
|
33
|
+
/** Which group of this level was simplified together. Clusters sharing one share a parent. */
|
|
34
|
+
groupOf: Uint32Array;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClusterDag {
|
|
38
|
+
levels: ClusterLevel[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The mean length of a triangle edge, which is the mesh's own sense of how far apart its detail is.
|
|
43
|
+
*
|
|
44
|
+
* Sampled over every triangle rather than every unique edge: each interior edge is counted twice
|
|
45
|
+
* and each boundary edge once, which biases the mean by a fraction of a percent on any mesh large
|
|
46
|
+
* enough to matter and saves building an edge set.
|
|
47
|
+
*/
|
|
48
|
+
function meanEdgeLength(positions: Float32Array, indices: Uint32Array): number {
|
|
49
|
+
let total = 0;
|
|
50
|
+
let count = 0;
|
|
51
|
+
for (let t = 0; t < indices.length / 3; t += 1) {
|
|
52
|
+
for (let e = 0; e < 3; e += 1) {
|
|
53
|
+
const a = (indices[t * 3 + e] as number) * 3;
|
|
54
|
+
const b = (indices[t * 3 + ((e + 1) % 3)] as number) * 3;
|
|
55
|
+
total += Math.hypot(
|
|
56
|
+
(positions[b] as number) - (positions[a] as number),
|
|
57
|
+
(positions[b + 1] as number) - (positions[a + 1] as number),
|
|
58
|
+
(positions[b + 2] as number) - (positions[a + 2] as number),
|
|
59
|
+
);
|
|
60
|
+
count += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return count === 0 ? 0 : total / count;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** How far a vertex can move when snapped to a grid of this size: half the cell diagonal. */
|
|
67
|
+
function snapError(cell: number): number {
|
|
68
|
+
return (cell * Math.sqrt(3)) / 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collapse a group's triangles by snapping vertices to a grid and dropping what degenerates.
|
|
73
|
+
*
|
|
74
|
+
* Returns the surviving geometry and the error bound, which is a property of the grid rather than
|
|
75
|
+
* a measurement of the result — see the header for why that matters.
|
|
76
|
+
*/
|
|
77
|
+
function simplifyGroup(
|
|
78
|
+
positions: Float32Array,
|
|
79
|
+
indices: Uint32Array,
|
|
80
|
+
cell: number,
|
|
81
|
+
): { positions: Float32Array; indices: Uint32Array; error: number } {
|
|
82
|
+
const keyOf = new Map<string, number>();
|
|
83
|
+
const remap = new Map<number, number>();
|
|
84
|
+
const out: number[] = [];
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < indices.length; i += 1) {
|
|
87
|
+
const v = indices[i] as number;
|
|
88
|
+
if (remap.has(v)) continue;
|
|
89
|
+
const at = v * 3;
|
|
90
|
+
const gx = Math.round((positions[at] as number) / cell);
|
|
91
|
+
const gy = Math.round((positions[at + 1] as number) / cell);
|
|
92
|
+
const gz = Math.round((positions[at + 2] as number) / cell);
|
|
93
|
+
const key = `${gx},${gy},${gz}`;
|
|
94
|
+
let slot = keyOf.get(key);
|
|
95
|
+
if (slot === undefined) {
|
|
96
|
+
slot = out.length / 3;
|
|
97
|
+
keyOf.set(key, slot);
|
|
98
|
+
out.push(gx * cell, gy * cell, gz * cell);
|
|
99
|
+
}
|
|
100
|
+
remap.set(v, slot);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const kept: number[] = [];
|
|
104
|
+
for (let t = 0; t < indices.length / 3; t += 1) {
|
|
105
|
+
const a = remap.get(indices[t * 3] as number) as number;
|
|
106
|
+
const b = remap.get(indices[t * 3 + 1] as number) as number;
|
|
107
|
+
const c = remap.get(indices[t * 3 + 2] as number) as number;
|
|
108
|
+
/* A triangle whose corners snapped together has no area and is dropped, not kept flat. */
|
|
109
|
+
if (a === b || b === c || a === c) continue;
|
|
110
|
+
kept.push(a, b, c);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
positions: Float32Array.from(out),
|
|
115
|
+
indices: Uint32Array.from(kept),
|
|
116
|
+
error: snapError(cell),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Group clusters for simplification, by proximity, `size` at a time.
|
|
122
|
+
*
|
|
123
|
+
* Ordered by cluster index rather than by a spatial sort, then filled greedily from the nearest
|
|
124
|
+
* unassigned centroid — deterministic, and close enough to spatial that groups are contiguous on
|
|
125
|
+
* ordinary geometry. Clusters in one group are simplified together, so edges interior to the group
|
|
126
|
+
* may collapse and edges on its boundary may not.
|
|
127
|
+
*/
|
|
128
|
+
function groupClusters(set: ClusterSet, size: number): Uint32Array {
|
|
129
|
+
const groupOf = new Uint32Array(set.count).fill(0xffffffff);
|
|
130
|
+
let group = 0;
|
|
131
|
+
for (let seed = 0; seed < set.count; seed += 1) {
|
|
132
|
+
if (groupOf[seed] !== 0xffffffff) continue;
|
|
133
|
+
groupOf[seed] = group;
|
|
134
|
+
let taken = 1;
|
|
135
|
+
while (taken < size) {
|
|
136
|
+
let best = -1;
|
|
137
|
+
let bestDistance = Infinity;
|
|
138
|
+
for (let c = 0; c < set.count; c += 1) {
|
|
139
|
+
if (groupOf[c] !== 0xffffffff) continue;
|
|
140
|
+
const d = Math.hypot(
|
|
141
|
+
(set.boundsCentre[c * 3] as number) - (set.boundsCentre[seed * 3] as number),
|
|
142
|
+
(set.boundsCentre[c * 3 + 1] as number) - (set.boundsCentre[seed * 3 + 1] as number),
|
|
143
|
+
(set.boundsCentre[c * 3 + 2] as number) - (set.boundsCentre[seed * 3 + 2] as number),
|
|
144
|
+
);
|
|
145
|
+
if (d < bestDistance) {
|
|
146
|
+
bestDistance = d;
|
|
147
|
+
best = c;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (best === -1) break;
|
|
151
|
+
groupOf[best] = group;
|
|
152
|
+
taken += 1;
|
|
153
|
+
}
|
|
154
|
+
group += 1;
|
|
155
|
+
}
|
|
156
|
+
return groupOf;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const GROUP_SIZE = 4;
|
|
160
|
+
|
|
161
|
+
export function buildClusterDag(
|
|
162
|
+
set: ClusterSet,
|
|
163
|
+
mesh: MeshData,
|
|
164
|
+
options: { target?: number; maxLevels?: number } = {},
|
|
165
|
+
): ClusterDag {
|
|
166
|
+
const target = options.target ?? 128;
|
|
167
|
+
const maxLevels = options.maxLevels ?? 8;
|
|
168
|
+
|
|
169
|
+
const levels: ClusterLevel[] = [];
|
|
170
|
+
let currentSet = set;
|
|
171
|
+
let currentPositions = mesh.positions;
|
|
172
|
+
let currentError = new Float32Array(set.count);
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* The starting cell comes from the mesh's own edge length, not from its extent.
|
|
176
|
+
*
|
|
177
|
+
* Derived from the extent it is arbitrary, and arbitrary here means wrong in a way that looks
|
|
178
|
+
* like the algorithm failing: a cell finer than the vertex spacing merges nothing, the triangle
|
|
179
|
+
* count does not fall, and the build stops at one level believing the mesh cannot be
|
|
180
|
+
* simplified. Starting a little above the mean edge collapses neighbouring vertices on the
|
|
181
|
+
* first pass, which is what the doubling then builds on.
|
|
182
|
+
*/
|
|
183
|
+
let cell = Math.max(1e-3, meanEdgeLength(mesh.positions, set.indices) * 1.5);
|
|
184
|
+
|
|
185
|
+
for (let level = 0; level < maxLevels; level += 1) {
|
|
186
|
+
const groupOf = groupClusters(currentSet, GROUP_SIZE);
|
|
187
|
+
const parentError = new Float32Array(currentSet.count);
|
|
188
|
+
|
|
189
|
+
const last = currentSet.count <= 1;
|
|
190
|
+
if (last) {
|
|
191
|
+
parentError.fill(Infinity);
|
|
192
|
+
levels.push({
|
|
193
|
+
set: currentSet,
|
|
194
|
+
positions: currentPositions,
|
|
195
|
+
ownError: currentError,
|
|
196
|
+
parentError,
|
|
197
|
+
groupOf,
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Every cluster of a group takes the group's error, so two in one group cannot disagree. */
|
|
203
|
+
const error = Math.max(snapError(cell), ...Array.from(currentError));
|
|
204
|
+
for (let c = 0; c < currentSet.count; c += 1) parentError[c] = error;
|
|
205
|
+
|
|
206
|
+
levels.push({
|
|
207
|
+
set: currentSet,
|
|
208
|
+
positions: currentPositions,
|
|
209
|
+
ownError: currentError,
|
|
210
|
+
parentError,
|
|
211
|
+
groupOf,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const simplified = simplifyGroup(currentPositions, currentSet.indices, cell);
|
|
215
|
+
if (simplified.indices.length === 0 || simplified.indices.length >= currentSet.indices.length) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const nextMesh = { ...mesh, positions: simplified.positions, indices: simplified.indices };
|
|
220
|
+
const nextSet = buildClusters(nextMesh as MeshData, target);
|
|
221
|
+
currentSet = nextSet;
|
|
222
|
+
currentPositions = simplified.positions;
|
|
223
|
+
currentError = new Float32Array(nextSet.count).fill(error);
|
|
224
|
+
cell *= 2;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { levels };
|
|
228
|
+
}
|