@tinypoly/gpu-renderer 0.2.5 → 0.3.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/ARCHITECTURE.md +48 -10
- package/README.md +2 -0
- package/dist/accumulator.d.ts.map +1 -1
- package/dist/accumulator.js +1 -0
- package/dist/accumulator.js.map +1 -1
- package/dist/denoise.js +2 -2
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +9 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +5 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts +48 -8
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +409 -134
- package/dist/geometry.js.map +1 -1
- package/dist/grass.d.ts.map +1 -1
- package/dist/grass.js +2 -1
- package/dist/grass.js.map +1 -1
- package/dist/imageResolve.js +2 -2
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instancing.d.ts +100 -0
- package/dist/instancing.d.ts.map +1 -0
- package/dist/instancing.js +286 -0
- package/dist/instancing.js.map +1 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +8 -5
- package/dist/lights.js.map +1 -1
- package/dist/memoryProbe.d.ts +76 -0
- package/dist/memoryProbe.d.ts.map +1 -0
- package/dist/memoryProbe.js +197 -0
- package/dist/memoryProbe.js.map +1 -0
- package/dist/oidnPass.d.ts +6 -0
- package/dist/oidnPass.d.ts.map +1 -1
- package/dist/oidnPass.js +18 -1
- package/dist/oidnPass.js.map +1 -1
- package/dist/pathTracePass.d.ts +15 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +38 -16
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +248 -41
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +26 -8
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +97 -27
- package/dist/sceneUpload.js.map +1 -1
- package/dist/shaderModule.d.ts.map +1 -1
- package/dist/shaderModule.js +2 -1
- package/dist/shaderModule.js.map +1 -1
- package/dist/skyBake.d.ts.map +1 -1
- package/dist/skyBake.js +5 -3
- package/dist/skyBake.js.map +1 -1
- package/dist/texturePack.d.ts +43 -3
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +209 -37
- package/dist/texturePack.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +5 -1
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +105 -15
- package/dist/wgsl/bvh.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +92 -11
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +24 -14
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/dist/wgsl/texture.wgsl.d.ts +1 -1
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
- package/dist/wgsl/texture.wgsl.js +11 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/accumulator.ts +1 -0
- package/src/denoise.ts +2 -2
- package/src/device.ts +12 -0
- package/src/frameLayout.ts +5 -0
- package/src/geometry.ts +527 -133
- package/src/grass.ts +2 -1
- package/src/imageResolve.ts +2 -2
- package/src/index.ts +1 -0
- package/src/instancing.ts +415 -0
- package/src/lights.ts +9 -5
- package/src/memoryProbe.ts +275 -0
- package/src/oidnPass.ts +21 -1
- package/src/pathTracePass.ts +42 -18
- package/src/renderer.ts +308 -47
- package/src/sceneUpload.ts +139 -36
- package/src/shaderModule.ts +2 -1
- package/src/skyBake.ts +5 -3
- package/src/texturePack.ts +274 -43
- package/src/wgsl/bvh.wgsl.ts +105 -15
- package/src/wgsl/common.wgsl.ts +92 -11
- package/src/wgsl/surface.wgsl.ts +24 -14
- package/src/wgsl/texture.wgsl.ts +11 -0
package/src/geometry.ts
CHANGED
|
@@ -3,32 +3,56 @@ import {
|
|
|
3
3
|
isMaterialVisible,
|
|
4
4
|
MATERIAL_KIND,
|
|
5
5
|
MATERIAL_UNLIT_LIGHT,
|
|
6
|
+
MATERIAL_VOLUME,
|
|
6
7
|
materialProperties,
|
|
7
8
|
NO_PLACEMENT,
|
|
8
9
|
NO_PROGRAM,
|
|
9
10
|
type MaterialPlacement,
|
|
10
11
|
type MaterialProperties,
|
|
11
12
|
} from "./materialLayout.js";
|
|
13
|
+
import {
|
|
14
|
+
geometryKey,
|
|
15
|
+
INSTANCE_MIN_TRIANGLES,
|
|
16
|
+
instanceable,
|
|
17
|
+
transformBox,
|
|
18
|
+
trianglesBox,
|
|
19
|
+
type Instance,
|
|
20
|
+
type PackedInstancing,
|
|
21
|
+
type Prototype,
|
|
22
|
+
} from "./instancing.js";
|
|
12
23
|
import { normalMatrix, transformNormal, transformPoint } from "./math.js";
|
|
13
24
|
import type { ShaderPrograms } from "./shaderProgram.js";
|
|
14
25
|
import { type CpuShader, matrixValue, sampleTexture } from "./upstream.js";
|
|
15
26
|
import type { SerializedMaterial, SerializedMesh, SerializedScene, SerializedTexture, UniformValue } from "./upstream.js";
|
|
16
27
|
|
|
17
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The GPU ready form of a scene's triangles. Positions are nine coordinates per triangle, world space, which is what the
|
|
30
|
+
* traversal reads; everything shading reads is kept per vertex, and each triangle names its three vertices. Geometry the
|
|
31
|
+
* scene repeats is kept once, in `instancing`.
|
|
32
|
+
*/
|
|
18
33
|
export interface PackedGeometry {
|
|
19
|
-
/** Nine coordinates per triangle. */
|
|
34
|
+
/** Nine coordinates per triangle of the scene's own, world space. */
|
|
20
35
|
positions: Float32Array;
|
|
21
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Three vertex numbers per stored triangle: the scene's own first, then every prototype's (see `instancing`), in the
|
|
38
|
+
* order the stored triangles are numbered.
|
|
39
|
+
*/
|
|
40
|
+
corners: Uint32Array;
|
|
41
|
+
/** Three normal components per vertex; zero where the mesh authored a zero normal. */
|
|
22
42
|
normals: Float32Array;
|
|
23
|
-
/**
|
|
43
|
+
/** Four per vertex: two UV sets, which is what Three's map channels need. */
|
|
24
44
|
uvs: Float32Array;
|
|
25
|
-
/**
|
|
45
|
+
/** Four per vertex, RGBA, when any material reads vertex colours; empty otherwise. */
|
|
26
46
|
colors: Float32Array;
|
|
47
|
+
vertexCount: number;
|
|
27
48
|
/** Shader varyings, three corners per triangle of a shader material, laid out as each program asks. */
|
|
28
49
|
varyings: Float32Array;
|
|
29
|
-
/** One material index per triangle. */
|
|
50
|
+
/** One material index per triangle of the scene's own. */
|
|
30
51
|
materials: Uint32Array;
|
|
52
|
+
/** Triangles of the scene's own: what lights, windows, volumes and grass are found among. */
|
|
31
53
|
triangleCount: number;
|
|
54
|
+
/** Repeated geometry, kept once with a transform per copy; null when nothing repeats. */
|
|
55
|
+
instancing: PackedInstancing | null;
|
|
32
56
|
/** Four floats per material: linear rgb and opacity. */
|
|
33
57
|
materialColors: Float32Array;
|
|
34
58
|
/** Four floats per material: linear emissive rgb and its intensity. */
|
|
@@ -47,6 +71,15 @@ export interface PackedGeometry {
|
|
|
47
71
|
cameraDependent: boolean;
|
|
48
72
|
}
|
|
49
73
|
|
|
74
|
+
/** How one mesh was packed, for a host measuring where the time goes. */
|
|
75
|
+
export interface MeshPacked {
|
|
76
|
+
/** Its index among the scene's meshes. */
|
|
77
|
+
mesh: number;
|
|
78
|
+
path: "plain" | "general" | "instance" | "prototype";
|
|
79
|
+
triangles: number;
|
|
80
|
+
ms: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
50
83
|
export interface PackOptions {
|
|
51
84
|
/** The index in the texture table of each texture, by id. */
|
|
52
85
|
textures?: ReadonlyMap<string, number>;
|
|
@@ -62,6 +95,10 @@ export interface PackOptions {
|
|
|
62
95
|
extraBase?: number;
|
|
63
96
|
/** Scene fog, which a shader's fog uniforms read. */
|
|
64
97
|
fog?: SerializedScene["fog"];
|
|
98
|
+
/** Whether repeated geometry may become instances. On by default. */
|
|
99
|
+
instancing?: boolean;
|
|
100
|
+
/** Told about every mesh once it is packed. */
|
|
101
|
+
onMesh?: (info: MeshPacked) => void;
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
/** Three's wireframe and line width: WebGL draws every line one pixel wide whatever the material asks. */
|
|
@@ -88,6 +125,33 @@ interface Entry {
|
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
/** Grows a float array as triangles are added, since how many a primitive makes is only known as it is built. */
|
|
128
|
+
class UintStream {
|
|
129
|
+
data: Uint32Array;
|
|
130
|
+
length = 0;
|
|
131
|
+
|
|
132
|
+
constructor(capacity: number) {
|
|
133
|
+
this.data = new Uint32Array(Math.max(16, capacity));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
reserve(count: number) {
|
|
137
|
+
if (this.length + count <= this.data.length) return;
|
|
138
|
+
|
|
139
|
+
const next = new Uint32Array(Math.max(this.data.length * 2, this.length + count));
|
|
140
|
+
|
|
141
|
+
next.set(this.data.subarray(0, this.length));
|
|
142
|
+
this.data = next;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
push(value: number) {
|
|
146
|
+
this.reserve(1);
|
|
147
|
+
this.data[this.length++] = value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
view(): Uint32Array {
|
|
151
|
+
return this.data.subarray(0, this.length);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
91
155
|
class FloatStream {
|
|
92
156
|
data: Float32Array;
|
|
93
157
|
length = 0;
|
|
@@ -202,16 +266,65 @@ function flatten(value: unknown): number[] | null {
|
|
|
202
266
|
return null;
|
|
203
267
|
}
|
|
204
268
|
|
|
269
|
+
/** Triangles one slice of packing covers before it may hand the thread back. */
|
|
270
|
+
const SLICE_TRIANGLES = 32768;
|
|
271
|
+
|
|
272
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
273
|
+
|
|
274
|
+
/** A mesh's groups, or the one group covering it when it declares none. */
|
|
275
|
+
function groupsOf(mesh: SerializedMesh) {
|
|
276
|
+
return mesh.groups.length
|
|
277
|
+
? mesh.groups
|
|
278
|
+
: [{
|
|
279
|
+
start: 0,
|
|
280
|
+
count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
|
|
281
|
+
materialIndex: 0,
|
|
282
|
+
}];
|
|
283
|
+
}
|
|
284
|
+
|
|
205
285
|
/**
|
|
206
286
|
* Flattens the scene into triangles, materials resolved and transforms applied. Every triangle carries its own three
|
|
207
|
-
*
|
|
208
|
-
*
|
|
287
|
+
* positions: the traversal reads them straight off the buffer, and sharing them would cost an indirection on the
|
|
288
|
+
* hottest path of the kernel. What shading reads (normals, UVs, colours) is kept per vertex instead, since it is read
|
|
289
|
+
* once a hit, and a mesh that repeats becomes instances of one prototype (see `instancing.ts`).
|
|
209
290
|
*
|
|
210
291
|
* Points, lines and sprites become triangles that face the camera, sized the way Three draws them, so a path has a
|
|
211
292
|
* surface to find; vertex shaders run here, on the CPU renderer's interpreter, and their varyings ride along per corner.
|
|
212
293
|
*/
|
|
213
294
|
export function packGeometry(
|
|
214
295
|
scene: SerializedScene, options: PackOptions | ReadonlyMap<string, number> = {}): PackedGeometry {
|
|
296
|
+
const steps = packingSteps(scene, options);
|
|
297
|
+
let step = steps.next();
|
|
298
|
+
|
|
299
|
+
while (!step.done) step = steps.next();
|
|
300
|
+
|
|
301
|
+
return step.value;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The same packing, handing the thread back between slices: `pause` is awaited whenever the work has held the thread
|
|
306
|
+
* for `sliceMs`, so a large scene does not freeze the page while it packs.
|
|
307
|
+
*/
|
|
308
|
+
export async function packGeometrySliced(
|
|
309
|
+
scene: SerializedScene, options: PackOptions, pause: () => Promise<void>, sliceMs = 12): Promise<PackedGeometry> {
|
|
310
|
+
const steps = packingSteps(scene, options);
|
|
311
|
+
let started = performance.now();
|
|
312
|
+
let step = steps.next();
|
|
313
|
+
|
|
314
|
+
while (!step.done) {
|
|
315
|
+
if (performance.now() - started >= sliceMs) {
|
|
316
|
+
await pause();
|
|
317
|
+
started = performance.now();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
step = steps.next();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return step.value;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function* packingSteps(
|
|
327
|
+
scene: SerializedScene, options: PackOptions | ReadonlyMap<string, number>): Generator<void, PackedGeometry> {
|
|
215
328
|
const settings: PackOptions = options instanceof Map ? { textures: options } : options as PackOptions;
|
|
216
329
|
const textures = settings.textures ?? new Map<string, number>();
|
|
217
330
|
const textureData = settings.textureData ?? new Map<string, SerializedTexture>();
|
|
@@ -221,22 +334,50 @@ export function packGeometry(
|
|
|
221
334
|
const programs = settings.programs;
|
|
222
335
|
const extraBase = settings.extraBase ?? 0;
|
|
223
336
|
const meshes = scene.meshes.filter(mesh => mesh.attributes.position);
|
|
224
|
-
|
|
337
|
+
|
|
338
|
+
const repeated = settings.instancing === false ? new Map<SerializedMesh, string>()
|
|
339
|
+
: repeatedGeometry(meshes, material => plainMaterial(material, textures, programs));
|
|
340
|
+
|
|
341
|
+
const estimate = meshes.reduce((count, mesh) =>
|
|
342
|
+
count + (mesh.primitive || repeated.has(mesh) ? 0 : triangleCountOf(mesh)), 0);
|
|
343
|
+
|
|
344
|
+
const vertexEstimate = meshes.reduce((count, mesh) =>
|
|
345
|
+
count + (mesh.primitive || repeated.has(mesh) ? 0 : mesh.attributes.position.data.length / 3), 0);
|
|
225
346
|
|
|
226
347
|
const anyVertexColors = meshes.some(mesh =>
|
|
227
348
|
mesh.attributes.color && mesh.materials.some(m => m?.values?.vertexColors === true));
|
|
228
349
|
|
|
229
350
|
const positions = new FloatStream(estimate * 9);
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const
|
|
351
|
+
const corners = new UintStream(estimate * 3);
|
|
352
|
+
const normals = new FloatStream(vertexEstimate * 3);
|
|
353
|
+
const uvs = new FloatStream(vertexEstimate * 4);
|
|
354
|
+
const colors = new FloatStream(anyVertexColors ? vertexEstimate * 4 : 0);
|
|
355
|
+
let vertexCount = 0;
|
|
233
356
|
const varyings = new FloatStream(0);
|
|
234
|
-
const triangleMaterials
|
|
357
|
+
const triangleMaterials = new UintStream(estimate);
|
|
235
358
|
const entries: Entry[] = [];
|
|
236
359
|
const shared = new Map<SerializedMaterial | undefined, Entry>();
|
|
237
360
|
const extra: number[] = [];
|
|
238
361
|
let cameraDependent = false;
|
|
239
362
|
|
|
363
|
+
// Repeated geometry: each prototype's triangles once, in object space, with the group each one came from.
|
|
364
|
+
const prototypePositions = new FloatStream(0);
|
|
365
|
+
const prototypeCorners = new UintStream(0);
|
|
366
|
+
const prototypeGroups = new UintStream(0);
|
|
367
|
+
const prototypes: Prototype[] = [];
|
|
368
|
+
const prototypeOf = new Map<string, number>();
|
|
369
|
+
const instances: Instance[] = [];
|
|
370
|
+
const instanceMaterials = new UintStream(0);
|
|
371
|
+
let instanced = 0;
|
|
372
|
+
|
|
373
|
+
const flatSink: TriangleSink = { positions, corners, tags: triangleMaterials };
|
|
374
|
+
|
|
375
|
+
const prototypeSink: TriangleSink = {
|
|
376
|
+
positions: prototypePositions,
|
|
377
|
+
corners: prototypeCorners,
|
|
378
|
+
tags: prototypeGroups,
|
|
379
|
+
};
|
|
380
|
+
|
|
240
381
|
/** A material's clipping planes, written once to the extra words. */
|
|
241
382
|
const clipPlanes = new Map<SerializedMaterial, { start: number; count: number }>();
|
|
242
383
|
|
|
@@ -327,24 +468,49 @@ export function packGeometry(
|
|
|
327
468
|
return entry;
|
|
328
469
|
};
|
|
329
470
|
|
|
330
|
-
|
|
471
|
+
/** A vertex of its own, for everything a hit reads per vertex; its number. */
|
|
472
|
+
const emitVertex = (normal: ArrayLike<number>, n: number, uv: ArrayLike<number>, u: number,
|
|
473
|
+
color: ArrayLike<number>, c: number): number => {
|
|
474
|
+
normals.reserve(3);
|
|
475
|
+
normals.data[normals.length++] = normal[n];
|
|
476
|
+
normals.data[normals.length++] = normal[n + 1];
|
|
477
|
+
normals.data[normals.length++] = normal[n + 2];
|
|
478
|
+
uvs.reserve(4);
|
|
479
|
+
uvs.data[uvs.length++] = uv[u];
|
|
480
|
+
uvs.data[uvs.length++] = uv[u + 1];
|
|
481
|
+
uvs.data[uvs.length++] = uv[u + 2];
|
|
482
|
+
uvs.data[uvs.length++] = uv[u + 3];
|
|
483
|
+
|
|
484
|
+
if (anyVertexColors) {
|
|
485
|
+
colors.reserve(4);
|
|
486
|
+
colors.data[colors.length++] = color[c];
|
|
487
|
+
colors.data[colors.length++] = color[c + 1];
|
|
488
|
+
colors.data[colors.length++] = color[c + 2];
|
|
489
|
+
colors.data[colors.length++] = color[c + 3];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return vertexCount++;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const writeTriangle = (triangle: readonly Corner[], entry: Entry) => {
|
|
331
496
|
// Geometry without normals is shaded flat: the three corners share the face normal.
|
|
332
|
-
const flat =
|
|
333
|
-
? faceNormal(
|
|
497
|
+
const flat = triangle.some(corner => corner.normal.length < 3)
|
|
498
|
+
? faceNormal(triangle[0].position, triangle[1].position, triangle[2].position)
|
|
334
499
|
: null;
|
|
335
500
|
|
|
336
|
-
for (const corner of
|
|
501
|
+
for (const corner of triangle) {
|
|
337
502
|
positions.push(corner.position, 3);
|
|
338
|
-
|
|
339
|
-
uvs.push([corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 4);
|
|
503
|
+
const color = corner.color;
|
|
340
504
|
|
|
341
|
-
|
|
505
|
+
// A missing component reads as zero, as the stream it replaces padded it.
|
|
506
|
+
corners.push(emitVertex(flat ?? corner.normal, 0, [corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 0,
|
|
507
|
+
[color[0] ?? 0, color[1] ?? 0, color[2] ?? 0, color[3] ?? 0], 0));
|
|
342
508
|
}
|
|
343
509
|
|
|
344
510
|
const stride = entry.placement.varyingStride;
|
|
345
511
|
|
|
346
512
|
if (stride > 0)
|
|
347
|
-
for (const corner of
|
|
513
|
+
for (const corner of triangle) {
|
|
348
514
|
if (corner.varyings) varyings.push(corner.varyings, stride);
|
|
349
515
|
else varyings.pad(stride);
|
|
350
516
|
}
|
|
@@ -352,179 +518,260 @@ export function packGeometry(
|
|
|
352
518
|
triangleMaterials.push(entry.index);
|
|
353
519
|
};
|
|
354
520
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const { world, normal, hasNormal, uv, color, valid } = plain;
|
|
362
|
-
const index = mesh.index;
|
|
363
|
-
const stride = entry.placement.varyingStride;
|
|
364
|
-
const corners = [0, 0, 0];
|
|
521
|
+
const writePlainTriangles = (vertices: PlainVertices, start: number, end: number, mirrored: boolean,
|
|
522
|
+
stride: number, sink: TriangleSink, tag: number) => {
|
|
523
|
+
const { world, normal, hasNormal, uv, color, valid, ids } = vertices;
|
|
524
|
+
const index = vertices.mesh.index;
|
|
525
|
+
const triangle = [0, 0, 0];
|
|
365
526
|
|
|
366
527
|
for (let v = start; v + 2 < end; v += 3) {
|
|
367
528
|
const a = index ? index[v] : v, b = index ? index[v + 1] : v + 1, c = index ? index[v + 2] : v + 2;
|
|
368
529
|
|
|
369
530
|
if (!valid(a) || !valid(b) || !valid(c)) continue;
|
|
370
531
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
532
|
+
triangle[0] = a;
|
|
533
|
+
triangle[1] = mirrored ? c : b;
|
|
534
|
+
triangle[2] = mirrored ? b : c;
|
|
374
535
|
|
|
375
|
-
// Geometry without normals is shaded flat: the three corners share the face normal.
|
|
536
|
+
// Geometry without normals is shaded flat: the three corners share the face normal, so a vertex of their own.
|
|
376
537
|
const flat = !hasNormal[a] || !hasNormal[b] || !hasNormal[c]
|
|
377
538
|
? faceNormal(
|
|
378
|
-
Array.from(world.subarray(
|
|
379
|
-
Array.from(world.subarray(
|
|
380
|
-
Array.from(world.subarray(
|
|
539
|
+
Array.from(world.subarray(triangle[0] * 3, triangle[0] * 3 + 3)),
|
|
540
|
+
Array.from(world.subarray(triangle[1] * 3, triangle[1] * 3 + 3)),
|
|
541
|
+
Array.from(world.subarray(triangle[2] * 3, triangle[2] * 3 + 3)))
|
|
381
542
|
: null;
|
|
382
543
|
|
|
383
|
-
positions.reserve(9);
|
|
384
|
-
|
|
385
|
-
uvs.reserve(12);
|
|
544
|
+
sink.positions.reserve(9);
|
|
545
|
+
sink.corners.reserve(3);
|
|
386
546
|
|
|
387
|
-
|
|
547
|
+
for (const corner of triangle) {
|
|
548
|
+
const p = corner * 3;
|
|
388
549
|
|
|
389
|
-
|
|
390
|
-
|
|
550
|
+
sink.positions.data[sink.positions.length++] = world[p];
|
|
551
|
+
sink.positions.data[sink.positions.length++] = world[p + 1];
|
|
552
|
+
sink.positions.data[sink.positions.length++] = world[p + 2];
|
|
391
553
|
|
|
392
|
-
|
|
393
|
-
positions.data[positions.length++] = world[p + 1];
|
|
394
|
-
positions.data[positions.length++] = world[p + 2];
|
|
554
|
+
let id: number;
|
|
395
555
|
|
|
396
556
|
if (flat) {
|
|
397
|
-
|
|
398
|
-
normals.data[normals.length++] = flat[1];
|
|
399
|
-
normals.data[normals.length++] = flat[2];
|
|
557
|
+
id = emitVertex(flat, 0, uv, corner * 4, color, corner * 4);
|
|
400
558
|
} else {
|
|
401
|
-
|
|
402
|
-
normals.data[normals.length++] = normal[p + 1];
|
|
403
|
-
normals.data[normals.length++] = normal[p + 2];
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
uvs.data[uvs.length++] = uv[q];
|
|
407
|
-
uvs.data[uvs.length++] = uv[q + 1];
|
|
408
|
-
uvs.data[uvs.length++] = uv[q + 2];
|
|
409
|
-
uvs.data[uvs.length++] = uv[q + 3];
|
|
559
|
+
id = ids[corner];
|
|
410
560
|
|
|
411
|
-
|
|
412
|
-
colors.data[colors.length++] = color[q];
|
|
413
|
-
colors.data[colors.length++] = color[q + 1];
|
|
414
|
-
colors.data[colors.length++] = color[q + 2];
|
|
415
|
-
colors.data[colors.length++] = color[q + 3];
|
|
561
|
+
if (id < 0) id = ids[corner] = emitVertex(normal, p, uv, corner * 4, color, corner * 4);
|
|
416
562
|
}
|
|
563
|
+
|
|
564
|
+
sink.corners.data[sink.corners.length++] = id;
|
|
417
565
|
}
|
|
418
566
|
|
|
419
567
|
if (stride > 0) varyings.pad(3 * stride);
|
|
420
568
|
|
|
421
|
-
|
|
569
|
+
sink.tags.push(tag);
|
|
422
570
|
}
|
|
423
571
|
};
|
|
424
572
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
573
|
+
// Time spent on the mesh at hand, without the pauses between slices.
|
|
574
|
+
let meshMs = 0;
|
|
575
|
+
let segment = performance.now();
|
|
576
|
+
|
|
577
|
+
const pause = function* () {
|
|
578
|
+
meshMs += performance.now() - segment;
|
|
579
|
+
yield;
|
|
580
|
+
segment = performance.now();
|
|
581
|
+
};
|
|
433
582
|
|
|
434
|
-
|
|
583
|
+
/** A plain run of triangles, sliced so the page is not held for the whole of a large mesh. */
|
|
584
|
+
const plainRun = function* (vertices: PlainVertices, start: number, end: number, mirrored: boolean, stride: number,
|
|
585
|
+
sink: TriangleSink, tag: number) {
|
|
586
|
+
for (let from = start; from < end; from += SLICE_TRIANGLES * 3) {
|
|
587
|
+
writePlainTriangles(vertices, from, Math.min(end, from + SLICE_TRIANGLES * 3), mirrored, stride, sink, tag);
|
|
435
588
|
|
|
436
|
-
|
|
437
|
-
|
|
589
|
+
if (from + SLICE_TRIANGLES * 3 < end) yield* pause();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
438
592
|
|
|
439
|
-
|
|
593
|
+
for (let meshIndex = 0; meshIndex < meshes.length; meshIndex++) {
|
|
594
|
+
const mesh = meshes[meshIndex];
|
|
595
|
+
const groups = groupsOf(mesh);
|
|
596
|
+
const key = repeated.get(mesh);
|
|
597
|
+
const before = triangleMaterials.length + instanced;
|
|
598
|
+
let path: MeshPacked["path"] = "plain";
|
|
440
599
|
|
|
441
|
-
|
|
442
|
-
|
|
600
|
+
meshMs = 0;
|
|
601
|
+
segment = performance.now();
|
|
443
602
|
|
|
444
|
-
|
|
603
|
+
if (key !== undefined) {
|
|
604
|
+
let prototype = prototypeOf.get(key);
|
|
445
605
|
|
|
446
|
-
//
|
|
447
|
-
if (
|
|
606
|
+
// The first copy lays the prototype down, in object space; the next ones only point at it.
|
|
607
|
+
if (prototype === undefined) {
|
|
608
|
+
const first = prototypeGroups.length;
|
|
609
|
+
const vertices = plainVertices(mesh, IDENTITY);
|
|
448
610
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const start = Math.max(0, group.start);
|
|
453
|
-
const end = Math.min(group.start + group.count, count);
|
|
454
|
-
const at = (offset: number) => (mesh.index ? mesh.index[offset] : offset);
|
|
611
|
+
for (let g = 0; g < groups.length; g++) {
|
|
612
|
+
const group = groups[g];
|
|
613
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
455
614
|
|
|
456
|
-
|
|
457
|
-
// A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
|
|
458
|
-
const mirrored = determinant3(mesh.matrixWorld) < 0;
|
|
459
|
-
// Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once per
|
|
460
|
-
// instance, reading the per-instance attributes at that instance.
|
|
461
|
-
const instancing = vertexShader ? instancedAttributes(mesh) : null;
|
|
615
|
+
if (!isMaterialVisible(material)) continue;
|
|
462
616
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
continue;
|
|
617
|
+
const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
|
|
618
|
+
|
|
619
|
+
yield* plainRun(vertices, Math.max(0, group.start), Math.min(group.start + group.count, count), false, 0,
|
|
620
|
+
prototypeSink, g);
|
|
468
621
|
}
|
|
469
622
|
|
|
470
|
-
|
|
471
|
-
|
|
623
|
+
prototype = prototypes.length;
|
|
624
|
+
prototypes.push({ first, count: prototypeGroups.length - first });
|
|
625
|
+
prototypeOf.set(key, prototype);
|
|
626
|
+
path = "prototype";
|
|
627
|
+
} else {
|
|
628
|
+
path = "instance";
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const { first, count } = prototypes[prototype];
|
|
472
632
|
|
|
473
|
-
|
|
474
|
-
|
|
633
|
+
if (count > 0) {
|
|
634
|
+
const entryOfGroup = groups.map(group => {
|
|
635
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
636
|
+
|
|
637
|
+
return isMaterialVisible(material) ? entryFor(material, mesh).index : 0;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
instances.push({ prototype, matrix: mesh.matrixWorld, firstTriangle: instanced });
|
|
641
|
+
instanceMaterials.reserve(count);
|
|
642
|
+
|
|
643
|
+
for (let i = 0; i < count; i++)
|
|
644
|
+
instanceMaterials.data[instanceMaterials.length++] = entryOfGroup[prototypeGroups.data[first + i]];
|
|
645
|
+
|
|
646
|
+
instanced += count;
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
if (mesh.primitive) cameraDependent = true;
|
|
650
|
+
|
|
651
|
+
// Every vertex of a plain mesh, transformed once and shared by its groups.
|
|
652
|
+
let plain: PlainVertices | null = null;
|
|
653
|
+
|
|
654
|
+
for (const group of groups) {
|
|
655
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
656
|
+
|
|
657
|
+
if (!isMaterialVisible(material)) continue;
|
|
658
|
+
|
|
659
|
+
const entry = entryFor(material, mesh);
|
|
660
|
+
const vertexShader = material ? programs?.vertex(material) ?? null : null;
|
|
661
|
+
|
|
662
|
+
if (vertexShader) cameraDependent = true;
|
|
663
|
+
|
|
664
|
+
// Primitives that face the camera need one to face; without it there is nothing sensible to build.
|
|
665
|
+
if (mesh.primitive && !camera) continue;
|
|
666
|
+
|
|
667
|
+
const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
|
|
668
|
+
const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
|
|
669
|
+
const start = Math.max(0, group.start);
|
|
670
|
+
const end = Math.min(group.start + group.count, count);
|
|
671
|
+
const at = (offset: number) => (mesh.index ? mesh.index[offset] : offset);
|
|
672
|
+
|
|
673
|
+
if (!mesh.primitive) {
|
|
674
|
+
// A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
|
|
675
|
+
const mirrored = determinant3(mesh.matrixWorld) < 0;
|
|
676
|
+
|
|
677
|
+
// The common case, a plain mesh, skips the general reader: each vertex is transformed once, straight into
|
|
678
|
+
// typed arrays, and each triangle is copied from them. What it writes is exactly what the reader would.
|
|
679
|
+
if (!vertexShader && !material?.maps?.displacementMap) {
|
|
680
|
+
plain ??= plainVertices(mesh);
|
|
681
|
+
yield* plainRun(plain, start, end, mirrored, entry.placement.varyingStride, flatSink, entry.index);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
475
684
|
|
|
476
|
-
|
|
685
|
+
path = "general";
|
|
477
686
|
|
|
478
|
-
|
|
687
|
+
// Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once
|
|
688
|
+
// per instance, reading the per-instance attributes at that instance.
|
|
689
|
+
const instancing = vertexShader ? instancedAttributes(mesh) : null;
|
|
690
|
+
|
|
691
|
+
for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
|
|
692
|
+
const reader = new VertexReader(mesh, material, context, instancing?.names, instance);
|
|
693
|
+
|
|
694
|
+
for (let v = start; v + 2 < end; v += 3) {
|
|
695
|
+
const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
|
|
696
|
+
|
|
697
|
+
if (a && b && c) writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
|
|
698
|
+
|
|
699
|
+
if ((v - start) % (SLICE_TRIANGLES * 3) === 0 && v > start) yield* pause();
|
|
700
|
+
}
|
|
479
701
|
}
|
|
702
|
+
|
|
703
|
+
continue;
|
|
480
704
|
}
|
|
481
705
|
|
|
482
|
-
|
|
483
|
-
}
|
|
706
|
+
path = "general";
|
|
484
707
|
|
|
485
|
-
|
|
486
|
-
for (let v = start; v < end; v++)
|
|
487
|
-
for (const triangle of vertices.point(at(v))) writeTriangle(triangle, entry);
|
|
708
|
+
const vertices = new VertexReader(mesh, material, context);
|
|
488
709
|
|
|
489
|
-
|
|
490
|
-
|
|
710
|
+
if (mesh.primitive === "points") {
|
|
711
|
+
for (let v = start; v < end; v++)
|
|
712
|
+
for (const triangle of vertices.point(at(v))) writeTriangle(triangle, entry);
|
|
713
|
+
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
491
716
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
717
|
+
if (mesh.primitive === "sprite") {
|
|
718
|
+
for (let v = start; v + 2 < end; v += 3) {
|
|
719
|
+
const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
|
|
720
|
+
|
|
721
|
+
if (a && b && c) writeTriangle([a, b, c], entry);
|
|
722
|
+
}
|
|
495
723
|
|
|
496
|
-
|
|
724
|
+
continue;
|
|
497
725
|
}
|
|
498
726
|
|
|
499
|
-
|
|
500
|
-
|
|
727
|
+
// Lines: segments, a strip, or a strip that closes on itself.
|
|
728
|
+
const segments: [number, number][] = [];
|
|
501
729
|
|
|
502
|
-
|
|
503
|
-
|
|
730
|
+
if (mesh.primitive === "lineSegments") {
|
|
731
|
+
for (let v = start; v + 1 < end; v += 2) segments.push([at(v), at(v + 1)]);
|
|
732
|
+
} else {
|
|
733
|
+
for (let v = start; v + 1 < end; v++) segments.push([at(v), at(v + 1)]);
|
|
504
734
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
} else {
|
|
508
|
-
for (let v = start; v + 1 < end; v++) segments.push([at(v), at(v + 1)]);
|
|
735
|
+
if (mesh.primitive === "lineLoop" && end - start > 2) segments.push([at(end - 1), at(start)]);
|
|
736
|
+
}
|
|
509
737
|
|
|
510
|
-
|
|
738
|
+
for (const [a, b] of segments)
|
|
739
|
+
for (const triangle of vertices.segment(a, b)) writeTriangle(triangle, entry);
|
|
511
740
|
}
|
|
512
|
-
|
|
513
|
-
for (const [a, b] of segments)
|
|
514
|
-
for (const triangle of vertices.segment(a, b)) writeTriangle(triangle, entry);
|
|
515
741
|
}
|
|
742
|
+
|
|
743
|
+
meshMs += performance.now() - segment;
|
|
744
|
+
settings.onMesh?.({
|
|
745
|
+
mesh: meshIndex,
|
|
746
|
+
path,
|
|
747
|
+
triangles: triangleMaterials.length + instanced - before,
|
|
748
|
+
ms: meshMs,
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
yield* pause();
|
|
516
752
|
}
|
|
517
753
|
|
|
518
754
|
const properties = entries.map(entry => entry.properties);
|
|
755
|
+
const sceneTriangles = triangleMaterials.length;
|
|
756
|
+
const prototypeTriangles = prototypeGroups.length;
|
|
757
|
+
const allCorners = new Uint32Array((sceneTriangles + prototypeTriangles) * 3);
|
|
758
|
+
|
|
759
|
+
allCorners.set(corners.view(), 0);
|
|
760
|
+
allCorners.set(prototypeCorners.view(), sceneTriangles * 3);
|
|
519
761
|
|
|
520
762
|
return {
|
|
521
763
|
positions: positions.view(),
|
|
764
|
+
corners: allCorners,
|
|
522
765
|
normals: normals.view(),
|
|
523
766
|
uvs: uvs.view(),
|
|
524
767
|
colors: colors.view(),
|
|
768
|
+
vertexCount,
|
|
525
769
|
varyings: varyings.view(),
|
|
526
|
-
materials:
|
|
527
|
-
triangleCount:
|
|
770
|
+
materials: triangleMaterials.view(),
|
|
771
|
+
triangleCount: sceneTriangles,
|
|
772
|
+
instancing: instances.length
|
|
773
|
+
? packedInstancing(prototypePositions.view(), prototypes, instances, sceneTriangles, instanceMaterials.view())
|
|
774
|
+
: null,
|
|
528
775
|
materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
|
|
529
776
|
// Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs. An unlit
|
|
530
777
|
// material of a fixed colour emits that colour, so it is one too.
|
|
@@ -540,6 +787,150 @@ export function packGeometry(
|
|
|
540
787
|
};
|
|
541
788
|
}
|
|
542
789
|
|
|
790
|
+
/** Where a run of plain triangles goes: their positions, their vertices, and a number each. */
|
|
791
|
+
interface TriangleSink {
|
|
792
|
+
positions: FloatStream;
|
|
793
|
+
corners: UintStream;
|
|
794
|
+
tags: UintStream;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** The instances, numbered after the scene's own triangles, with their prototypes and the box around them all. */
|
|
798
|
+
function packedInstancing(positions: Float32Array, prototypes: Prototype[], instances: Instance[],
|
|
799
|
+
sceneTriangles: number, materials: Uint32Array): PackedInstancing {
|
|
800
|
+
const boxes = prototypes.map(prototype => trianglesBox(positions, prototype.first, prototype.count));
|
|
801
|
+
const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
|
|
802
|
+
|
|
803
|
+
for (const instance of instances) {
|
|
804
|
+
const box = transformBox(instance.matrix, boxes[instance.prototype].low, boxes[instance.prototype].high);
|
|
805
|
+
|
|
806
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
807
|
+
low[axis] = Math.min(low[axis], box.low[axis]);
|
|
808
|
+
high[axis] = Math.max(high[axis], box.high[axis]);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return {
|
|
813
|
+
positions,
|
|
814
|
+
triangleCount: positions.length / 9,
|
|
815
|
+
prototypes,
|
|
816
|
+
instances: instances.map(instance => ({ ...instance, firstTriangle: sceneTriangles + instance.firstTriangle })),
|
|
817
|
+
materials,
|
|
818
|
+
bounds: { low, high },
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Whether a material can be shared by instances: nothing about it is read triangle by triangle on the CPU (lights,
|
|
824
|
+
* windows, volumes, grass) or tied to one mesh (shaders, object space normal maps, displacement).
|
|
825
|
+
*/
|
|
826
|
+
function plainMaterial(material: SerializedMaterial | undefined, textures: ReadonlyMap<string, number>,
|
|
827
|
+
programs: ShaderPrograms | undefined): boolean {
|
|
828
|
+
if (!material) return true;
|
|
829
|
+
if (programs?.fragment(material) || programs?.vertex(material)) return false;
|
|
830
|
+
if (material.values?.normalMapType === 1 || material.maps?.displacementMap) return false;
|
|
831
|
+
|
|
832
|
+
const p = materialProperties(material, textures);
|
|
833
|
+
|
|
834
|
+
if (p.kind === MATERIAL_KIND.shader || (p.flags & (MATERIAL_UNLIT_LIGHT | MATERIAL_VOLUME)) !== 0) return false;
|
|
835
|
+
if ((p.emissive[0] + p.emissive[1] + p.emissive[2]) * p.emissiveIntensity > 0) return false;
|
|
836
|
+
if (p.transmission > 0 && p.thickness <= 0) return false;
|
|
837
|
+
|
|
838
|
+
return !p.grass;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The meshes whose geometry another mesh repeats, each with the key its copies share. Meshes are first set apart by
|
|
843
|
+
* their sizes, which is free, and only those that still have a match are hashed.
|
|
844
|
+
*/
|
|
845
|
+
function repeatedGeometry(meshes: readonly SerializedMesh[],
|
|
846
|
+
plain: (material: SerializedMaterial | undefined) => boolean): Map<SerializedMesh, string> {
|
|
847
|
+
const plainOf = new Map<SerializedMaterial | undefined, boolean>();
|
|
848
|
+
|
|
849
|
+
const isPlain = (material: SerializedMaterial | undefined) => {
|
|
850
|
+
let known = plainOf.get(material);
|
|
851
|
+
|
|
852
|
+
if (known === undefined) plainOf.set(material, known = plain(material));
|
|
853
|
+
|
|
854
|
+
return known;
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const bySize = new Map<string, { mesh: SerializedMesh; visible: string }[]>();
|
|
858
|
+
|
|
859
|
+
for (const mesh of meshes) {
|
|
860
|
+
if (mesh.primitive || !instanceable(mesh.matrixWorld) || triangleCountOf(mesh) < INSTANCE_MIN_TRIANGLES) continue;
|
|
861
|
+
|
|
862
|
+
let visible = "";
|
|
863
|
+
let usable = true;
|
|
864
|
+
|
|
865
|
+
for (const group of groupsOf(mesh)) {
|
|
866
|
+
const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
|
|
867
|
+
const shown = isMaterialVisible(material);
|
|
868
|
+
|
|
869
|
+
visible += shown ? "1" : "0";
|
|
870
|
+
|
|
871
|
+
if (shown && !isPlain(material)) usable = false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (!usable) continue;
|
|
875
|
+
|
|
876
|
+
const size = `${mesh.attributes.position.data.length}|${mesh.index?.length ?? -1}|${mesh.groups.length}|${visible}`;
|
|
877
|
+
const bucket = bySize.get(size);
|
|
878
|
+
|
|
879
|
+
if (bucket) bucket.push({ mesh, visible });
|
|
880
|
+
else bySize.set(size, [{ mesh, visible }]);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const byKey = new Map<string, SerializedMesh[]>();
|
|
884
|
+
|
|
885
|
+
for (const bucket of bySize.values()) {
|
|
886
|
+
if (bucket.length < 2) continue;
|
|
887
|
+
|
|
888
|
+
for (const { mesh, visible } of bucket) {
|
|
889
|
+
const key = geometryKey(mesh, visible);
|
|
890
|
+
const same = byKey.get(key);
|
|
891
|
+
|
|
892
|
+
if (same) same.push(mesh);
|
|
893
|
+
else byKey.set(key, [mesh]);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const repeated = new Map<SerializedMesh, string>();
|
|
898
|
+
|
|
899
|
+
for (const [key, same] of byKey) if (same.length > 1) for (const mesh of same) repeated.set(mesh, key);
|
|
900
|
+
|
|
901
|
+
return repeated;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** The three vertex numbers of a stored triangle. */
|
|
905
|
+
export function triangleVertices(geometry: PackedGeometry, triangle: number): [number, number, number] {
|
|
906
|
+
const at = triangle * 3;
|
|
907
|
+
|
|
908
|
+
return [geometry.corners[at], geometry.corners[at + 1], geometry.corners[at + 2]];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** The normal at one corner of a stored triangle. */
|
|
912
|
+
export function cornerNormal(geometry: PackedGeometry, triangle: number, corner: number): number[] {
|
|
913
|
+
const vertex = geometry.corners[triangle * 3 + corner];
|
|
914
|
+
|
|
915
|
+
return Array.from(geometry.normals.subarray(vertex * 3, vertex * 3 + 3));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** The two UV sets at one corner of a stored triangle, four numbers. */
|
|
919
|
+
export function cornerUvs(geometry: PackedGeometry, triangle: number, corner: number): number[] {
|
|
920
|
+
const vertex = geometry.corners[triangle * 3 + corner];
|
|
921
|
+
|
|
922
|
+
return Array.from(geometry.uvs.subarray(vertex * 4, vertex * 4 + 4));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** The colour at one corner of a stored triangle, RGBA, or empty when the scene has no vertex colours. */
|
|
926
|
+
export function cornerColor(geometry: PackedGeometry, triangle: number, corner: number): number[] {
|
|
927
|
+
if (!geometry.colors.length) return [];
|
|
928
|
+
|
|
929
|
+
const vertex = geometry.corners[triangle * 3 + corner];
|
|
930
|
+
|
|
931
|
+
return Array.from(geometry.colors.subarray(vertex * 4, vertex * 4 + 4));
|
|
932
|
+
}
|
|
933
|
+
|
|
543
934
|
/** A plain mesh's vertices in world space, read lazily: only the ones some triangle uses are transformed. */
|
|
544
935
|
interface PlainVertices {
|
|
545
936
|
mesh: SerializedMesh;
|
|
@@ -552,9 +943,11 @@ interface PlainVertices {
|
|
|
552
943
|
color: Float32Array;
|
|
553
944
|
/** Whether the vertex exists and has a finite position, transforming it on first use. */
|
|
554
945
|
valid(index: number): boolean;
|
|
946
|
+
/** The packed vertex each one became, or -1 before a triangle uses it. */
|
|
947
|
+
ids: Int32Array;
|
|
555
948
|
}
|
|
556
949
|
|
|
557
|
-
function plainVertices(mesh: SerializedMesh): PlainVertices {
|
|
950
|
+
function plainVertices(mesh: SerializedMesh, matrixWorld: readonly number[] = mesh.matrixWorld): PlainVertices {
|
|
558
951
|
const position = mesh.attributes.position;
|
|
559
952
|
const count = Math.floor(position.data.length / Math.max(1, position.itemSize));
|
|
560
953
|
const world = new Float32Array(count * 3);
|
|
@@ -564,8 +957,8 @@ function plainVertices(mesh: SerializedMesh): PlainVertices {
|
|
|
564
957
|
const color = new Float32Array(count * 4);
|
|
565
958
|
// 0 not read yet, 1 usable, 2 refused.
|
|
566
959
|
const state = new Uint8Array(count);
|
|
567
|
-
const rotation = normalMatrix(
|
|
568
|
-
const matrix =
|
|
960
|
+
const rotation = normalMatrix(matrixWorld);
|
|
961
|
+
const matrix = matrixWorld as number[];
|
|
569
962
|
const { normal: normals, uv: uv0, uv1, color: colors } = mesh.attributes;
|
|
570
963
|
|
|
571
964
|
/** Component `c` of a vertex, as the reader's `Array.from(subarray(...))[c]` would see it. */
|
|
@@ -621,6 +1014,7 @@ function plainVertices(mesh: SerializedMesh): PlainVertices {
|
|
|
621
1014
|
hasNormal,
|
|
622
1015
|
uv,
|
|
623
1016
|
color,
|
|
1017
|
+
ids: new Int32Array(count).fill(-1),
|
|
624
1018
|
valid(vertex: number) {
|
|
625
1019
|
if (!(vertex >= 0 && vertex < count)) return false;
|
|
626
1020
|
|