three-virtual-geometry 0.0.0-stage → 0.1.1
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/AGENTS.md +65 -0
- package/LICENSE +23 -0
- package/README.md +165 -2
- package/THIRD_PARTY_NOTICES.md +42 -0
- package/dist/bake.mjs +1378 -0
- package/dist/core/constants.d.ts +27 -0
- package/dist/core/import/fromObject3D.d.ts +116 -0
- package/dist/core/import/toNodeMaterial.d.ts +10 -0
- package/dist/core/io/cache.d.ts +32 -0
- package/dist/core/io/serialize.d.ts +21 -0
- package/dist/core/preprocess/buildVirtualMesh.d.ts +105 -0
- package/dist/core/preprocess/partition.d.ts +16 -0
- package/dist/core/preprocess/voxelProxy.d.ts +30 -0
- package/dist/core/runtime/GeometryPool.d.ts +219 -0
- package/dist/core/runtime/OcclusionCulling.d.ts +100 -0
- package/dist/core/runtime/VirtualGeometry.d.ts +201 -0
- package/dist/core/runtime/VirtualMesh.d.ts +93 -0
- package/dist/core/runtime/cut.d.ts +50 -0
- package/dist/core/runtime/vgMaterial.d.ts +33 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +2470 -0
- package/dist/index.js.map +1 -0
- package/dist/source.d.ts +34 -0
- package/dist/three-virtual-geometry.all.min.js +585 -0
- package/dist/three-virtual-geometry.all.min.js.map +6 -0
- package/dist/three-virtual-geometry.min.js +7 -0
- package/dist/three-virtual-geometry.min.js.map +6 -0
- package/docs/public/llms.txt +101 -0
- package/package.json +89 -4
package/dist/bake.mjs
ADDED
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/bake.ts
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
7
|
+
import * as THREE2 from "three";
|
|
8
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
9
|
+
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
|
10
|
+
import { MeshoptDecoder as MeshoptDecoder2 } from "meshoptimizer";
|
|
11
|
+
|
|
12
|
+
// src/core/preprocess/buildVirtualMesh.ts
|
|
13
|
+
import { MeshoptClusterizer, MeshoptSimplifier } from "meshoptimizer";
|
|
14
|
+
|
|
15
|
+
// src/core/constants.ts
|
|
16
|
+
var MAX_MESHLET_TRIANGLES = 128;
|
|
17
|
+
var MAX_MESHLET_VERTICES = 128;
|
|
18
|
+
var ERROR_INFINITY = 1e30;
|
|
19
|
+
var MESHLET_BOUNDS_STRIDE = 16;
|
|
20
|
+
var MESHLET_INFO_STRIDE = 4;
|
|
21
|
+
|
|
22
|
+
// src/core/preprocess/partition.ts
|
|
23
|
+
function partitionMeshlets(meshlets, groupSize) {
|
|
24
|
+
const count = meshlets.length;
|
|
25
|
+
if (count <= groupSize) return [meshlets.map((_, i) => i)];
|
|
26
|
+
const edgeOwners = /* @__PURE__ */ new Map();
|
|
27
|
+
for (let i = 0; i < count; i++) {
|
|
28
|
+
for (const e of meshlets[i].boundaryEdges) {
|
|
29
|
+
const owners = edgeOwners.get(e);
|
|
30
|
+
if (owners) {
|
|
31
|
+
if (owners[owners.length - 1] !== i) owners.push(i);
|
|
32
|
+
} else edgeOwners.set(e, [i]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const adjacency = Array.from({ length: count }, () => /* @__PURE__ */ new Map());
|
|
36
|
+
for (const owners of edgeOwners.values()) {
|
|
37
|
+
for (let a = 0; a < owners.length; a++) {
|
|
38
|
+
for (let b = a + 1; b < owners.length; b++) {
|
|
39
|
+
const i = owners[a];
|
|
40
|
+
const j = owners[b];
|
|
41
|
+
adjacency[i].set(j, (adjacency[i].get(j) || 0) + 1);
|
|
42
|
+
adjacency[j].set(i, (adjacency[j].get(i) || 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const order = mortonOrder(meshlets.map((m) => m.center));
|
|
47
|
+
const assigned = new Uint8Array(count);
|
|
48
|
+
const groups = [];
|
|
49
|
+
for (const seed of order) {
|
|
50
|
+
if (assigned[seed]) continue;
|
|
51
|
+
const group = [seed];
|
|
52
|
+
assigned[seed] = 1;
|
|
53
|
+
const frontier = /* @__PURE__ */ new Map();
|
|
54
|
+
const addNeighbours = (m) => {
|
|
55
|
+
for (const [n, shared] of adjacency[m]) {
|
|
56
|
+
if (!assigned[n]) frontier.set(n, (frontier.get(n) || 0) + shared);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
addNeighbours(seed);
|
|
60
|
+
while (group.length < groupSize && frontier.size > 0) {
|
|
61
|
+
let best = -1;
|
|
62
|
+
let bestScore = -1;
|
|
63
|
+
let bestDist = Infinity;
|
|
64
|
+
for (const [n, score] of frontier) {
|
|
65
|
+
if (assigned[n]) continue;
|
|
66
|
+
const d = dist2(meshlets[n].center, meshlets[seed].center);
|
|
67
|
+
if (score > bestScore || score === bestScore && d < bestDist) {
|
|
68
|
+
best = n;
|
|
69
|
+
bestScore = score;
|
|
70
|
+
bestDist = d;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (best < 0) break;
|
|
74
|
+
frontier.delete(best);
|
|
75
|
+
assigned[best] = 1;
|
|
76
|
+
group.push(best);
|
|
77
|
+
addNeighbours(best);
|
|
78
|
+
}
|
|
79
|
+
groups.push(group);
|
|
80
|
+
}
|
|
81
|
+
const result = [];
|
|
82
|
+
let pending = [];
|
|
83
|
+
const minSize = Math.max(2, Math.floor(groupSize / 2));
|
|
84
|
+
for (const group of groups) {
|
|
85
|
+
if (group.length >= minSize) {
|
|
86
|
+
result.push(group);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (pending.length + group.length > groupSize) {
|
|
90
|
+
result.push(pending);
|
|
91
|
+
pending = [];
|
|
92
|
+
}
|
|
93
|
+
pending.push(...group);
|
|
94
|
+
}
|
|
95
|
+
if (pending.length > 0) result.push(pending);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function dist2(a, b) {
|
|
99
|
+
const dx = a[0] - b[0];
|
|
100
|
+
const dy = a[1] - b[1];
|
|
101
|
+
const dz = a[2] - b[2];
|
|
102
|
+
return dx * dx + dy * dy + dz * dz;
|
|
103
|
+
}
|
|
104
|
+
function mortonOrder(points) {
|
|
105
|
+
const min = [Infinity, Infinity, Infinity];
|
|
106
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
107
|
+
for (const p of points) {
|
|
108
|
+
for (let k = 0; k < 3; k++) {
|
|
109
|
+
min[k] = Math.min(min[k], p[k]);
|
|
110
|
+
max[k] = Math.max(max[k], p[k]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const extent = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]) || 1;
|
|
114
|
+
const codes = points.map((p) => {
|
|
115
|
+
const q = (k) => Math.min(1023, Math.max(0, Math.floor((p[k] - min[k]) / extent * 1023)));
|
|
116
|
+
return part1By2(q(0)) | part1By2(q(1)) << 1 | part1By2(q(2)) << 2;
|
|
117
|
+
});
|
|
118
|
+
return points.map((_, i) => i).sort((a, b) => codes[a] - codes[b]);
|
|
119
|
+
}
|
|
120
|
+
function part1By2(x) {
|
|
121
|
+
x &= 1023;
|
|
122
|
+
x = (x | x << 16) & 50331903;
|
|
123
|
+
x = (x | x << 8) & 50393103;
|
|
124
|
+
x = (x | x << 4) & 51130563;
|
|
125
|
+
x = (x | x << 2) & 153391689;
|
|
126
|
+
return x;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/core/preprocess/voxelProxy.ts
|
|
130
|
+
var VOXEL_ERROR_CELLS = 2;
|
|
131
|
+
function buildVoxelProxy(positions, colors, indices, resolution, uvs) {
|
|
132
|
+
if (indices.length === 0) return null;
|
|
133
|
+
const min = [Infinity, Infinity, Infinity];
|
|
134
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
135
|
+
for (let i = 0; i < indices.length; i++) {
|
|
136
|
+
const o = indices[i] * 3;
|
|
137
|
+
for (let k = 0; k < 3; k++) {
|
|
138
|
+
min[k] = Math.min(min[k], positions[o + k]);
|
|
139
|
+
max[k] = Math.max(max[k], positions[o + k]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const extent = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
|
|
143
|
+
if (!(extent > 0)) return null;
|
|
144
|
+
const cell = extent / resolution;
|
|
145
|
+
const PAD = 3;
|
|
146
|
+
const n = [0, 1, 2].map((k) => Math.ceil((max[k] - min[k]) / cell) + 1 + PAD * 2);
|
|
147
|
+
const origin = [0, 1, 2].map((k) => min[k] - PAD * cell);
|
|
148
|
+
const [nx, ny, nz] = n;
|
|
149
|
+
const sx = 1;
|
|
150
|
+
const sy = nx;
|
|
151
|
+
const sz = nx * ny;
|
|
152
|
+
const total = nx * ny * nz;
|
|
153
|
+
const occupied = new Uint8Array(total);
|
|
154
|
+
const colorSum = colors ? new Float32Array(total * 3) : null;
|
|
155
|
+
const colorCount = colors ? new Uint32Array(total) : null;
|
|
156
|
+
const voxelUv = uvs ? new Float32Array(total * 2).fill(NaN) : null;
|
|
157
|
+
let sampleU = 0;
|
|
158
|
+
let sampleV = 0;
|
|
159
|
+
const mark = (x, y, z, r, g, b) => {
|
|
160
|
+
const i = Math.floor((x - origin[0]) / cell) * sx + Math.floor((y - origin[1]) / cell) * sy + Math.floor((z - origin[2]) / cell) * sz;
|
|
161
|
+
occupied[i] = 1;
|
|
162
|
+
if (voxelUv && Number.isNaN(voxelUv[i * 2])) {
|
|
163
|
+
voxelUv[i * 2] = sampleU;
|
|
164
|
+
voxelUv[i * 2 + 1] = sampleV;
|
|
165
|
+
}
|
|
166
|
+
if (colorSum) {
|
|
167
|
+
colorSum[i * 3] += r;
|
|
168
|
+
colorSum[i * 3 + 1] += g;
|
|
169
|
+
colorSum[i * 3 + 2] += b;
|
|
170
|
+
colorCount[i]++;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
174
|
+
const a = indices[t] * 3;
|
|
175
|
+
const b = indices[t + 1] * 3;
|
|
176
|
+
const c = indices[t + 2] * 3;
|
|
177
|
+
const longest = Math.max(dist(positions, a, b), dist(positions, b, c), dist(positions, c, a));
|
|
178
|
+
const steps = Math.max(1, Math.ceil(longest / (cell * 0.5)));
|
|
179
|
+
for (let i = 0; i <= steps; i++) {
|
|
180
|
+
for (let j = 0; j <= steps - i; j++) {
|
|
181
|
+
const u = i / steps;
|
|
182
|
+
const v = j / steps;
|
|
183
|
+
const w = 1 - u - v;
|
|
184
|
+
const x = positions[a] * w + positions[b] * u + positions[c] * v;
|
|
185
|
+
const y = positions[a + 1] * w + positions[b + 1] * u + positions[c + 1] * v;
|
|
186
|
+
const z = positions[a + 2] * w + positions[b + 2] * u + positions[c + 2] * v;
|
|
187
|
+
if (uvs) {
|
|
188
|
+
const ta = indices[t] * 2;
|
|
189
|
+
const tb = indices[t + 1] * 2;
|
|
190
|
+
const tc = indices[t + 2] * 2;
|
|
191
|
+
sampleU = uvs[ta] * w + uvs[tb] * u + uvs[tc] * v;
|
|
192
|
+
sampleV = uvs[ta + 1] * w + uvs[tb + 1] * u + uvs[tc + 1] * v;
|
|
193
|
+
}
|
|
194
|
+
if (colors) {
|
|
195
|
+
mark(
|
|
196
|
+
x,
|
|
197
|
+
y,
|
|
198
|
+
z,
|
|
199
|
+
colors[a] * w + colors[b] * u + colors[c] * v,
|
|
200
|
+
colors[a + 1] * w + colors[b + 1] * u + colors[c + 1] * v,
|
|
201
|
+
colors[a + 2] * w + colors[b + 2] * u + colors[c + 2] * v
|
|
202
|
+
);
|
|
203
|
+
} else mark(x, y, z, 0, 0, 0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const closed = erode(dilate(occupied, n), n);
|
|
208
|
+
const field = new Float32Array(total);
|
|
209
|
+
for (let i = 0; i < total; i++) field[i] = closed[i];
|
|
210
|
+
blurAxis(field, n, sx, nx);
|
|
211
|
+
blurAxis(field, n, sy, ny);
|
|
212
|
+
blurAxis(field, n, sz, nz);
|
|
213
|
+
const ISO = 0.5;
|
|
214
|
+
const cellsX = nx - 1;
|
|
215
|
+
const cellsY = ny - 1;
|
|
216
|
+
const cellsZ = nz - 1;
|
|
217
|
+
const cellVertex = new Int32Array(cellsX * cellsY * cellsZ).fill(-1);
|
|
218
|
+
const outPositions = [];
|
|
219
|
+
const outNormals = [];
|
|
220
|
+
const outColors = [];
|
|
221
|
+
const outUvs = [];
|
|
222
|
+
const corner = new Float32Array(8);
|
|
223
|
+
for (let z = 0; z < cellsZ; z++) {
|
|
224
|
+
for (let y = 0; y < cellsY; y++) {
|
|
225
|
+
for (let x = 0; x < cellsX; x++) {
|
|
226
|
+
const base = x * sx + y * sy + z * sz;
|
|
227
|
+
let inside = 0;
|
|
228
|
+
for (let c = 0; c < 8; c++) {
|
|
229
|
+
corner[c] = field[base + (c & 1) * sx + (c >> 1 & 1) * sy + (c >> 2 & 1) * sz];
|
|
230
|
+
if (corner[c] >= ISO) inside++;
|
|
231
|
+
}
|
|
232
|
+
if (inside === 0 || inside === 8) continue;
|
|
233
|
+
let px = 0;
|
|
234
|
+
let py = 0;
|
|
235
|
+
let pz = 0;
|
|
236
|
+
let crossings = 0;
|
|
237
|
+
for (let c = 0; c < 8; c++) {
|
|
238
|
+
for (const bit of [1, 2, 4]) {
|
|
239
|
+
if (c & bit) continue;
|
|
240
|
+
const d = c | bit;
|
|
241
|
+
const f0 = corner[c];
|
|
242
|
+
const f1 = corner[d];
|
|
243
|
+
if (f0 >= ISO === f1 >= ISO) continue;
|
|
244
|
+
const s = (ISO - f0) / (f1 - f0);
|
|
245
|
+
px += (c & 1) + (bit === 1 ? s : 0);
|
|
246
|
+
py += (c >> 1 & 1) + (bit === 2 ? s : 0);
|
|
247
|
+
pz += (c >> 2 & 1) + (bit === 4 ? s : 0);
|
|
248
|
+
crossings++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
cellVertex[x + y * cellsX + z * cellsX * cellsY] = outPositions.length / 3;
|
|
252
|
+
outPositions.push(origin[0] + (x + 0.5 + px / crossings) * cell, origin[1] + (y + 0.5 + py / crossings) * cell, origin[2] + (z + 0.5 + pz / crossings) * cell);
|
|
253
|
+
const gx = corner[1] + corner[3] + corner[5] + corner[7] - corner[0] - corner[2] - corner[4] - corner[6];
|
|
254
|
+
const gy = corner[2] + corner[3] + corner[6] + corner[7] - corner[0] - corner[1] - corner[4] - corner[5];
|
|
255
|
+
const gz = corner[4] + corner[5] + corner[6] + corner[7] - corner[0] - corner[1] - corner[2] - corner[3];
|
|
256
|
+
const len = Math.hypot(gx, gy, gz) || 1;
|
|
257
|
+
outNormals.push(-gx / len, -gy / len, -gz / len);
|
|
258
|
+
if (colorSum) {
|
|
259
|
+
let r = 0;
|
|
260
|
+
let g = 0;
|
|
261
|
+
let b = 0;
|
|
262
|
+
let count = 0;
|
|
263
|
+
for (let c = 0; c < 8; c++) {
|
|
264
|
+
const i = base + (c & 1) * sx + (c >> 1 & 1) * sy + (c >> 2 & 1) * sz;
|
|
265
|
+
if (!colorCount[i]) continue;
|
|
266
|
+
r += colorSum[i * 3];
|
|
267
|
+
g += colorSum[i * 3 + 1];
|
|
268
|
+
b += colorSum[i * 3 + 2];
|
|
269
|
+
count += colorCount[i];
|
|
270
|
+
}
|
|
271
|
+
if (count) outColors.push(r / count, g / count, b / count);
|
|
272
|
+
else outColors.push(NaN, NaN, NaN);
|
|
273
|
+
}
|
|
274
|
+
if (voxelUv) {
|
|
275
|
+
let u = NaN;
|
|
276
|
+
let v = NaN;
|
|
277
|
+
for (let c = 0; c < 8 && Number.isNaN(u); c++) {
|
|
278
|
+
const i = base + (c & 1) * sx + (c >> 1 & 1) * sy + (c >> 2 & 1) * sz;
|
|
279
|
+
u = voxelUv[i * 2];
|
|
280
|
+
v = voxelUv[i * 2 + 1];
|
|
281
|
+
}
|
|
282
|
+
outUvs.push(u, v);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const outIndices = [];
|
|
288
|
+
const cellIndex = (x, y, z) => cellVertex[x + y * cellsX + z * cellsX * cellsY];
|
|
289
|
+
const quad = (a, b, c, d, nx_, ny_, nz_) => {
|
|
290
|
+
const ux = outPositions[c * 3] - outPositions[a * 3];
|
|
291
|
+
const uy = outPositions[c * 3 + 1] - outPositions[a * 3 + 1];
|
|
292
|
+
const uz = outPositions[c * 3 + 2] - outPositions[a * 3 + 2];
|
|
293
|
+
const vx = outPositions[d * 3] - outPositions[b * 3];
|
|
294
|
+
const vy = outPositions[d * 3 + 1] - outPositions[b * 3 + 1];
|
|
295
|
+
const vz = outPositions[d * 3 + 2] - outPositions[b * 3 + 2];
|
|
296
|
+
const cx = uy * vz - uz * vy;
|
|
297
|
+
const cy = uz * vx - ux * vz;
|
|
298
|
+
const cz = ux * vy - uy * vx;
|
|
299
|
+
if (cx * nx_ + cy * ny_ + cz * nz_ >= 0) outIndices.push(a, b, c, a, c, d);
|
|
300
|
+
else outIndices.push(a, c, b, a, d, c);
|
|
301
|
+
};
|
|
302
|
+
for (let z = 1; z < nz - 1; z++) {
|
|
303
|
+
for (let y = 1; y < ny - 1; y++) {
|
|
304
|
+
for (let x = 1; x < nx - 1; x++) {
|
|
305
|
+
const i = x * sx + y * sy + z * sz;
|
|
306
|
+
const here = field[i] >= ISO;
|
|
307
|
+
if (here !== field[i + sx] >= ISO) {
|
|
308
|
+
const s = here ? 1 : -1;
|
|
309
|
+
quad(cellIndex(x, y - 1, z - 1), cellIndex(x, y, z - 1), cellIndex(x, y, z), cellIndex(x, y - 1, z), s, 0, 0);
|
|
310
|
+
}
|
|
311
|
+
if (here !== field[i + sy] >= ISO) {
|
|
312
|
+
const s = here ? 1 : -1;
|
|
313
|
+
quad(cellIndex(x - 1, y, z - 1), cellIndex(x, y, z - 1), cellIndex(x, y, z), cellIndex(x - 1, y, z), 0, s, 0);
|
|
314
|
+
}
|
|
315
|
+
if (here !== field[i + sz] >= ISO) {
|
|
316
|
+
const s = here ? 1 : -1;
|
|
317
|
+
quad(cellIndex(x - 1, y - 1, z), cellIndex(x, y - 1, z), cellIndex(x, y, z), cellIndex(x - 1, y, z), 0, 0, s);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (outIndices.length === 0) return null;
|
|
323
|
+
let proxyColors = null;
|
|
324
|
+
if (colorSum) {
|
|
325
|
+
proxyColors = Float32Array.from(outColors);
|
|
326
|
+
fillMissing(proxyColors, outIndices, 3);
|
|
327
|
+
}
|
|
328
|
+
let proxyUvs = null;
|
|
329
|
+
if (voxelUv) {
|
|
330
|
+
proxyUvs = Float32Array.from(outUvs);
|
|
331
|
+
fillMissing(proxyUvs, outIndices, 2);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
positions: Float32Array.from(outPositions),
|
|
335
|
+
normals: Float32Array.from(outNormals),
|
|
336
|
+
colors: proxyColors,
|
|
337
|
+
uvs: proxyUvs,
|
|
338
|
+
indices: Uint32Array.from(outIndices),
|
|
339
|
+
cellSize: cell
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function dist(p, a, b) {
|
|
343
|
+
return Math.hypot(p[a] - p[b], p[a + 1] - p[b + 1], p[a + 2] - p[b + 2]);
|
|
344
|
+
}
|
|
345
|
+
function dilate(src, n) {
|
|
346
|
+
let a = src;
|
|
347
|
+
for (let axis = 0; axis < 3; axis++) a = morph(a, n, axis, 1);
|
|
348
|
+
return a;
|
|
349
|
+
}
|
|
350
|
+
function erode(src, n) {
|
|
351
|
+
let a = src;
|
|
352
|
+
for (let axis = 0; axis < 3; axis++) a = morph(a, n, axis, 0);
|
|
353
|
+
return a;
|
|
354
|
+
}
|
|
355
|
+
function morph(src, n, axis, grow) {
|
|
356
|
+
const [nx, ny, nz] = n;
|
|
357
|
+
const stride = axis === 0 ? 1 : axis === 1 ? nx : nx * ny;
|
|
358
|
+
const size = n[axis];
|
|
359
|
+
const out = new Uint8Array(src.length);
|
|
360
|
+
for (let z = 0; z < nz; z++) {
|
|
361
|
+
for (let y = 0; y < ny; y++) {
|
|
362
|
+
for (let x = 0; x < nx; x++) {
|
|
363
|
+
const i = x + y * nx + z * nx * ny;
|
|
364
|
+
const c = axis === 0 ? x : axis === 1 ? y : z;
|
|
365
|
+
const prev = c > 0 ? src[i - stride] : 0;
|
|
366
|
+
const next = c < size - 1 ? src[i + stride] : 0;
|
|
367
|
+
out[i] = grow ? src[i] | prev | next : src[i] & prev & next;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
function blurAxis(field, n, stride, size) {
|
|
374
|
+
const [nx, ny, nz] = n;
|
|
375
|
+
const line = new Float32Array(size);
|
|
376
|
+
const lines = nx * ny * nz / size;
|
|
377
|
+
for (let l = 0; l < lines; l++) {
|
|
378
|
+
let start;
|
|
379
|
+
if (stride === 1) start = l * nx;
|
|
380
|
+
else if (stride === nx) start = l % nx + Math.floor(l / nx) * nx * ny;
|
|
381
|
+
else start = l;
|
|
382
|
+
for (let k = 0; k < size; k++) line[k] = field[start + k * stride];
|
|
383
|
+
for (let k = 0; k < size; k++) {
|
|
384
|
+
const a = k > 0 ? line[k - 1] : 0;
|
|
385
|
+
const b = k < size - 1 ? line[k + 1] : 0;
|
|
386
|
+
field[start + k * stride] = (a + line[k] + b) / 3;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function fillMissing(values, indices, size) {
|
|
391
|
+
const count = values.length / size;
|
|
392
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
393
|
+
let missing = 0;
|
|
394
|
+
const sum = new Float32Array(count * size);
|
|
395
|
+
const hits = new Uint32Array(count);
|
|
396
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
397
|
+
for (let k = 0; k < 3; k++) {
|
|
398
|
+
const v = indices[t + k];
|
|
399
|
+
if (!Number.isNaN(values[v * size])) continue;
|
|
400
|
+
for (let m = 1; m < 3; m++) {
|
|
401
|
+
const u = indices[t + (k + m) % 3];
|
|
402
|
+
if (Number.isNaN(values[u * size])) continue;
|
|
403
|
+
for (let c = 0; c < size; c++) sum[v * size + c] += values[u * size + c];
|
|
404
|
+
hits[v]++;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (let v = 0; v < count; v++) {
|
|
409
|
+
if (!Number.isNaN(values[v * size])) continue;
|
|
410
|
+
if (hits[v]) {
|
|
411
|
+
for (let c = 0; c < size; c++) values[v * size + c] = sum[v * size + c] / hits[v];
|
|
412
|
+
} else missing++;
|
|
413
|
+
}
|
|
414
|
+
if (!missing) return;
|
|
415
|
+
}
|
|
416
|
+
for (let i = 0; i < values.length; i++) if (Number.isNaN(values[i])) values[i] = 0.5;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/core/preprocess/buildVirtualMesh.ts
|
|
420
|
+
var DEFAULTS = {
|
|
421
|
+
groupSize: 12,
|
|
422
|
+
maxLodLevels: 24,
|
|
423
|
+
maxGroupKeepRatio: 0.9,
|
|
424
|
+
simplifyRatio: 0.5,
|
|
425
|
+
minLevelReduction: 0.03,
|
|
426
|
+
prune: false,
|
|
427
|
+
tailSimplify: true,
|
|
428
|
+
minRootTriangles: 4,
|
|
429
|
+
voxelLods: true
|
|
430
|
+
};
|
|
431
|
+
var TAIL_START_TRIANGLES = 1024;
|
|
432
|
+
var VOXEL_RESOLUTIONS = [64, 32, 16, 8, 4];
|
|
433
|
+
var yieldToBrowser = () => new Promise((r) => setTimeout(r, 0));
|
|
434
|
+
async function buildVirtualMesh(src, options = {}) {
|
|
435
|
+
const opts = { ...DEFAULTS, ...options };
|
|
436
|
+
const t0 = performance.now();
|
|
437
|
+
await MeshoptClusterizer.ready;
|
|
438
|
+
await MeshoptSimplifier.ready;
|
|
439
|
+
let positions = src.positions;
|
|
440
|
+
let normals = src.normals;
|
|
441
|
+
let colors = src.colors ?? null;
|
|
442
|
+
let uvs = src.uvs ?? null;
|
|
443
|
+
let weld2 = MeshoptSimplifier.generatePositionRemap(positions, 3);
|
|
444
|
+
let voxelLevels = 0;
|
|
445
|
+
const flags = ["LockBorder", "ErrorAbsolute"];
|
|
446
|
+
if (opts.prune) flags.push("Prune");
|
|
447
|
+
const all = [];
|
|
448
|
+
const proxyCache = /* @__PURE__ */ new Map();
|
|
449
|
+
const leafs = splitIntoMeshlets(src.indices, 0, null, 0);
|
|
450
|
+
let current = leafs;
|
|
451
|
+
let lastTriangles = countTriangles(current);
|
|
452
|
+
let lodLevels = 1;
|
|
453
|
+
const estimatedTotal = Math.max(1, leafs.length * 2);
|
|
454
|
+
for (let level = 1; level <= opts.maxLodLevels && current.length > 1; level++) {
|
|
455
|
+
if (opts.tailSimplify && lastTriangles <= TAIL_START_TRIANGLES) break;
|
|
456
|
+
const groups = partitionMeshlets(current, opts.groupSize);
|
|
457
|
+
const next = [];
|
|
458
|
+
for (const groupIdx of groups) {
|
|
459
|
+
const group = groupIdx.map((i) => current[i]);
|
|
460
|
+
const merged = concatIndices(group);
|
|
461
|
+
const local = compact(merged, positions);
|
|
462
|
+
const target = Math.max(3, Math.floor(merged.length * opts.simplifyRatio / 3) * 3);
|
|
463
|
+
const [simplified, simplifyError] = MeshoptSimplifier.simplify(local.indices, local.positions, 3, target, 1e10, flags);
|
|
464
|
+
if (simplified.length > merged.length * opts.maxGroupKeepRatio) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const lodSphere = enclosingSphere(group.map((m) => m.lodSphere));
|
|
468
|
+
const childError = Math.max(...group.map((m) => m.error));
|
|
469
|
+
const shapeError = Math.max(simplifyError, extentShrink(local.indices, simplified, local.positions));
|
|
470
|
+
const error = childError + Math.max(shapeError, 1e-7);
|
|
471
|
+
for (const child of group) {
|
|
472
|
+
child.parentSphere = lodSphere;
|
|
473
|
+
child.parentError = error;
|
|
474
|
+
}
|
|
475
|
+
const globalIndices = new Uint32Array(simplified.length);
|
|
476
|
+
for (let i = 0; i < simplified.length; i++) globalIndices[i] = local.toGlobal[simplified[i]];
|
|
477
|
+
if (globalIndices.length > 0) {
|
|
478
|
+
const outputs = splitIntoMeshlets(globalIndices, level, lodSphere, error);
|
|
479
|
+
next.push(...outputs);
|
|
480
|
+
for (const child of group) child.replacements = outputs;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
await opts.onProgress?.(Math.min(0.99, all.length / estimatedTotal));
|
|
484
|
+
await yieldToBrowser();
|
|
485
|
+
if (next.length === 0) break;
|
|
486
|
+
lodLevels = level + 1;
|
|
487
|
+
current = next;
|
|
488
|
+
const triangles = countTriangles(current);
|
|
489
|
+
if (triangles > lastTriangles * (1 - opts.minLevelReduction)) break;
|
|
490
|
+
lastTriangles = triangles;
|
|
491
|
+
}
|
|
492
|
+
if (opts.tailSimplify) lodLevels = simplifyTail(all.filter((m) => m.parentError >= ERROR_INFINITY), lodLevels);
|
|
493
|
+
await opts.onProgress?.(1);
|
|
494
|
+
return pack({ positions, normals, colors: colors ?? void 0, uvs: uvs ?? void 0, indices: src.indices }, all, lodLevels, voxelLevels, performance.now() - t0);
|
|
495
|
+
function appendVertices(p, n, c, uv) {
|
|
496
|
+
const first = positions.length / 3;
|
|
497
|
+
const count = p.length / 3;
|
|
498
|
+
const grow = (a, b) => {
|
|
499
|
+
const out = new Float32Array(a.length + b.length);
|
|
500
|
+
out.set(a);
|
|
501
|
+
out.set(b, a.length);
|
|
502
|
+
return out;
|
|
503
|
+
};
|
|
504
|
+
positions = grow(positions, p);
|
|
505
|
+
normals = grow(normals, n);
|
|
506
|
+
if (colors) colors = grow(colors, c ?? new Float32Array(count * 3).fill(1));
|
|
507
|
+
if (uvs) uvs = grow(uvs, uv ?? new Float32Array(count * 2));
|
|
508
|
+
const w = new Uint32Array(weld2.length + count);
|
|
509
|
+
w.set(weld2);
|
|
510
|
+
for (let i = 0; i < count; i++) w[weld2.length + i] = first + i;
|
|
511
|
+
weld2 = w;
|
|
512
|
+
return first;
|
|
513
|
+
}
|
|
514
|
+
function voxelProxy(resolution) {
|
|
515
|
+
if (!proxyCache.has(resolution)) proxyCache.set(resolution, buildVoxelProxy(src.positions, src.colors, src.indices, resolution, src.uvs));
|
|
516
|
+
return proxyCache.get(resolution);
|
|
517
|
+
}
|
|
518
|
+
function simplifyTail(roots, levels) {
|
|
519
|
+
if (roots.length === 0) return levels;
|
|
520
|
+
const tailFlags = ["ErrorAbsolute"];
|
|
521
|
+
if (opts.prune) tailFlags.push("Prune");
|
|
522
|
+
let nodes = roots;
|
|
523
|
+
let level = levels;
|
|
524
|
+
for (; level < levels + opts.maxLodLevels; level++) {
|
|
525
|
+
const merged = concatIndices(nodes);
|
|
526
|
+
if (merged.length / 3 <= opts.minRootTriangles) break;
|
|
527
|
+
const local = compact(merged, positions);
|
|
528
|
+
const target = Math.max(3, Math.floor(merged.length / 2 / 3) * 3);
|
|
529
|
+
const keepLimit = merged.length * opts.maxGroupKeepRatio;
|
|
530
|
+
const childError = Math.max(...nodes.map((m) => m.error));
|
|
531
|
+
let [result, error] = MeshoptSimplifier.simplify(local.indices, local.positions, 3, target, 1e10, tailFlags);
|
|
532
|
+
if (result.length > keepLimit) {
|
|
533
|
+
const [sloppy, relativeError] = MeshoptSimplifier.simplifySloppy(local.indices, local.positions, 3, null, target, 1);
|
|
534
|
+
if (sloppy.length < result.length) {
|
|
535
|
+
result = sloppy;
|
|
536
|
+
error = relativeError * MeshoptSimplifier.getScale(local.positions, 3);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
let globalIndices = null;
|
|
540
|
+
let parentError = Infinity;
|
|
541
|
+
if (result.length <= keepLimit) {
|
|
542
|
+
parentError = childError + Math.max(Math.max(error, extentShrink(local.indices, result, local.positions)), 1e-7);
|
|
543
|
+
globalIndices = new Uint32Array(result.length);
|
|
544
|
+
for (let i = 0; i < result.length; i++) globalIndices[i] = local.toGlobal[result[i]];
|
|
545
|
+
}
|
|
546
|
+
let voxel = null;
|
|
547
|
+
if (opts.voxelLods) {
|
|
548
|
+
for (const resolution of VOXEL_RESOLUTIONS) {
|
|
549
|
+
const proxy = voxelProxy(resolution);
|
|
550
|
+
if (!proxy) continue;
|
|
551
|
+
const proxyTarget = Math.min(target, proxy.indices.length);
|
|
552
|
+
const [simplified, proxyError] = MeshoptSimplifier.simplify(proxy.indices, proxy.positions, 3, proxyTarget, 1e10, ["ErrorAbsolute"]);
|
|
553
|
+
if (simplified.length === 0 || simplified.length > keepLimit) continue;
|
|
554
|
+
const voxelError = Math.max(childError * (1 + 1e-5) + 1e-7, VOXEL_ERROR_CELLS * proxy.cellSize + proxyError);
|
|
555
|
+
if (voxelError < parentError) {
|
|
556
|
+
parentError = voxelError;
|
|
557
|
+
voxel = { proxy, indices: simplified };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (!Number.isFinite(parentError)) break;
|
|
562
|
+
if (voxel) {
|
|
563
|
+
const used = compact(voxel.indices, voxel.proxy.positions);
|
|
564
|
+
const pick = (src2, size = 3) => {
|
|
565
|
+
const out = new Float32Array(used.toGlobal.length * size);
|
|
566
|
+
used.toGlobal.forEach((v, i) => out.set(src2.subarray(v * size, v * size + size), i * size));
|
|
567
|
+
return out;
|
|
568
|
+
};
|
|
569
|
+
const { proxy } = voxel;
|
|
570
|
+
const first = appendVertices(used.positions, pick(proxy.normals), proxy.colors ? pick(proxy.colors) : null, proxy.uvs ? pick(proxy.uvs, 2) : null);
|
|
571
|
+
globalIndices = used.indices.map((i) => i + first);
|
|
572
|
+
voxelLevels++;
|
|
573
|
+
}
|
|
574
|
+
const lodSphere = enclosingSphere(nodes.map((m) => m.lodSphere));
|
|
575
|
+
for (const node of nodes) {
|
|
576
|
+
node.parentSphere = lodSphere;
|
|
577
|
+
node.parentError = parentError;
|
|
578
|
+
}
|
|
579
|
+
if (globalIndices.length === 0) return level + 1;
|
|
580
|
+
const outputs = splitIntoMeshlets(globalIndices, level, lodSphere, parentError);
|
|
581
|
+
for (const node of nodes) node.replacements = outputs;
|
|
582
|
+
nodes = outputs;
|
|
583
|
+
}
|
|
584
|
+
return level;
|
|
585
|
+
}
|
|
586
|
+
function splitIntoMeshlets(indices, lodLevel, lodSphere, error) {
|
|
587
|
+
const local = compact(indices, positions);
|
|
588
|
+
const built = MeshoptClusterizer.buildMeshlets(local.indices, local.positions, 3, MAX_MESHLET_VERTICES, MAX_MESHLET_TRIANGLES, 0);
|
|
589
|
+
const result = [];
|
|
590
|
+
for (let m = 0; m < built.meshletCount; m++) {
|
|
591
|
+
const vertexOffset = built.meshlets[m * 4 + 0];
|
|
592
|
+
const triangleOffset = built.meshlets[m * 4 + 1];
|
|
593
|
+
const vertexCount = built.meshlets[m * 4 + 2];
|
|
594
|
+
const triangleCount = built.meshlets[m * 4 + 3];
|
|
595
|
+
const vertices = new Uint32Array(vertexCount);
|
|
596
|
+
for (let v = 0; v < vertexCount; v++) vertices[v] = local.toGlobal[built.vertices[vertexOffset + v]];
|
|
597
|
+
const localTriangles = built.triangles.slice(triangleOffset, triangleOffset + triangleCount * 3);
|
|
598
|
+
const meshletIndices = new Uint32Array(triangleCount * 3);
|
|
599
|
+
for (let i = 0; i < triangleCount * 3; i++) meshletIndices[i] = vertices[localTriangles[i]];
|
|
600
|
+
const cullSphere = boundingSphere(meshletIndices, positions);
|
|
601
|
+
const meshlet = {
|
|
602
|
+
indices: meshletIndices,
|
|
603
|
+
lodLevel,
|
|
604
|
+
lodSphere: lodSphere ?? cullSphere,
|
|
605
|
+
error,
|
|
606
|
+
parentSphere: [0, 0, 0, 0],
|
|
607
|
+
parentError: ERROR_INFINITY,
|
|
608
|
+
cullSphere,
|
|
609
|
+
boundaryEdges: findBoundaryEdges(meshletIndices, weld2),
|
|
610
|
+
center: [cullSphere[0], cullSphere[1], cullSphere[2]],
|
|
611
|
+
replacements: [],
|
|
612
|
+
vertices,
|
|
613
|
+
localTriangles
|
|
614
|
+
};
|
|
615
|
+
result.push(meshlet);
|
|
616
|
+
all.push(meshlet);
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function pack(src, unsorted, lodLevels, voxelLevels, buildMs) {
|
|
622
|
+
const all = [...unsorted].sort((a, b) => a.lodLevel - b.lodLevel);
|
|
623
|
+
const meshletCount = all.length;
|
|
624
|
+
const indices = new Uint32Array(all.reduce((acc, m) => acc + m.indices.length, 0));
|
|
625
|
+
const meshletBounds = new Float32Array(meshletCount * MESHLET_BOUNDS_STRIDE);
|
|
626
|
+
const meshletInfo = new Uint32Array(meshletCount * MESHLET_INFO_STRIDE);
|
|
627
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
628
|
+
all.forEach((m, i) => indexOf.set(m, i));
|
|
629
|
+
const replacementStart = new Uint32Array(meshletCount + 1);
|
|
630
|
+
const replacementIndices = [];
|
|
631
|
+
all.forEach((m, i) => {
|
|
632
|
+
replacementStart[i] = replacementIndices.length;
|
|
633
|
+
for (const r of m.replacements) replacementIndices.push(indexOf.get(r));
|
|
634
|
+
});
|
|
635
|
+
replacementStart[meshletCount] = replacementIndices.length;
|
|
636
|
+
const meshletVertices = new Uint32Array(all.reduce((acc, m) => acc + m.vertices.length, 0));
|
|
637
|
+
const meshletTriangles = new Uint32Array(indices.length / 3);
|
|
638
|
+
let firstIndex = 0;
|
|
639
|
+
let vertexOffset = 0;
|
|
640
|
+
const stats = { leafTriangles: 0, leafMeshlets: 0, rootTriangles: 0, rootMeshlets: 0, lodLevels, voxelLevels, buildMs };
|
|
641
|
+
all.forEach((m, i) => {
|
|
642
|
+
indices.set(m.indices, firstIndex);
|
|
643
|
+
meshletVertices.set(m.vertices, vertexOffset);
|
|
644
|
+
const t = m.localTriangles;
|
|
645
|
+
for (let k = 0, tri = firstIndex / 3; k < t.length; k += 3, tri++) {
|
|
646
|
+
meshletTriangles[tri] = t[k] | t[k + 1] << 8 | t[k + 2] << 16;
|
|
647
|
+
}
|
|
648
|
+
const b = i * MESHLET_BOUNDS_STRIDE;
|
|
649
|
+
meshletBounds.set(m.lodSphere, b);
|
|
650
|
+
meshletBounds.set(m.parentSphere, b + 4);
|
|
651
|
+
meshletBounds[b + 8] = m.error;
|
|
652
|
+
meshletBounds[b + 9] = m.parentError;
|
|
653
|
+
meshletBounds.set(m.cullSphere, b + 12);
|
|
654
|
+
const triangleCount = m.indices.length / 3;
|
|
655
|
+
const o = i * MESHLET_INFO_STRIDE;
|
|
656
|
+
meshletInfo[o + 0] = triangleCount;
|
|
657
|
+
meshletInfo[o + 1] = firstIndex;
|
|
658
|
+
meshletInfo[o + 2] = m.lodLevel;
|
|
659
|
+
meshletInfo[o + 3] = vertexOffset;
|
|
660
|
+
firstIndex += m.indices.length;
|
|
661
|
+
vertexOffset += m.vertices.length;
|
|
662
|
+
if (m.lodLevel === 0) {
|
|
663
|
+
stats.leafTriangles += triangleCount;
|
|
664
|
+
stats.leafMeshlets += 1;
|
|
665
|
+
}
|
|
666
|
+
if (m.parentError >= ERROR_INFINITY) {
|
|
667
|
+
stats.rootTriangles += triangleCount;
|
|
668
|
+
stats.rootMeshlets += 1;
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
const whole = enclosingSphere(all.filter((m) => m.lodLevel === 0).map((m) => m.cullSphere));
|
|
672
|
+
const levelCount = all.length ? all[all.length - 1].lodLevel + 1 : 0;
|
|
673
|
+
const levelRanges = new Uint32Array(levelCount * 2);
|
|
674
|
+
const levelErrors = new Float32Array(levelCount * 2);
|
|
675
|
+
const levelGuardErrors = new Float32Array(levelCount * 2);
|
|
676
|
+
for (let l = 0; l < levelCount; l++) {
|
|
677
|
+
levelErrors[l * 2] = Infinity;
|
|
678
|
+
levelErrors[l * 2 + 1] = 0;
|
|
679
|
+
levelGuardErrors[l * 2] = 0;
|
|
680
|
+
levelGuardErrors[l * 2 + 1] = Infinity;
|
|
681
|
+
}
|
|
682
|
+
all.forEach((m, i) => {
|
|
683
|
+
const l = m.lodLevel;
|
|
684
|
+
if (levelRanges[l * 2 + 1] === 0) levelRanges[l * 2] = i;
|
|
685
|
+
levelRanges[l * 2 + 1]++;
|
|
686
|
+
levelErrors[l * 2] = Math.min(levelErrors[l * 2], m.error);
|
|
687
|
+
levelErrors[l * 2 + 1] = Math.max(levelErrors[l * 2 + 1], m.parentError);
|
|
688
|
+
levelGuardErrors[l * 2] = Math.max(levelGuardErrors[l * 2], m.error);
|
|
689
|
+
levelGuardErrors[l * 2 + 1] = Math.min(levelGuardErrors[l * 2 + 1], m.parentError);
|
|
690
|
+
});
|
|
691
|
+
const lodBoundsSphere = enclosingSphere([
|
|
692
|
+
...all.map((m) => m.lodSphere),
|
|
693
|
+
...all.filter((m) => m.parentError < ERROR_INFINITY).map((m) => m.parentSphere)
|
|
694
|
+
]);
|
|
695
|
+
const cullBoundsSphere = enclosingSphere(all.map((m) => m.cullSphere));
|
|
696
|
+
return {
|
|
697
|
+
positions: src.positions,
|
|
698
|
+
normals: src.normals,
|
|
699
|
+
colors: src.colors ?? null,
|
|
700
|
+
uvs: src.uvs ?? null,
|
|
701
|
+
indices,
|
|
702
|
+
meshletCount,
|
|
703
|
+
meshletBounds,
|
|
704
|
+
meshletInfo,
|
|
705
|
+
replacementStart,
|
|
706
|
+
replacementIndices: Uint32Array.from(replacementIndices),
|
|
707
|
+
meshletVertices,
|
|
708
|
+
meshletTriangles,
|
|
709
|
+
levelRanges,
|
|
710
|
+
levelErrors,
|
|
711
|
+
levelGuardErrors,
|
|
712
|
+
lodBoundsSphere,
|
|
713
|
+
cullBoundsSphere,
|
|
714
|
+
boundingSphere: whole,
|
|
715
|
+
stats
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function extentShrink(before, after, positions) {
|
|
719
|
+
const box = (indices) => {
|
|
720
|
+
const min = [Infinity, Infinity, Infinity];
|
|
721
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
722
|
+
for (let i = 0; i < indices.length; i++) {
|
|
723
|
+
const o = indices[i] * 3;
|
|
724
|
+
for (let k = 0; k < 3; k++) {
|
|
725
|
+
const v = positions[o + k];
|
|
726
|
+
if (v < min[k]) min[k] = v;
|
|
727
|
+
if (v > max[k]) max[k] = v;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return { min, max };
|
|
731
|
+
};
|
|
732
|
+
const a = box(before);
|
|
733
|
+
if (after.length === 0) return Math.max(a.max[0] - a.min[0], a.max[1] - a.min[1], a.max[2] - a.min[2]) / 2;
|
|
734
|
+
const b = box(after);
|
|
735
|
+
let shrink = 0;
|
|
736
|
+
for (let k = 0; k < 3; k++) shrink = Math.max(shrink, b.min[k] - a.min[k], a.max[k] - b.max[k]);
|
|
737
|
+
return shrink;
|
|
738
|
+
}
|
|
739
|
+
var localOf = new Int32Array(0);
|
|
740
|
+
function compact(indices, positions) {
|
|
741
|
+
const vertexCount = positions.length / 3;
|
|
742
|
+
if (localOf.length < vertexCount) localOf = new Int32Array(vertexCount).fill(-1);
|
|
743
|
+
const toGlobalAll = new Uint32Array(indices.length);
|
|
744
|
+
const localIndices = new Uint32Array(indices.length);
|
|
745
|
+
let count = 0;
|
|
746
|
+
for (let i = 0; i < indices.length; i++) {
|
|
747
|
+
const g = indices[i];
|
|
748
|
+
let l = localOf[g];
|
|
749
|
+
if (l < 0) {
|
|
750
|
+
l = count++;
|
|
751
|
+
localOf[g] = l;
|
|
752
|
+
toGlobalAll[l] = g;
|
|
753
|
+
}
|
|
754
|
+
localIndices[i] = l;
|
|
755
|
+
}
|
|
756
|
+
const toGlobal = toGlobalAll.subarray(0, count);
|
|
757
|
+
const localPositions = new Float32Array(count * 3);
|
|
758
|
+
for (let i = 0; i < count; i++) {
|
|
759
|
+
const g = toGlobal[i];
|
|
760
|
+
localOf[g] = -1;
|
|
761
|
+
localPositions[i * 3 + 0] = positions[g * 3 + 0];
|
|
762
|
+
localPositions[i * 3 + 1] = positions[g * 3 + 1];
|
|
763
|
+
localPositions[i * 3 + 2] = positions[g * 3 + 2];
|
|
764
|
+
}
|
|
765
|
+
return { indices: localIndices, positions: localPositions, toGlobal };
|
|
766
|
+
}
|
|
767
|
+
var edgeTableKeys = new Float64Array(1024);
|
|
768
|
+
var edgeTableCounts = new Uint8Array(1024);
|
|
769
|
+
var edgeOrder = new Int32Array(512);
|
|
770
|
+
function findBoundaryEdges(indices, weld2) {
|
|
771
|
+
const edgeCount = indices.length;
|
|
772
|
+
let size = edgeTableKeys.length;
|
|
773
|
+
if (size < edgeCount * 2) {
|
|
774
|
+
while (size < edgeCount * 2) size <<= 1;
|
|
775
|
+
edgeTableKeys = new Float64Array(size);
|
|
776
|
+
edgeTableCounts = new Uint8Array(size);
|
|
777
|
+
edgeOrder = new Int32Array(size / 2);
|
|
778
|
+
}
|
|
779
|
+
const mask = size - 1;
|
|
780
|
+
let inserted = 0;
|
|
781
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
782
|
+
for (let k = 0; k < 3; k++) {
|
|
783
|
+
const a = weld2[indices[t + k]];
|
|
784
|
+
const b = weld2[indices[t + (k + 1) % 3]];
|
|
785
|
+
const key = edgeKey(a, b);
|
|
786
|
+
let h = (Math.imul(a < b ? a : b, 2654435761) ^ Math.imul(a < b ? b : a, 2246822507)) & mask;
|
|
787
|
+
while (edgeTableCounts[h] !== 0 && edgeTableKeys[h] !== key) h = h + 1 & mask;
|
|
788
|
+
if (edgeTableCounts[h] === 0) {
|
|
789
|
+
edgeTableKeys[h] = key;
|
|
790
|
+
edgeOrder[inserted++] = h;
|
|
791
|
+
}
|
|
792
|
+
if (edgeTableCounts[h] < 255) edgeTableCounts[h]++;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const result = [];
|
|
796
|
+
for (let i = 0; i < inserted; i++) {
|
|
797
|
+
const h = edgeOrder[i];
|
|
798
|
+
if (edgeTableCounts[h] === 1) result.push(edgeTableKeys[h]);
|
|
799
|
+
edgeTableCounts[h] = 0;
|
|
800
|
+
}
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
var edgeKey = (a, b) => a < b ? a * 67108864 + b : b * 67108864 + a;
|
|
804
|
+
function concatIndices(meshlets) {
|
|
805
|
+
const result = new Uint32Array(meshlets.reduce((acc, m) => acc + m.indices.length, 0));
|
|
806
|
+
let offset = 0;
|
|
807
|
+
for (const m of meshlets) {
|
|
808
|
+
result.set(m.indices, offset);
|
|
809
|
+
offset += m.indices.length;
|
|
810
|
+
}
|
|
811
|
+
return result;
|
|
812
|
+
}
|
|
813
|
+
var countTriangles = (meshlets) => meshlets.reduce((acc, m) => acc + m.indices.length / 3, 0);
|
|
814
|
+
function boundingSphere(indices, positions) {
|
|
815
|
+
const min = [Infinity, Infinity, Infinity];
|
|
816
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
817
|
+
for (let i = 0; i < indices.length; i++) {
|
|
818
|
+
const o = indices[i] * 3;
|
|
819
|
+
for (let k = 0; k < 3; k++) {
|
|
820
|
+
const v = positions[o + k];
|
|
821
|
+
if (v < min[k]) min[k] = v;
|
|
822
|
+
if (v > max[k]) max[k] = v;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const cx = (min[0] + max[0]) / 2;
|
|
826
|
+
const cy = (min[1] + max[1]) / 2;
|
|
827
|
+
const cz = (min[2] + max[2]) / 2;
|
|
828
|
+
let r2 = 0;
|
|
829
|
+
for (let i = 0; i < indices.length; i++) {
|
|
830
|
+
const o = indices[i] * 3;
|
|
831
|
+
const dx = positions[o] - cx;
|
|
832
|
+
const dy = positions[o + 1] - cy;
|
|
833
|
+
const dz = positions[o + 2] - cz;
|
|
834
|
+
r2 = Math.max(r2, dx * dx + dy * dy + dz * dz);
|
|
835
|
+
}
|
|
836
|
+
return [cx, cy, cz, Math.sqrt(r2)];
|
|
837
|
+
}
|
|
838
|
+
function enclosingSphere(spheres) {
|
|
839
|
+
if (spheres.length === 1) return [...spheres[0]];
|
|
840
|
+
const centers = new Float32Array(spheres.length * 3);
|
|
841
|
+
const radii = new Float32Array(spheres.length);
|
|
842
|
+
spheres.forEach((s, i) => {
|
|
843
|
+
centers.set([s[0], s[1], s[2]], i * 3);
|
|
844
|
+
radii[i] = s[3];
|
|
845
|
+
});
|
|
846
|
+
const b = MeshoptClusterizer.computeSphereBounds(centers, 3, radii, 1);
|
|
847
|
+
let radius = b.radius;
|
|
848
|
+
for (const s of spheres) {
|
|
849
|
+
const d = Math.hypot(s[0] - b.centerX, s[1] - b.centerY, s[2] - b.centerZ) + s[3];
|
|
850
|
+
radius = Math.max(radius, d);
|
|
851
|
+
}
|
|
852
|
+
return [b.centerX, b.centerY, b.centerZ, radius * 1.0001];
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/core/io/serialize.ts
|
|
856
|
+
import { MeshoptDecoder, MeshoptEncoder } from "meshoptimizer";
|
|
857
|
+
var VG_FORMAT_VERSION = 1;
|
|
858
|
+
var MAGIC = 1329940310;
|
|
859
|
+
var PREFIX_BYTES = 16;
|
|
860
|
+
var TYPED_ARRAYS = {
|
|
861
|
+
Int8Array,
|
|
862
|
+
Uint8Array,
|
|
863
|
+
Uint8ClampedArray,
|
|
864
|
+
Int16Array,
|
|
865
|
+
Uint16Array,
|
|
866
|
+
Int32Array,
|
|
867
|
+
Uint32Array,
|
|
868
|
+
Float32Array,
|
|
869
|
+
Float64Array
|
|
870
|
+
};
|
|
871
|
+
var FIELD_HINTS = {
|
|
872
|
+
positions: { strides: [12] },
|
|
873
|
+
normals: { strides: [12] },
|
|
874
|
+
colors: { strides: [12] },
|
|
875
|
+
uvs: { strides: [8] },
|
|
876
|
+
tangents: { strides: [16] },
|
|
877
|
+
meshletBounds: { strides: [64], deflateRaw: true },
|
|
878
|
+
meshletInfo: { strides: [16] }
|
|
879
|
+
};
|
|
880
|
+
var VirtualMeshFormatError = class extends Error {
|
|
881
|
+
constructor(message) {
|
|
882
|
+
super(`three-virtual-geometry: ${message}`);
|
|
883
|
+
this.name = "VirtualMeshFormatError";
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
async function encodeVirtualMesh(data, options = {}) {
|
|
887
|
+
const compress = options.compress ?? true;
|
|
888
|
+
if (compress) await MeshoptEncoder.ready;
|
|
889
|
+
const record = data;
|
|
890
|
+
const keys = Object.keys(record).filter((k) => record[k] !== void 0 && typeof record[k] !== "function");
|
|
891
|
+
const fields = [];
|
|
892
|
+
const blobs = [];
|
|
893
|
+
const values = {};
|
|
894
|
+
let offset = 0;
|
|
895
|
+
for (const name of keys) {
|
|
896
|
+
const value = record[name];
|
|
897
|
+
if (!ArrayBuffer.isView(value)) {
|
|
898
|
+
values[name] = value;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const type = value.constructor.name;
|
|
902
|
+
if (!(type in TYPED_ARRAYS)) throw new VirtualMeshFormatError(`cannot serialize field "${name}" of type ${type}`);
|
|
903
|
+
const array = value;
|
|
904
|
+
let blob;
|
|
905
|
+
let entry;
|
|
906
|
+
if (name === "indices" && !options.keepIndices && indicesMatchMeshlets(data)) {
|
|
907
|
+
entry = { name, type, length: array.length, encoding: "meshlet-indices" };
|
|
908
|
+
blob = new Uint8Array(0);
|
|
909
|
+
} else if (compress) {
|
|
910
|
+
({ entry, blob } = await encodeField(name, type, array, data, options.deflate ?? true));
|
|
911
|
+
} else {
|
|
912
|
+
entry = { name, type, length: array.length, encoding: "raw" };
|
|
913
|
+
blob = bytesOf(array);
|
|
914
|
+
}
|
|
915
|
+
fields.push({ ...entry, offset, byteLength: blob.length });
|
|
916
|
+
blobs.push(blob);
|
|
917
|
+
offset += align4(blob.length);
|
|
918
|
+
}
|
|
919
|
+
const headerJson = { generator: "three-virtual-geometry", keys, fields, values };
|
|
920
|
+
const headerBytes = new TextEncoder().encode(JSON.stringify(headerJson, jsonReplacer));
|
|
921
|
+
const headerPadded = align4(headerBytes.length);
|
|
922
|
+
const out = new Uint8Array(PREFIX_BYTES + headerPadded + offset);
|
|
923
|
+
out.set(headerBytes, PREFIX_BYTES);
|
|
924
|
+
out.fill(32, PREFIX_BYTES + headerBytes.length, PREFIX_BYTES + headerPadded);
|
|
925
|
+
const payloadStart = PREFIX_BYTES + headerPadded;
|
|
926
|
+
fields.forEach((f, i) => out.set(blobs[i], payloadStart + f.offset));
|
|
927
|
+
const view = new DataView(out.buffer);
|
|
928
|
+
view.setUint32(0, MAGIC, true);
|
|
929
|
+
view.setUint32(4, VG_FORMAT_VERSION, true);
|
|
930
|
+
view.setUint32(8, headerBytes.length, true);
|
|
931
|
+
view.setUint32(12, checksum(out, PREFIX_BYTES), true);
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
async function encodeField(name, type, array, data, useDeflate) {
|
|
935
|
+
const raw = bytesOf(array);
|
|
936
|
+
const base = { name, type, length: array.length };
|
|
937
|
+
const hint = FIELD_HINTS[name] ?? {};
|
|
938
|
+
const rawCandidate = { entry: { ...base, encoding: "raw" }, blob: raw };
|
|
939
|
+
const candidates = [rawCandidate];
|
|
940
|
+
const triangles = name === "meshletTriangles" && array instanceof Uint32Array ? encodeMeshletTriangles(array, data.meshletInfo) : null;
|
|
941
|
+
if (triangles) candidates.push({ entry: { ...base, encoding: "meshlet-triangles" }, blob: triangles });
|
|
942
|
+
else if (raw.length % 4 === 0 && raw.length > 0) {
|
|
943
|
+
const strides = (hint.strides ?? [Math.max(4, array.BYTES_PER_ELEMENT)]).filter((s) => s % 4 === 0 && s <= 256 && raw.length % s === 0);
|
|
944
|
+
for (const stride of strides) {
|
|
945
|
+
const blob = MeshoptEncoder.encodeVertexBufferLevel(raw, raw.length / stride, stride, 2, 1);
|
|
946
|
+
candidates.push({ entry: { ...base, encoding: "meshopt-vertex", stride }, blob });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
candidates.sort((a, b) => a.blob.length - b.blob.length);
|
|
950
|
+
if (useDeflate && canDeflate() && raw.length >= 64) {
|
|
951
|
+
const toDeflate = hint.deflateRaw && candidates[0] !== rawCandidate ? [candidates[0], rawCandidate] : [candidates[0]];
|
|
952
|
+
for (const c of toDeflate) candidates.push({ entry: { ...c.entry, deflate: true }, blob: await deflate(c.blob) });
|
|
953
|
+
candidates.sort((a, b) => a.blob.length - b.blob.length);
|
|
954
|
+
}
|
|
955
|
+
for (const c of candidates) {
|
|
956
|
+
const decoded = await decodeField({ ...c.entry, offset: 0, byteLength: c.blob.length }, c.blob, { meshletInfo: data.meshletInfo });
|
|
957
|
+
if (sameBytes(bytesOf(decoded), raw)) return c;
|
|
958
|
+
}
|
|
959
|
+
return rawCandidate;
|
|
960
|
+
}
|
|
961
|
+
function encodeMeshletTriangles(triangles, meshletInfo) {
|
|
962
|
+
if (!(meshletInfo instanceof Uint32Array)) return null;
|
|
963
|
+
const slots = new Uint32Array(triangles.length * 3);
|
|
964
|
+
const covered = new Uint8Array(triangles.length);
|
|
965
|
+
for (let m = 0; m < meshletInfo.length; m += MESHLET_INFO_STRIDE) {
|
|
966
|
+
const count = meshletInfo[m];
|
|
967
|
+
const first = meshletInfo[m + 1] / 3;
|
|
968
|
+
const vertexOffset = meshletInfo[m + 3];
|
|
969
|
+
if (first + count > triangles.length) return null;
|
|
970
|
+
for (let t = first; t < first + count; t++) {
|
|
971
|
+
const packed = triangles[t];
|
|
972
|
+
if (covered[t] || packed >>> 24 !== 0) return null;
|
|
973
|
+
covered[t] = 1;
|
|
974
|
+
slots[t * 3] = vertexOffset + (packed & 255);
|
|
975
|
+
slots[t * 3 + 1] = vertexOffset + (packed >> 8 & 255);
|
|
976
|
+
slots[t * 3 + 2] = vertexOffset + (packed >> 16 & 255);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (!covered.every((c) => c === 1)) return null;
|
|
980
|
+
const codec = MeshoptEncoder.encodeIndexBuffer(bytesOf(slots), slots.length, 4);
|
|
981
|
+
const decoded = new Uint32Array(slots.length);
|
|
982
|
+
MeshoptDecoder.decodeIndexBuffer(bytesOf(decoded), slots.length, 4, codec);
|
|
983
|
+
const rotations = new Uint8Array(Math.ceil(triangles.length / 4));
|
|
984
|
+
for (let t = 0; t < triangles.length; t++) {
|
|
985
|
+
const a = slots[t * 3], b = slots[t * 3 + 1], c = slots[t * 3 + 2];
|
|
986
|
+
const x = decoded[t * 3], y = decoded[t * 3 + 1], z = decoded[t * 3 + 2];
|
|
987
|
+
let r;
|
|
988
|
+
if (a === x && b === y && c === z) r = 0;
|
|
989
|
+
else if (a === y && b === z && c === x) r = 1;
|
|
990
|
+
else if (a === z && b === x && c === y) r = 2;
|
|
991
|
+
else return null;
|
|
992
|
+
rotations[t >> 2] |= r << (t & 3) * 2;
|
|
993
|
+
}
|
|
994
|
+
const out = new Uint8Array(4 + align4(codec.length) + rotations.length);
|
|
995
|
+
new DataView(out.buffer).setUint32(0, codec.length, true);
|
|
996
|
+
out.set(codec, 4);
|
|
997
|
+
out.set(rotations, 4 + align4(codec.length));
|
|
998
|
+
return out;
|
|
999
|
+
}
|
|
1000
|
+
function indicesMatchMeshlets(data) {
|
|
1001
|
+
const { indices, meshletInfo, meshletVertices, meshletTriangles } = data;
|
|
1002
|
+
if (!(indices instanceof Uint32Array && meshletInfo instanceof Uint32Array && meshletVertices instanceof Uint32Array && meshletTriangles instanceof Uint32Array)) {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
return sameBytes(bytesOf(rebuildIndices(indices.length, meshletInfo, meshletVertices, meshletTriangles)), bytesOf(indices));
|
|
1007
|
+
} catch {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function decodeField(field, blob, arrays) {
|
|
1012
|
+
const Ctor = TYPED_ARRAYS[field.type];
|
|
1013
|
+
const out = new Ctor(field.length);
|
|
1014
|
+
const target = bytesOf(out);
|
|
1015
|
+
const src = field.deflate ? await inflate(blob) : blob;
|
|
1016
|
+
switch (field.encoding) {
|
|
1017
|
+
case "raw":
|
|
1018
|
+
if (src.length !== target.length) throw new VirtualMeshFormatError(`geometry file is corrupt (field "${field.name}" has the wrong size)`);
|
|
1019
|
+
target.set(src);
|
|
1020
|
+
break;
|
|
1021
|
+
case "meshopt-vertex": {
|
|
1022
|
+
const stride = field.stride ?? 0;
|
|
1023
|
+
if (stride <= 0 || stride % 4 !== 0 || target.length % stride !== 0) throw new VirtualMeshFormatError(`geometry file is corrupt (field "${field.name}" stride)`);
|
|
1024
|
+
MeshoptDecoder.decodeVertexBuffer(target, target.length / stride, stride, src);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
case "meshlet-triangles":
|
|
1028
|
+
decodeMeshletTriangles(out, src, requireArray(arrays, "meshletInfo", field.name));
|
|
1029
|
+
break;
|
|
1030
|
+
case "meshlet-indices":
|
|
1031
|
+
out.set(
|
|
1032
|
+
rebuildIndices(
|
|
1033
|
+
field.length,
|
|
1034
|
+
requireArray(arrays, "meshletInfo", field.name),
|
|
1035
|
+
requireArray(arrays, "meshletVertices", field.name),
|
|
1036
|
+
requireArray(arrays, "meshletTriangles", field.name)
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
break;
|
|
1040
|
+
default:
|
|
1041
|
+
throw new VirtualMeshFormatError(`unknown encoding "${field.encoding}" for field "${field.name}"`);
|
|
1042
|
+
}
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
function requireArray(arrays, name, forField) {
|
|
1046
|
+
const a = arrays[name];
|
|
1047
|
+
if (!(a instanceof Uint32Array)) throw new VirtualMeshFormatError(`geometry file is corrupt ("${forField}" needs "${name}")`);
|
|
1048
|
+
return a;
|
|
1049
|
+
}
|
|
1050
|
+
function decodeMeshletTriangles(out, blob, meshletInfo) {
|
|
1051
|
+
const codecLength = new DataView(blob.buffer, blob.byteOffset, blob.byteLength).getUint32(0, true);
|
|
1052
|
+
const rotationsStart = 4 + align4(codecLength);
|
|
1053
|
+
if (rotationsStart + Math.ceil(out.length / 4) > blob.length) throw new Error("truncated triangle data");
|
|
1054
|
+
const slots = new Uint32Array(out.length * 3);
|
|
1055
|
+
MeshoptDecoder.decodeIndexBuffer(bytesOf(slots), slots.length, 4, blob.subarray(4, 4 + codecLength));
|
|
1056
|
+
const rotations = blob.subarray(rotationsStart);
|
|
1057
|
+
for (let m = 0; m < meshletInfo.length; m += MESHLET_INFO_STRIDE) {
|
|
1058
|
+
const count = meshletInfo[m];
|
|
1059
|
+
const first = meshletInfo[m + 1] / 3;
|
|
1060
|
+
const vertexOffset = meshletInfo[m + 3];
|
|
1061
|
+
if (first + count > out.length) throw new Error("meshlet range out of bounds");
|
|
1062
|
+
for (let t = first; t < first + count; t++) {
|
|
1063
|
+
const r = rotations[t >> 2] >> (t & 3) * 2 & 3;
|
|
1064
|
+
const x = slots[t * 3] - vertexOffset, y = slots[t * 3 + 1] - vertexOffset, z = slots[t * 3 + 2] - vertexOffset;
|
|
1065
|
+
const [a, b, c] = r === 0 ? [x, y, z] : r === 1 ? [y, z, x] : [z, x, y];
|
|
1066
|
+
if ((a | b | c) >>> 8 !== 0) throw new Error("meshlet-local vertex id out of range");
|
|
1067
|
+
out[t] = a | b << 8 | c << 16;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function rebuildIndices(length, meshletInfo, meshletVertices, meshletTriangles) {
|
|
1072
|
+
const indices = new Uint32Array(length);
|
|
1073
|
+
for (let m = 0; m < meshletInfo.length; m += MESHLET_INFO_STRIDE) {
|
|
1074
|
+
const count = meshletInfo[m];
|
|
1075
|
+
const first = meshletInfo[m + 1];
|
|
1076
|
+
const vertexOffset = meshletInfo[m + 3];
|
|
1077
|
+
if (first + count * 3 > length || first / 3 + count > meshletTriangles.length) throw new VirtualMeshFormatError("geometry file is corrupt (meshlet range out of bounds)");
|
|
1078
|
+
for (let t = 0; t < count; t++) {
|
|
1079
|
+
const packed = meshletTriangles[first / 3 + t];
|
|
1080
|
+
for (let k = 0; k < 3; k++) {
|
|
1081
|
+
const v = vertexOffset + (packed >> 8 * k & 255);
|
|
1082
|
+
if (v >= meshletVertices.length) throw new VirtualMeshFormatError("geometry file is corrupt (meshlet vertex out of bounds)");
|
|
1083
|
+
indices[first + t * 3 + k] = meshletVertices[v];
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return indices;
|
|
1088
|
+
}
|
|
1089
|
+
var align4 = (n) => n + 3 & ~3;
|
|
1090
|
+
function bytesOf(a) {
|
|
1091
|
+
return new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
|
|
1092
|
+
}
|
|
1093
|
+
function sameBytes(a, b) {
|
|
1094
|
+
if (a.length !== b.length) return false;
|
|
1095
|
+
if (a.byteOffset % 4 === 0 && b.byteOffset % 4 === 0 && a.length % 4 === 0) {
|
|
1096
|
+
const x = new Uint32Array(a.buffer, a.byteOffset, a.length / 4);
|
|
1097
|
+
const y = new Uint32Array(b.buffer, b.byteOffset, b.length / 4);
|
|
1098
|
+
for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
function checksum(bytes, start) {
|
|
1105
|
+
const words = new Uint32Array(bytes.buffer, bytes.byteOffset + start, bytes.length - start >> 2);
|
|
1106
|
+
let h = 2166136261 ^ words.length;
|
|
1107
|
+
for (let i = 0; i < words.length; i++) {
|
|
1108
|
+
h = Math.imul(h ^ words[i], 16777619);
|
|
1109
|
+
h ^= h >>> 15;
|
|
1110
|
+
}
|
|
1111
|
+
return h >>> 0;
|
|
1112
|
+
}
|
|
1113
|
+
var canDeflate = () => typeof CompressionStream !== "undefined" && typeof DecompressionStream !== "undefined";
|
|
1114
|
+
async function deflate(bytes) {
|
|
1115
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
|
|
1116
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
1117
|
+
}
|
|
1118
|
+
async function inflate(bytes) {
|
|
1119
|
+
if (typeof DecompressionStream === "undefined") throw new VirtualMeshFormatError("this environment cannot decompress the geometry file (no DecompressionStream)");
|
|
1120
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
|
|
1121
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
1122
|
+
}
|
|
1123
|
+
function jsonReplacer(key, value) {
|
|
1124
|
+
if (typeof value === "number" && !Number.isFinite(value)) return { $number: String(value) };
|
|
1125
|
+
if (ArrayBuffer.isView(value)) throw new VirtualMeshFormatError(`typed array nested inside field "${key}" cannot be serialized; store it as a top-level field`);
|
|
1126
|
+
return value;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/source.ts
|
|
1130
|
+
import * as THREE from "three/webgpu";
|
|
1131
|
+
var POSITION_TOLERANCE = 1e-6;
|
|
1132
|
+
var ATTRIBUTE_TOLERANCE = 1e-4;
|
|
1133
|
+
function fromBufferGeometry(geometry, options = {}) {
|
|
1134
|
+
const position = geometry.attributes.position;
|
|
1135
|
+
const uv = options.uvs ?? true ? geometry.attributes.uv : void 0;
|
|
1136
|
+
const color = options.colors ? geometry.attributes.color : void 0;
|
|
1137
|
+
const normal = options.normals === "keep" ? geometry.attributes.normal : void 0;
|
|
1138
|
+
const index = geometry.index;
|
|
1139
|
+
const total = index ? index.count : position.count;
|
|
1140
|
+
const start = Math.max(0, Math.min(total, options.range?.start ?? 0));
|
|
1141
|
+
const count = Math.floor((Math.min(total, start + (options.range?.count ?? total)) - start) / 3) * 3;
|
|
1142
|
+
const vertexOf = (corner) => index ? index.getX(start + corner) : start + corner;
|
|
1143
|
+
const byPosition = weld(count, vertexOf, [{ attribute: position, size: 3, tolerance: POSITION_TOLERANCE }]);
|
|
1144
|
+
const positions = gather(byPosition.firstVertex, position, 3);
|
|
1145
|
+
let smoothNormals = null;
|
|
1146
|
+
if (!normal) {
|
|
1147
|
+
const g = new THREE.BufferGeometry();
|
|
1148
|
+
g.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
1149
|
+
g.setIndex(new THREE.BufferAttribute(byPosition.indices, 1));
|
|
1150
|
+
g.computeVertexNormals();
|
|
1151
|
+
smoothNormals = g.attributes.normal.array;
|
|
1152
|
+
}
|
|
1153
|
+
if (!uv && !color && !normal) return { positions, normals: smoothNormals, indices: byPosition.indices };
|
|
1154
|
+
const channels = [{ attribute: position, size: 3, tolerance: POSITION_TOLERANCE }];
|
|
1155
|
+
if (uv) channels.push({ attribute: uv, size: 2, tolerance: POSITION_TOLERANCE });
|
|
1156
|
+
if (normal) channels.push({ attribute: normal, size: 3, tolerance: ATTRIBUTE_TOLERANCE });
|
|
1157
|
+
if (color) channels.push({ attribute: color, size: 3, tolerance: ATTRIBUTE_TOLERANCE });
|
|
1158
|
+
const full = weld(count, vertexOf, channels);
|
|
1159
|
+
const result = { positions: gather(full.firstVertex, position, 3), normals: new Float32Array(0), indices: full.indices };
|
|
1160
|
+
if (normal) {
|
|
1161
|
+
result.normals = gather(full.firstVertex, normal, 3);
|
|
1162
|
+
for (let i = 0; i < result.normals.length; i += 3) {
|
|
1163
|
+
const l = Math.hypot(result.normals[i], result.normals[i + 1], result.normals[i + 2]) || 1;
|
|
1164
|
+
result.normals[i] /= l;
|
|
1165
|
+
result.normals[i + 1] /= l;
|
|
1166
|
+
result.normals[i + 2] /= l;
|
|
1167
|
+
}
|
|
1168
|
+
} else {
|
|
1169
|
+
result.normals = new Float32Array(full.firstVertex.length * 3);
|
|
1170
|
+
for (let w = 0; w < full.firstVertex.length; w++) {
|
|
1171
|
+
const p = byPosition.indices[full.firstCorner[w]];
|
|
1172
|
+
result.normals.set(smoothNormals.subarray(p * 3, p * 3 + 3), w * 3);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (uv) result.uvs = gather(full.firstVertex, uv, 2);
|
|
1176
|
+
if (color) result.colors = gather(full.firstVertex, color, 3);
|
|
1177
|
+
return result;
|
|
1178
|
+
}
|
|
1179
|
+
function gather(vertices, attribute, size) {
|
|
1180
|
+
const out = new Float32Array(vertices.length * size);
|
|
1181
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
1182
|
+
for (let k = 0; k < size; k++) out[i * size + k] = attribute.getComponent(vertices[i], k);
|
|
1183
|
+
}
|
|
1184
|
+
return out;
|
|
1185
|
+
}
|
|
1186
|
+
function weld(count, vertexOf, channels) {
|
|
1187
|
+
const width = channels.reduce((a, c) => a + c.size, 0);
|
|
1188
|
+
const quantize = channels.map(({ tolerance }) => {
|
|
1189
|
+
const multiplier = Math.pow(10, Math.log10(1 / tolerance));
|
|
1190
|
+
return { multiplier, additive: tolerance * 0.5 * multiplier };
|
|
1191
|
+
});
|
|
1192
|
+
let size = 1;
|
|
1193
|
+
while (size < count * 2) size <<= 1;
|
|
1194
|
+
const mask = size - 1;
|
|
1195
|
+
const table = new Int32Array(size).fill(-1);
|
|
1196
|
+
const keys = new Float64Array(count * width);
|
|
1197
|
+
const key = new Float64Array(width);
|
|
1198
|
+
const firstVertex = new Uint32Array(count);
|
|
1199
|
+
const firstCorner = new Uint32Array(count);
|
|
1200
|
+
const indices = new Uint32Array(count);
|
|
1201
|
+
let welded = 0;
|
|
1202
|
+
for (let i = 0; i < count; i++) {
|
|
1203
|
+
const v = vertexOf(i);
|
|
1204
|
+
let h = 0;
|
|
1205
|
+
let o = 0;
|
|
1206
|
+
for (let c = 0; c < channels.length; c++) {
|
|
1207
|
+
const { attribute, size: n } = channels[c];
|
|
1208
|
+
const { multiplier, additive } = quantize[c];
|
|
1209
|
+
for (let k = 0; k < n; k++, o++) {
|
|
1210
|
+
key[o] = Math.trunc(attribute.getComponent(v, k) * multiplier + additive);
|
|
1211
|
+
h = h ^ Math.imul(key[o] | 0, HASH_PRIMES[o % HASH_PRIMES.length]) | 0;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
h &= mask;
|
|
1215
|
+
for (; ; ) {
|
|
1216
|
+
const w = table[h];
|
|
1217
|
+
if (w < 0) {
|
|
1218
|
+
table[h] = welded;
|
|
1219
|
+
keys.set(key, welded * width);
|
|
1220
|
+
firstVertex[welded] = v;
|
|
1221
|
+
firstCorner[welded] = i;
|
|
1222
|
+
indices[i] = welded++;
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
let same = true;
|
|
1226
|
+
for (let k = 0; k < width && same; k++) same = keys[w * width + k] === key[k];
|
|
1227
|
+
if (same) {
|
|
1228
|
+
indices[i] = w;
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
h = h + 1 & mask;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return { indices, firstVertex: firstVertex.slice(0, welded), firstCorner: firstCorner.slice(0, welded) };
|
|
1235
|
+
}
|
|
1236
|
+
var HASH_PRIMES = [73856093, 19349663, 83492791, 2654435761, 2246822519, 3266489917, 668265263, 374761393, 1181783497, 3735928559, 2028178513];
|
|
1237
|
+
|
|
1238
|
+
// scripts/bake.ts
|
|
1239
|
+
var USAGE = "usage: npx vg-bake <model.glb|model.gltf|model.obj> [-o outDir] [--prune] [--no-voxel] [--no-deflate]";
|
|
1240
|
+
function parseArgs(argv) {
|
|
1241
|
+
const args = { input: "", outDir: "", build: {}, deflate: true };
|
|
1242
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1243
|
+
const a = argv[i];
|
|
1244
|
+
if (a === "-o" || a === "--out") args.outDir = argv[++i] ?? "";
|
|
1245
|
+
else if (a === "--prune") args.build.prune = true;
|
|
1246
|
+
else if (a === "--no-voxel") args.build.voxelLods = false;
|
|
1247
|
+
else if (a === "--no-deflate") args.deflate = false;
|
|
1248
|
+
else if (a === "-h" || a === "--help") {
|
|
1249
|
+
console.log(USAGE);
|
|
1250
|
+
process.exit(0);
|
|
1251
|
+
} else if (a.startsWith("-")) throw new Error(`unknown option ${a}
|
|
1252
|
+
${USAGE}`);
|
|
1253
|
+
else args.input = a;
|
|
1254
|
+
}
|
|
1255
|
+
if (!args.input) throw new Error(USAGE);
|
|
1256
|
+
args.outDir ||= `${basename(args.input, extname(args.input))}.vg`;
|
|
1257
|
+
return args;
|
|
1258
|
+
}
|
|
1259
|
+
function serveFileUrls() {
|
|
1260
|
+
const g = globalThis;
|
|
1261
|
+
g.ProgressEvent ??= class extends Event {
|
|
1262
|
+
constructor(type, init = {}) {
|
|
1263
|
+
super(type);
|
|
1264
|
+
Object.assign(this, init);
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
const nodeFetch = globalThis.fetch;
|
|
1268
|
+
globalThis.fetch = (async (input, init) => {
|
|
1269
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
1270
|
+
if (!url.startsWith("file:")) return nodeFetch(input, init);
|
|
1271
|
+
return new Response(readFileSync(fileURLToPath(url)));
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
async function load(path) {
|
|
1275
|
+
const ext = extname(path).toLowerCase();
|
|
1276
|
+
if (ext === ".obj") return new OBJLoader().parse(readFileSync(path, "utf8"));
|
|
1277
|
+
if (ext !== ".glb" && ext !== ".gltf") throw new Error(`unsupported file type ${ext} (expected .glb, .gltf or .obj)`);
|
|
1278
|
+
serveFileUrls();
|
|
1279
|
+
const loader = new GLTFLoader();
|
|
1280
|
+
loader.setMeshoptDecoder(MeshoptDecoder2);
|
|
1281
|
+
loader.register(
|
|
1282
|
+
(parser) => ({
|
|
1283
|
+
name: "three-virtual-geometry-skip-textures",
|
|
1284
|
+
beforeRoot: () => {
|
|
1285
|
+
for (const material of parser.json.materials ?? []) stripTextures(material);
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
})
|
|
1289
|
+
);
|
|
1290
|
+
const file = readFileSync(path);
|
|
1291
|
+
const data = ext === ".gltf" ? file.toString("utf8") : file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
1292
|
+
const resourcePath = pathToFileURL(dirname(resolve(path))).href + "/";
|
|
1293
|
+
const gltf = await new Promise((ok, fail) => loader.parse(data, resourcePath, ok, fail));
|
|
1294
|
+
return gltf.scene;
|
|
1295
|
+
}
|
|
1296
|
+
function stripTextures(node) {
|
|
1297
|
+
const record = node;
|
|
1298
|
+
for (const key of Object.keys(record)) {
|
|
1299
|
+
if (key.endsWith("Texture")) delete record[key];
|
|
1300
|
+
else if (record[key] && typeof record[key] === "object") stripTextures(record[key]);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function materialInfo(material) {
|
|
1304
|
+
const m = Array.isArray(material) ? material[0] : material;
|
|
1305
|
+
if (!m) return null;
|
|
1306
|
+
return {
|
|
1307
|
+
name: m.name || void 0,
|
|
1308
|
+
color: m.color ? `#${m.color.getHexString()}` : void 0,
|
|
1309
|
+
roughness: m.roughness === void 0 ? void 0 : Number(m.roughness.toFixed(4)),
|
|
1310
|
+
metalness: m.metalness === void 0 ? void 0 : Number(m.metalness.toFixed(4)),
|
|
1311
|
+
doubleSided: m.side === THREE2.DoubleSide || void 0
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
var slug = (s) => s.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48) || "mesh";
|
|
1315
|
+
async function main() {
|
|
1316
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1317
|
+
const t0 = performance.now();
|
|
1318
|
+
const root = await load(args.input);
|
|
1319
|
+
root.updateMatrixWorld(true);
|
|
1320
|
+
const byGeometry = /* @__PURE__ */ new Map();
|
|
1321
|
+
root.traverse((o) => {
|
|
1322
|
+
const mesh = o;
|
|
1323
|
+
if (!mesh.isMesh || !mesh.geometry?.attributes.position) return;
|
|
1324
|
+
if (mesh.isPoints || mesh.isLine) return;
|
|
1325
|
+
let entry = byGeometry.get(mesh.geometry);
|
|
1326
|
+
if (!entry) byGeometry.set(mesh.geometry, entry = { name: mesh.name || mesh.geometry.name, material: mesh.material, instances: [] });
|
|
1327
|
+
const instanced = mesh;
|
|
1328
|
+
if (instanced.isInstancedMesh) {
|
|
1329
|
+
const m = new THREE2.Matrix4();
|
|
1330
|
+
for (let i = 0; i < instanced.count; i++) {
|
|
1331
|
+
instanced.getMatrixAt(i, m);
|
|
1332
|
+
entry.instances.push(m.premultiply(mesh.matrixWorld).toArray());
|
|
1333
|
+
}
|
|
1334
|
+
} else {
|
|
1335
|
+
entry.instances.push(mesh.matrixWorld.toArray());
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
if (byGeometry.size === 0) throw new Error(`no triangle meshes found in ${args.input}`);
|
|
1339
|
+
console.log(`${args.input}: ${byGeometry.size} unique geometries (loaded in ${(performance.now() - t0).toFixed(0)} ms)`);
|
|
1340
|
+
mkdirSync(args.outDir, { recursive: true });
|
|
1341
|
+
const meshes = [];
|
|
1342
|
+
let index = 0;
|
|
1343
|
+
for (const [geometry, entry] of byGeometry) {
|
|
1344
|
+
const file = `${index++}-${slug(entry.name)}.vgeo`;
|
|
1345
|
+
const source = fromBufferGeometry(geometry);
|
|
1346
|
+
if (source.indices.length < 3) {
|
|
1347
|
+
console.log(` ${file}: skipped (no triangles)`);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const data = await buildVirtualMesh(source, args.build);
|
|
1351
|
+
const t = performance.now();
|
|
1352
|
+
const bytes = await encodeVirtualMesh(data, { deflate: args.deflate });
|
|
1353
|
+
const encodeMs = performance.now() - t;
|
|
1354
|
+
writeFileSync(join(args.outDir, file), bytes);
|
|
1355
|
+
const s = data.stats;
|
|
1356
|
+
console.log(
|
|
1357
|
+
` ${file}: ${s.leafTriangles} triangles, ${data.meshletCount} meshlets, ${s.lodLevels} LODs, built in ${s.buildMs.toFixed(0)} ms, encoded in ${encodeMs.toFixed(0)} ms, ${(bytes.length / 1024).toFixed(0)} KB, ${entry.instances.length} instance(s)`
|
|
1358
|
+
);
|
|
1359
|
+
meshes.push({
|
|
1360
|
+
name: entry.name,
|
|
1361
|
+
file,
|
|
1362
|
+
bytes: bytes.length,
|
|
1363
|
+
triangles: s.leafTriangles,
|
|
1364
|
+
meshlets: data.meshletCount,
|
|
1365
|
+
lodLevels: s.lodLevels,
|
|
1366
|
+
material: materialInfo(entry.material),
|
|
1367
|
+
instances: entry.instances
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
const manifest = { generator: "three-virtual-geometry", formatVersion: VG_FORMAT_VERSION, source: basename(args.input), build: args.build, meshes };
|
|
1371
|
+
const json = JSON.stringify(manifest, (k, v) => k === "instances" ? v.map((m) => JSON.stringify(m)) : v, 2).replace(/"(\[[^"]*\])"/g, "$1");
|
|
1372
|
+
writeFileSync(join(args.outDir, "manifest.json"), json);
|
|
1373
|
+
console.log(`wrote ${meshes.length} .vgeo files and manifest.json to ${args.outDir} in ${((performance.now() - t0) / 1e3).toFixed(1)} s`);
|
|
1374
|
+
}
|
|
1375
|
+
main().catch((e) => {
|
|
1376
|
+
console.error(e instanceof Error ? e.message : e);
|
|
1377
|
+
process.exit(1);
|
|
1378
|
+
});
|