@tinypoly/gpu-renderer 0.2.4 → 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.
Files changed (111) hide show
  1. package/ARCHITECTURE.md +48 -10
  2. package/README.md +2 -0
  3. package/dist/accumulator.d.ts.map +1 -1
  4. package/dist/accumulator.js +1 -0
  5. package/dist/accumulator.js.map +1 -1
  6. package/dist/denoise.js +2 -2
  7. package/dist/denoise.js.map +1 -1
  8. package/dist/device.d.ts.map +1 -1
  9. package/dist/device.js +9 -0
  10. package/dist/device.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +5 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts +48 -8
  15. package/dist/geometry.d.ts.map +1 -1
  16. package/dist/geometry.js +409 -134
  17. package/dist/geometry.js.map +1 -1
  18. package/dist/grass.d.ts.map +1 -1
  19. package/dist/grass.js +2 -1
  20. package/dist/grass.js.map +1 -1
  21. package/dist/imageResolve.js +2 -2
  22. package/dist/imageResolve.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/instancing.d.ts +100 -0
  28. package/dist/instancing.d.ts.map +1 -0
  29. package/dist/instancing.js +286 -0
  30. package/dist/instancing.js.map +1 -0
  31. package/dist/lensPass.d.ts +2 -0
  32. package/dist/lensPass.d.ts.map +1 -1
  33. package/dist/lensPass.js +7 -4
  34. package/dist/lensPass.js.map +1 -1
  35. package/dist/lights.d.ts.map +1 -1
  36. package/dist/lights.js +8 -5
  37. package/dist/lights.js.map +1 -1
  38. package/dist/memoryProbe.d.ts +76 -0
  39. package/dist/memoryProbe.d.ts.map +1 -0
  40. package/dist/memoryProbe.js +197 -0
  41. package/dist/memoryProbe.js.map +1 -0
  42. package/dist/oidnPass.d.ts +6 -0
  43. package/dist/oidnPass.d.ts.map +1 -1
  44. package/dist/oidnPass.js +18 -1
  45. package/dist/oidnPass.js.map +1 -1
  46. package/dist/pathTracePass.d.ts +15 -2
  47. package/dist/pathTracePass.d.ts.map +1 -1
  48. package/dist/pathTracePass.js +38 -16
  49. package/dist/pathTracePass.js.map +1 -1
  50. package/dist/renderer.d.ts +13 -0
  51. package/dist/renderer.d.ts.map +1 -1
  52. package/dist/renderer.js +248 -41
  53. package/dist/renderer.js.map +1 -1
  54. package/dist/sceneUpload.d.ts +26 -8
  55. package/dist/sceneUpload.d.ts.map +1 -1
  56. package/dist/sceneUpload.js +97 -27
  57. package/dist/sceneUpload.js.map +1 -1
  58. package/dist/shaderModule.d.ts.map +1 -1
  59. package/dist/shaderModule.js +2 -1
  60. package/dist/shaderModule.js.map +1 -1
  61. package/dist/skyBake.d.ts.map +1 -1
  62. package/dist/skyBake.js +5 -3
  63. package/dist/skyBake.js.map +1 -1
  64. package/dist/texturePack.d.ts +43 -3
  65. package/dist/texturePack.d.ts.map +1 -1
  66. package/dist/texturePack.js +209 -37
  67. package/dist/texturePack.js.map +1 -1
  68. package/dist/wgsl/bvh.wgsl.d.ts +5 -1
  69. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  70. package/dist/wgsl/bvh.wgsl.js +105 -15
  71. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  72. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  73. package/dist/wgsl/common.wgsl.js +92 -11
  74. package/dist/wgsl/common.wgsl.js.map +1 -1
  75. package/dist/wgsl/lens.wgsl.d.ts +1 -1
  76. package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
  77. package/dist/wgsl/lens.wgsl.js +14 -2
  78. package/dist/wgsl/lens.wgsl.js.map +1 -1
  79. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  80. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  81. package/dist/wgsl/surface.wgsl.js +24 -14
  82. package/dist/wgsl/surface.wgsl.js.map +1 -1
  83. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  84. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  85. package/dist/wgsl/texture.wgsl.js +11 -0
  86. package/dist/wgsl/texture.wgsl.js.map +1 -1
  87. package/package.json +1 -1
  88. package/src/accumulator.ts +1 -0
  89. package/src/denoise.ts +2 -2
  90. package/src/device.ts +12 -0
  91. package/src/frameLayout.ts +5 -0
  92. package/src/geometry.ts +527 -133
  93. package/src/grass.ts +2 -1
  94. package/src/imageResolve.ts +2 -2
  95. package/src/index.ts +1 -0
  96. package/src/instancing.ts +415 -0
  97. package/src/lensPass.ts +9 -4
  98. package/src/lights.ts +9 -5
  99. package/src/memoryProbe.ts +275 -0
  100. package/src/oidnPass.ts +21 -1
  101. package/src/pathTracePass.ts +42 -18
  102. package/src/renderer.ts +308 -47
  103. package/src/sceneUpload.ts +139 -36
  104. package/src/shaderModule.ts +2 -1
  105. package/src/skyBake.ts +5 -3
  106. package/src/texturePack.ts +274 -43
  107. package/src/wgsl/bvh.wgsl.ts +105 -15
  108. package/src/wgsl/common.wgsl.ts +92 -11
  109. package/src/wgsl/lens.wgsl.ts +14 -2
  110. package/src/wgsl/surface.wgsl.ts +24 -14
  111. 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
- /** The flat, GPU ready form of a scene's triangles. Positions and normals are world space, three vertices per entry. */
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
- /** Nine normal components per triangle, one per vertex. */
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
- /** Twelve per triangle: two UV sets for each of the three vertices, which is what Three's map channels need. */
43
+ /** Four per vertex: two UV sets, which is what Three's map channels need. */
24
44
  uvs: Float32Array;
25
- /** Twelve per triangle, RGBA per vertex, when any material reads vertex colours; empty otherwise. */
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
- * vertices: the path tracer reads a hit through its triangle index, and sharing vertices would cost an indirection on
208
- * the hottest path of the kernel for memory that low poly scenes do not miss.
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
- const estimate = meshes.reduce((count, mesh) => count + (mesh.primitive ? 0 : triangleCountOf(mesh)), 0);
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 normals = new FloatStream(estimate * 9);
231
- const uvs = new FloatStream(estimate * 12);
232
- const colors = new FloatStream(anyVertexColors ? estimate * 12 : 0);
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: number[] = [];
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
- const writeTriangle = (corners: readonly Corner[], entry: Entry) => {
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 = corners.some(corner => corner.normal.length < 3)
333
- ? faceNormal(corners[0].position, corners[1].position, corners[2].position)
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 corners) {
501
+ for (const corner of triangle) {
337
502
  positions.push(corner.position, 3);
338
- normals.push(flat ?? corner.normal, 3);
339
- uvs.push([corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 4);
503
+ const color = corner.color;
340
504
 
341
- if (anyVertexColors) colors.push(corner.color, 4);
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 corners) {
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
- /** Every vertex of a plain mesh, transformed once and shared by its groups. */
356
- let plain: PlainVertices | null = null;
357
-
358
- const writePlainTriangles = (mesh: SerializedMesh, start: number, end: number, mirrored: boolean, entry: Entry) => {
359
- if (plain?.mesh !== mesh) plain = plainVertices(mesh);
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
- corners[0] = a;
372
- corners[1] = mirrored ? c : b;
373
- corners[2] = mirrored ? b : c;
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(corners[0] * 3, corners[0] * 3 + 3)),
379
- Array.from(world.subarray(corners[1] * 3, corners[1] * 3 + 3)),
380
- Array.from(world.subarray(corners[2] * 3, corners[2] * 3 + 3)))
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
- normals.reserve(9);
385
- uvs.reserve(12);
544
+ sink.positions.reserve(9);
545
+ sink.corners.reserve(3);
386
546
 
387
- if (anyVertexColors) colors.reserve(12);
547
+ for (const corner of triangle) {
548
+ const p = corner * 3;
388
549
 
389
- for (const corner of corners) {
390
- const p = corner * 3, q = corner * 4;
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
- positions.data[positions.length++] = world[p];
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
- normals.data[normals.length++] = flat[0];
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
- normals.data[normals.length++] = normal[p];
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
- if (anyVertexColors) {
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
- triangleMaterials.push(entry.index);
569
+ sink.tags.push(tag);
422
570
  }
423
571
  };
424
572
 
425
- for (const mesh of meshes) {
426
- const groups = mesh.groups.length
427
- ? mesh.groups
428
- : [{
429
- start: 0,
430
- count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
431
- materialIndex: 0,
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
- if (mesh.primitive) cameraDependent = true;
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
- for (const group of groups) {
437
- const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
589
+ if (from + SLICE_TRIANGLES * 3 < end) yield* pause();
590
+ }
591
+ };
438
592
 
439
- if (!isMaterialVisible(material)) continue;
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
- const entry = entryFor(material, mesh);
442
- const vertexShader = material ? programs?.vertex(material) ?? null : null;
600
+ meshMs = 0;
601
+ segment = performance.now();
443
602
 
444
- if (vertexShader) cameraDependent = true;
603
+ if (key !== undefined) {
604
+ let prototype = prototypeOf.get(key);
445
605
 
446
- // Primitives that face the camera need one to face; without it there is nothing sensible to build.
447
- if (mesh.primitive && !camera) continue;
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
- const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
450
- const vertices = new VertexReader(mesh, material, context);
451
- const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
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
- if (!mesh.primitive) {
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
- // The common case, a plain mesh, skips the general reader: each vertex is transformed once, straight into
464
- // typed arrays, and each triangle is copied from them. What it writes is exactly what the reader would.
465
- if (!vertexShader && !material?.maps?.displacementMap) {
466
- writePlainTriangles(mesh, start, end, mirrored, entry);
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
- for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
471
- const reader = instancing ? new VertexReader(mesh, material, context, instancing.names, instance) : vertices;
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
- for (let v = start; v + 2 < end; v += 3) {
474
- const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
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
- if (!a || !b || !c) continue;
685
+ path = "general";
477
686
 
478
- writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
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
- continue;
483
- }
706
+ path = "general";
484
707
 
485
- if (mesh.primitive === "points") {
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
- continue;
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
- if (mesh.primitive === "sprite") {
493
- for (let v = start; v + 2 < end; v += 3) {
494
- const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
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
- if (a && b && c) writeTriangle([a, b, c], entry);
724
+ continue;
497
725
  }
498
726
 
499
- continue;
500
- }
727
+ // Lines: segments, a strip, or a strip that closes on itself.
728
+ const segments: [number, number][] = [];
501
729
 
502
- // Lines: segments, a strip, or a strip that closes on itself.
503
- const segments: [number, number][] = [];
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
- if (mesh.primitive === "lineSegments") {
506
- for (let v = start; v + 1 < end; v += 2) segments.push([at(v), at(v + 1)]);
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
- if (mesh.primitive === "lineLoop" && end - start > 2) segments.push([at(end - 1), at(start)]);
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: new Uint32Array(triangleMaterials),
527
- triangleCount: triangleMaterials.length,
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(mesh.matrixWorld);
568
- const matrix = mesh.matrixWorld as number[];
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