@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/dist/geometry.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { clipToWorld, transform4, worldToClip } from "./camera.js";
2
- import { isMaterialVisible, MATERIAL_KIND, MATERIAL_UNLIT_LIGHT, materialProperties, NO_PLACEMENT, NO_PROGRAM, } from "./materialLayout.js";
2
+ import { isMaterialVisible, MATERIAL_KIND, MATERIAL_UNLIT_LIGHT, MATERIAL_VOLUME, materialProperties, NO_PLACEMENT, NO_PROGRAM, } from "./materialLayout.js";
3
+ import { geometryKey, INSTANCE_MIN_TRIANGLES, instanceable, transformBox, trianglesBox, } from "./instancing.js";
3
4
  import { normalMatrix, transformNormal, transformPoint } from "./math.js";
4
5
  import { matrixValue, sampleTexture } from "./upstream.js";
5
6
  /** Three's wireframe and line width: WebGL draws every line one pixel wide whatever the material asks. */
@@ -7,6 +8,27 @@ const LINE_WIDTH = 1;
7
8
  /** A dashed line split finer than this is drawn solid rather than as millions of dashes. */
8
9
  const MAX_DASHES = 4096;
9
10
  /** Grows a float array as triangles are added, since how many a primitive makes is only known as it is built. */
11
+ class UintStream {
12
+ data;
13
+ length = 0;
14
+ constructor(capacity) {
15
+ this.data = new Uint32Array(Math.max(16, capacity));
16
+ }
17
+ reserve(count) {
18
+ if (this.length + count <= this.data.length)
19
+ return;
20
+ const next = new Uint32Array(Math.max(this.data.length * 2, this.length + count));
21
+ next.set(this.data.subarray(0, this.length));
22
+ this.data = next;
23
+ }
24
+ push(value) {
25
+ this.reserve(1);
26
+ this.data[this.length++] = value;
27
+ }
28
+ view() {
29
+ return this.data.subarray(0, this.length);
30
+ }
31
+ }
10
32
  class FloatStream {
11
33
  data;
12
34
  length = 0;
@@ -100,15 +122,53 @@ function flatten(value) {
100
122
  return [...value.matrix];
101
123
  return null;
102
124
  }
125
+ /** Triangles one slice of packing covers before it may hand the thread back. */
126
+ const SLICE_TRIANGLES = 32768;
127
+ const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
128
+ /** A mesh's groups, or the one group covering it when it declares none. */
129
+ function groupsOf(mesh) {
130
+ return mesh.groups.length
131
+ ? mesh.groups
132
+ : [{
133
+ start: 0,
134
+ count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
135
+ materialIndex: 0,
136
+ }];
137
+ }
103
138
  /**
104
139
  * Flattens the scene into triangles, materials resolved and transforms applied. Every triangle carries its own three
105
- * vertices: the path tracer reads a hit through its triangle index, and sharing vertices would cost an indirection on
106
- * the hottest path of the kernel for memory that low poly scenes do not miss.
140
+ * positions: the traversal reads them straight off the buffer, and sharing them would cost an indirection on the
141
+ * hottest path of the kernel. What shading reads (normals, UVs, colours) is kept per vertex instead, since it is read
142
+ * once a hit, and a mesh that repeats becomes instances of one prototype (see `instancing.ts`).
107
143
  *
108
144
  * Points, lines and sprites become triangles that face the camera, sized the way Three draws them, so a path has a
109
145
  * surface to find; vertex shaders run here, on the CPU renderer's interpreter, and their varyings ride along per corner.
110
146
  */
111
147
  export function packGeometry(scene, options = {}) {
148
+ const steps = packingSteps(scene, options);
149
+ let step = steps.next();
150
+ while (!step.done)
151
+ step = steps.next();
152
+ return step.value;
153
+ }
154
+ /**
155
+ * The same packing, handing the thread back between slices: `pause` is awaited whenever the work has held the thread
156
+ * for `sliceMs`, so a large scene does not freeze the page while it packs.
157
+ */
158
+ export async function packGeometrySliced(scene, options, pause, sliceMs = 12) {
159
+ const steps = packingSteps(scene, options);
160
+ let started = performance.now();
161
+ let step = steps.next();
162
+ while (!step.done) {
163
+ if (performance.now() - started >= sliceMs) {
164
+ await pause();
165
+ started = performance.now();
166
+ }
167
+ step = steps.next();
168
+ }
169
+ return step.value;
170
+ }
171
+ function* packingSteps(scene, options) {
112
172
  const settings = options instanceof Map ? { textures: options } : options;
113
173
  const textures = settings.textures ?? new Map();
114
174
  const textureData = settings.textureData ?? new Map();
@@ -118,18 +178,38 @@ export function packGeometry(scene, options = {}) {
118
178
  const programs = settings.programs;
119
179
  const extraBase = settings.extraBase ?? 0;
120
180
  const meshes = scene.meshes.filter(mesh => mesh.attributes.position);
121
- const estimate = meshes.reduce((count, mesh) => count + (mesh.primitive ? 0 : triangleCountOf(mesh)), 0);
181
+ const repeated = settings.instancing === false ? new Map()
182
+ : repeatedGeometry(meshes, material => plainMaterial(material, textures, programs));
183
+ const estimate = meshes.reduce((count, mesh) => count + (mesh.primitive || repeated.has(mesh) ? 0 : triangleCountOf(mesh)), 0);
184
+ const vertexEstimate = meshes.reduce((count, mesh) => count + (mesh.primitive || repeated.has(mesh) ? 0 : mesh.attributes.position.data.length / 3), 0);
122
185
  const anyVertexColors = meshes.some(mesh => mesh.attributes.color && mesh.materials.some(m => m?.values?.vertexColors === true));
123
186
  const positions = new FloatStream(estimate * 9);
124
- const normals = new FloatStream(estimate * 9);
125
- const uvs = new FloatStream(estimate * 12);
126
- const colors = new FloatStream(anyVertexColors ? estimate * 12 : 0);
187
+ const corners = new UintStream(estimate * 3);
188
+ const normals = new FloatStream(vertexEstimate * 3);
189
+ const uvs = new FloatStream(vertexEstimate * 4);
190
+ const colors = new FloatStream(anyVertexColors ? vertexEstimate * 4 : 0);
191
+ let vertexCount = 0;
127
192
  const varyings = new FloatStream(0);
128
- const triangleMaterials = [];
193
+ const triangleMaterials = new UintStream(estimate);
129
194
  const entries = [];
130
195
  const shared = new Map();
131
196
  const extra = [];
132
197
  let cameraDependent = false;
198
+ // Repeated geometry: each prototype's triangles once, in object space, with the group each one came from.
199
+ const prototypePositions = new FloatStream(0);
200
+ const prototypeCorners = new UintStream(0);
201
+ const prototypeGroups = new UintStream(0);
202
+ const prototypes = [];
203
+ const prototypeOf = new Map();
204
+ const instances = [];
205
+ const instanceMaterials = new UintStream(0);
206
+ let instanced = 0;
207
+ const flatSink = { positions, corners, tags: triangleMaterials };
208
+ const prototypeSink = {
209
+ positions: prototypePositions,
210
+ corners: prototypeCorners,
211
+ tags: prototypeGroups,
212
+ };
133
213
  /** A material's clipping planes, written once to the extra words. */
134
214
  const clipPlanes = new Map();
135
215
  const planesOf = (material) => {
@@ -199,21 +279,40 @@ export function packGeometry(scene, options = {}) {
199
279
  shared.set(material, entry);
200
280
  return entry;
201
281
  };
202
- const writeTriangle = (corners, entry) => {
282
+ /** A vertex of its own, for everything a hit reads per vertex; its number. */
283
+ const emitVertex = (normal, n, uv, u, color, c) => {
284
+ normals.reserve(3);
285
+ normals.data[normals.length++] = normal[n];
286
+ normals.data[normals.length++] = normal[n + 1];
287
+ normals.data[normals.length++] = normal[n + 2];
288
+ uvs.reserve(4);
289
+ uvs.data[uvs.length++] = uv[u];
290
+ uvs.data[uvs.length++] = uv[u + 1];
291
+ uvs.data[uvs.length++] = uv[u + 2];
292
+ uvs.data[uvs.length++] = uv[u + 3];
293
+ if (anyVertexColors) {
294
+ colors.reserve(4);
295
+ colors.data[colors.length++] = color[c];
296
+ colors.data[colors.length++] = color[c + 1];
297
+ colors.data[colors.length++] = color[c + 2];
298
+ colors.data[colors.length++] = color[c + 3];
299
+ }
300
+ return vertexCount++;
301
+ };
302
+ const writeTriangle = (triangle, entry) => {
203
303
  // Geometry without normals is shaded flat: the three corners share the face normal.
204
- const flat = corners.some(corner => corner.normal.length < 3)
205
- ? faceNormal(corners[0].position, corners[1].position, corners[2].position)
304
+ const flat = triangle.some(corner => corner.normal.length < 3)
305
+ ? faceNormal(triangle[0].position, triangle[1].position, triangle[2].position)
206
306
  : null;
207
- for (const corner of corners) {
307
+ for (const corner of triangle) {
208
308
  positions.push(corner.position, 3);
209
- normals.push(flat ?? corner.normal, 3);
210
- uvs.push([corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 4);
211
- if (anyVertexColors)
212
- colors.push(corner.color, 4);
309
+ const color = corner.color;
310
+ // A missing component reads as zero, as the stream it replaces padded it.
311
+ corners.push(emitVertex(flat ?? corner.normal, 0, [corner.uv0[0], corner.uv0[1], corner.uv1[0], corner.uv1[1]], 0, [color[0] ?? 0, color[1] ?? 0, color[2] ?? 0, color[3] ?? 0], 0));
213
312
  }
214
313
  const stride = entry.placement.varyingStride;
215
314
  if (stride > 0)
216
- for (const corner of corners) {
315
+ for (const corner of triangle) {
217
316
  if (corner.varyings)
218
317
  varyings.push(corner.varyings, stride);
219
318
  else
@@ -221,152 +320,211 @@ export function packGeometry(scene, options = {}) {
221
320
  }
222
321
  triangleMaterials.push(entry.index);
223
322
  };
224
- /** Every vertex of a plain mesh, transformed once and shared by its groups. */
225
- let plain = null;
226
- const writePlainTriangles = (mesh, start, end, mirrored, entry) => {
227
- if (plain?.mesh !== mesh)
228
- plain = plainVertices(mesh);
229
- const { world, normal, hasNormal, uv, color, valid } = plain;
230
- const index = mesh.index;
231
- const stride = entry.placement.varyingStride;
232
- const corners = [0, 0, 0];
323
+ const writePlainTriangles = (vertices, start, end, mirrored, stride, sink, tag) => {
324
+ const { world, normal, hasNormal, uv, color, valid, ids } = vertices;
325
+ const index = vertices.mesh.index;
326
+ const triangle = [0, 0, 0];
233
327
  for (let v = start; v + 2 < end; v += 3) {
234
328
  const a = index ? index[v] : v, b = index ? index[v + 1] : v + 1, c = index ? index[v + 2] : v + 2;
235
329
  if (!valid(a) || !valid(b) || !valid(c))
236
330
  continue;
237
- corners[0] = a;
238
- corners[1] = mirrored ? c : b;
239
- corners[2] = mirrored ? b : c;
240
- // Geometry without normals is shaded flat: the three corners share the face normal.
331
+ triangle[0] = a;
332
+ triangle[1] = mirrored ? c : b;
333
+ triangle[2] = mirrored ? b : c;
334
+ // Geometry without normals is shaded flat: the three corners share the face normal, so a vertex of their own.
241
335
  const flat = !hasNormal[a] || !hasNormal[b] || !hasNormal[c]
242
- ? faceNormal(Array.from(world.subarray(corners[0] * 3, corners[0] * 3 + 3)), Array.from(world.subarray(corners[1] * 3, corners[1] * 3 + 3)), Array.from(world.subarray(corners[2] * 3, corners[2] * 3 + 3)))
336
+ ? faceNormal(Array.from(world.subarray(triangle[0] * 3, triangle[0] * 3 + 3)), Array.from(world.subarray(triangle[1] * 3, triangle[1] * 3 + 3)), Array.from(world.subarray(triangle[2] * 3, triangle[2] * 3 + 3)))
243
337
  : null;
244
- positions.reserve(9);
245
- normals.reserve(9);
246
- uvs.reserve(12);
247
- if (anyVertexColors)
248
- colors.reserve(12);
249
- for (const corner of corners) {
250
- const p = corner * 3, q = corner * 4;
251
- positions.data[positions.length++] = world[p];
252
- positions.data[positions.length++] = world[p + 1];
253
- positions.data[positions.length++] = world[p + 2];
338
+ sink.positions.reserve(9);
339
+ sink.corners.reserve(3);
340
+ for (const corner of triangle) {
341
+ const p = corner * 3;
342
+ sink.positions.data[sink.positions.length++] = world[p];
343
+ sink.positions.data[sink.positions.length++] = world[p + 1];
344
+ sink.positions.data[sink.positions.length++] = world[p + 2];
345
+ let id;
254
346
  if (flat) {
255
- normals.data[normals.length++] = flat[0];
256
- normals.data[normals.length++] = flat[1];
257
- normals.data[normals.length++] = flat[2];
347
+ id = emitVertex(flat, 0, uv, corner * 4, color, corner * 4);
258
348
  }
259
349
  else {
260
- normals.data[normals.length++] = normal[p];
261
- normals.data[normals.length++] = normal[p + 1];
262
- normals.data[normals.length++] = normal[p + 2];
263
- }
264
- uvs.data[uvs.length++] = uv[q];
265
- uvs.data[uvs.length++] = uv[q + 1];
266
- uvs.data[uvs.length++] = uv[q + 2];
267
- uvs.data[uvs.length++] = uv[q + 3];
268
- if (anyVertexColors) {
269
- colors.data[colors.length++] = color[q];
270
- colors.data[colors.length++] = color[q + 1];
271
- colors.data[colors.length++] = color[q + 2];
272
- colors.data[colors.length++] = color[q + 3];
350
+ id = ids[corner];
351
+ if (id < 0)
352
+ id = ids[corner] = emitVertex(normal, p, uv, corner * 4, color, corner * 4);
273
353
  }
354
+ sink.corners.data[sink.corners.length++] = id;
274
355
  }
275
356
  if (stride > 0)
276
357
  varyings.pad(3 * stride);
277
- triangleMaterials.push(entry.index);
358
+ sink.tags.push(tag);
278
359
  }
279
360
  };
280
- for (const mesh of meshes) {
281
- const groups = mesh.groups.length
282
- ? mesh.groups
283
- : [{
284
- start: 0,
285
- count: mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3,
286
- materialIndex: 0,
287
- }];
288
- if (mesh.primitive)
289
- cameraDependent = true;
290
- for (const group of groups) {
291
- const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
292
- if (!isMaterialVisible(material))
293
- continue;
294
- const entry = entryFor(material, mesh);
295
- const vertexShader = material ? programs?.vertex(material) ?? null : null;
296
- if (vertexShader)
361
+ // Time spent on the mesh at hand, without the pauses between slices.
362
+ let meshMs = 0;
363
+ let segment = performance.now();
364
+ const pause = function* () {
365
+ meshMs += performance.now() - segment;
366
+ yield;
367
+ segment = performance.now();
368
+ };
369
+ /** A plain run of triangles, sliced so the page is not held for the whole of a large mesh. */
370
+ const plainRun = function* (vertices, start, end, mirrored, stride, sink, tag) {
371
+ for (let from = start; from < end; from += SLICE_TRIANGLES * 3) {
372
+ writePlainTriangles(vertices, from, Math.min(end, from + SLICE_TRIANGLES * 3), mirrored, stride, sink, tag);
373
+ if (from + SLICE_TRIANGLES * 3 < end)
374
+ yield* pause();
375
+ }
376
+ };
377
+ for (let meshIndex = 0; meshIndex < meshes.length; meshIndex++) {
378
+ const mesh = meshes[meshIndex];
379
+ const groups = groupsOf(mesh);
380
+ const key = repeated.get(mesh);
381
+ const before = triangleMaterials.length + instanced;
382
+ let path = "plain";
383
+ meshMs = 0;
384
+ segment = performance.now();
385
+ if (key !== undefined) {
386
+ let prototype = prototypeOf.get(key);
387
+ // The first copy lays the prototype down, in object space; the next ones only point at it.
388
+ if (prototype === undefined) {
389
+ const first = prototypeGroups.length;
390
+ const vertices = plainVertices(mesh, IDENTITY);
391
+ for (let g = 0; g < groups.length; g++) {
392
+ const group = groups[g];
393
+ const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
394
+ if (!isMaterialVisible(material))
395
+ continue;
396
+ const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
397
+ yield* plainRun(vertices, Math.max(0, group.start), Math.min(group.start + group.count, count), false, 0, prototypeSink, g);
398
+ }
399
+ prototype = prototypes.length;
400
+ prototypes.push({ first, count: prototypeGroups.length - first });
401
+ prototypeOf.set(key, prototype);
402
+ path = "prototype";
403
+ }
404
+ else {
405
+ path = "instance";
406
+ }
407
+ const { first, count } = prototypes[prototype];
408
+ if (count > 0) {
409
+ const entryOfGroup = groups.map(group => {
410
+ const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
411
+ return isMaterialVisible(material) ? entryFor(material, mesh).index : 0;
412
+ });
413
+ instances.push({ prototype, matrix: mesh.matrixWorld, firstTriangle: instanced });
414
+ instanceMaterials.reserve(count);
415
+ for (let i = 0; i < count; i++)
416
+ instanceMaterials.data[instanceMaterials.length++] = entryOfGroup[prototypeGroups.data[first + i]];
417
+ instanced += count;
418
+ }
419
+ }
420
+ else {
421
+ if (mesh.primitive)
297
422
  cameraDependent = true;
298
- // Primitives that face the camera need one to face; without it there is nothing sensible to build.
299
- if (mesh.primitive && !camera)
300
- continue;
301
- const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
302
- const vertices = new VertexReader(mesh, material, context);
303
- const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
304
- const start = Math.max(0, group.start);
305
- const end = Math.min(group.start + group.count, count);
306
- const at = (offset) => (mesh.index ? mesh.index[offset] : offset);
307
- if (!mesh.primitive) {
308
- // A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
309
- const mirrored = determinant3(mesh.matrixWorld) < 0;
310
- // Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once per
311
- // instance, reading the per-instance attributes at that instance.
312
- const instancing = vertexShader ? instancedAttributes(mesh) : null;
313
- // The common case, a plain mesh, skips the general reader: each vertex is transformed once, straight into
314
- // typed arrays, and each triangle is copied from them. What it writes is exactly what the reader would.
315
- if (!vertexShader && !material?.maps?.displacementMap) {
316
- writePlainTriangles(mesh, start, end, mirrored, entry);
423
+ // Every vertex of a plain mesh, transformed once and shared by its groups.
424
+ let plain = null;
425
+ for (const group of groups) {
426
+ const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
427
+ if (!isMaterialVisible(material))
428
+ continue;
429
+ const entry = entryFor(material, mesh);
430
+ const vertexShader = material ? programs?.vertex(material) ?? null : null;
431
+ if (vertexShader)
432
+ cameraDependent = true;
433
+ // Primitives that face the camera need one to face; without it there is nothing sensible to build.
434
+ if (mesh.primitive && !camera)
435
+ continue;
436
+ const context = { camera, width, height, textureData, vertexShader, programs, fog: settings.fog };
437
+ const count = mesh.index ? mesh.index.length : mesh.attributes.position.data.length / 3;
438
+ const start = Math.max(0, group.start);
439
+ const end = Math.min(group.start + group.count, count);
440
+ const at = (offset) => (mesh.index ? mesh.index[offset] : offset);
441
+ if (!mesh.primitive) {
442
+ // A mirroring transform turns the winding around; Three flips its front face to match, and so does this.
443
+ const mirrored = determinant3(mesh.matrixWorld) < 0;
444
+ // The common case, a plain mesh, skips the general reader: each vertex is transformed once, straight into
445
+ // typed arrays, and each triangle is copied from them. What it writes is exactly what the reader would.
446
+ if (!vertexShader && !material?.maps?.displacementMap) {
447
+ plain ??= plainVertices(mesh);
448
+ yield* plainRun(plain, start, end, mirrored, entry.placement.varyingStride, flatSink, entry.index);
449
+ continue;
450
+ }
451
+ path = "general";
452
+ // Instanced geometry drawn by a shader (Line2, particles built in a vertex shader): the template runs once
453
+ // per instance, reading the per-instance attributes at that instance.
454
+ const instancing = vertexShader ? instancedAttributes(mesh) : null;
455
+ for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
456
+ const reader = new VertexReader(mesh, material, context, instancing?.names, instance);
457
+ for (let v = start; v + 2 < end; v += 3) {
458
+ const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
459
+ if (a && b && c)
460
+ writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
461
+ if ((v - start) % (SLICE_TRIANGLES * 3) === 0 && v > start)
462
+ yield* pause();
463
+ }
464
+ }
317
465
  continue;
318
466
  }
319
- for (let instance = 0; instance < (instancing?.count ?? 1); instance++) {
320
- const reader = instancing ? new VertexReader(mesh, material, context, instancing.names, instance) : vertices;
467
+ path = "general";
468
+ const vertices = new VertexReader(mesh, material, context);
469
+ if (mesh.primitive === "points") {
470
+ for (let v = start; v < end; v++)
471
+ for (const triangle of vertices.point(at(v)))
472
+ writeTriangle(triangle, entry);
473
+ continue;
474
+ }
475
+ if (mesh.primitive === "sprite") {
321
476
  for (let v = start; v + 2 < end; v += 3) {
322
- const a = reader.corner(at(v)), b = reader.corner(at(v + 1)), c = reader.corner(at(v + 2));
323
- if (!a || !b || !c)
324
- continue;
325
- writeTriangle(mirrored ? [a, c, b] : [a, b, c], entry);
477
+ const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
478
+ if (a && b && c)
479
+ writeTriangle([a, b, c], entry);
326
480
  }
481
+ continue;
327
482
  }
328
- continue;
329
- }
330
- if (mesh.primitive === "points") {
331
- for (let v = start; v < end; v++)
332
- for (const triangle of vertices.point(at(v)))
333
- writeTriangle(triangle, entry);
334
- continue;
335
- }
336
- if (mesh.primitive === "sprite") {
337
- for (let v = start; v + 2 < end; v += 3) {
338
- const a = vertices.corner(at(v)), b = vertices.corner(at(v + 1)), c = vertices.corner(at(v + 2));
339
- if (a && b && c)
340
- writeTriangle([a, b, c], entry);
483
+ // Lines: segments, a strip, or a strip that closes on itself.
484
+ const segments = [];
485
+ if (mesh.primitive === "lineSegments") {
486
+ for (let v = start; v + 1 < end; v += 2)
487
+ segments.push([at(v), at(v + 1)]);
341
488
  }
342
- continue;
343
- }
344
- // Lines: segments, a strip, or a strip that closes on itself.
345
- const segments = [];
346
- if (mesh.primitive === "lineSegments") {
347
- for (let v = start; v + 1 < end; v += 2)
348
- segments.push([at(v), at(v + 1)]);
349
- }
350
- else {
351
- for (let v = start; v + 1 < end; v++)
352
- segments.push([at(v), at(v + 1)]);
353
- if (mesh.primitive === "lineLoop" && end - start > 2)
354
- segments.push([at(end - 1), at(start)]);
489
+ else {
490
+ for (let v = start; v + 1 < end; v++)
491
+ segments.push([at(v), at(v + 1)]);
492
+ if (mesh.primitive === "lineLoop" && end - start > 2)
493
+ segments.push([at(end - 1), at(start)]);
494
+ }
495
+ for (const [a, b] of segments)
496
+ for (const triangle of vertices.segment(a, b))
497
+ writeTriangle(triangle, entry);
355
498
  }
356
- for (const [a, b] of segments)
357
- for (const triangle of vertices.segment(a, b))
358
- writeTriangle(triangle, entry);
359
499
  }
500
+ meshMs += performance.now() - segment;
501
+ settings.onMesh?.({
502
+ mesh: meshIndex,
503
+ path,
504
+ triangles: triangleMaterials.length + instanced - before,
505
+ ms: meshMs,
506
+ });
507
+ yield* pause();
360
508
  }
361
509
  const properties = entries.map(entry => entry.properties);
510
+ const sceneTriangles = triangleMaterials.length;
511
+ const prototypeTriangles = prototypeGroups.length;
512
+ const allCorners = new Uint32Array((sceneTriangles + prototypeTriangles) * 3);
513
+ allCorners.set(corners.view(), 0);
514
+ allCorners.set(prototypeCorners.view(), sceneTriangles * 3);
362
515
  return {
363
516
  positions: positions.view(),
517
+ corners: allCorners,
364
518
  normals: normals.view(),
365
519
  uvs: uvs.view(),
366
520
  colors: colors.view(),
521
+ vertexCount,
367
522
  varyings: varyings.view(),
368
- materials: new Uint32Array(triangleMaterials),
369
- triangleCount: triangleMaterials.length,
523
+ materials: triangleMaterials.view(),
524
+ triangleCount: sceneTriangles,
525
+ instancing: instances.length
526
+ ? packedInstancing(prototypePositions.view(), prototypes, instances, sceneTriangles, instanceMaterials.view())
527
+ : null,
370
528
  materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
371
529
  // Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs. An unlit
372
530
  // material of a fixed colour emits that colour, so it is one too.
@@ -380,7 +538,123 @@ export function packGeometry(scene, options = {}) {
380
538
  cameraDependent,
381
539
  };
382
540
  }
383
- function plainVertices(mesh) {
541
+ /** The instances, numbered after the scene's own triangles, with their prototypes and the box around them all. */
542
+ function packedInstancing(positions, prototypes, instances, sceneTriangles, materials) {
543
+ const boxes = prototypes.map(prototype => trianglesBox(positions, prototype.first, prototype.count));
544
+ const low = [Infinity, Infinity, Infinity], high = [-Infinity, -Infinity, -Infinity];
545
+ for (const instance of instances) {
546
+ const box = transformBox(instance.matrix, boxes[instance.prototype].low, boxes[instance.prototype].high);
547
+ for (let axis = 0; axis < 3; axis++) {
548
+ low[axis] = Math.min(low[axis], box.low[axis]);
549
+ high[axis] = Math.max(high[axis], box.high[axis]);
550
+ }
551
+ }
552
+ return {
553
+ positions,
554
+ triangleCount: positions.length / 9,
555
+ prototypes,
556
+ instances: instances.map(instance => ({ ...instance, firstTriangle: sceneTriangles + instance.firstTriangle })),
557
+ materials,
558
+ bounds: { low, high },
559
+ };
560
+ }
561
+ /**
562
+ * Whether a material can be shared by instances: nothing about it is read triangle by triangle on the CPU (lights,
563
+ * windows, volumes, grass) or tied to one mesh (shaders, object space normal maps, displacement).
564
+ */
565
+ function plainMaterial(material, textures, programs) {
566
+ if (!material)
567
+ return true;
568
+ if (programs?.fragment(material) || programs?.vertex(material))
569
+ return false;
570
+ if (material.values?.normalMapType === 1 || material.maps?.displacementMap)
571
+ return false;
572
+ const p = materialProperties(material, textures);
573
+ if (p.kind === MATERIAL_KIND.shader || (p.flags & (MATERIAL_UNLIT_LIGHT | MATERIAL_VOLUME)) !== 0)
574
+ return false;
575
+ if ((p.emissive[0] + p.emissive[1] + p.emissive[2]) * p.emissiveIntensity > 0)
576
+ return false;
577
+ if (p.transmission > 0 && p.thickness <= 0)
578
+ return false;
579
+ return !p.grass;
580
+ }
581
+ /**
582
+ * The meshes whose geometry another mesh repeats, each with the key its copies share. Meshes are first set apart by
583
+ * their sizes, which is free, and only those that still have a match are hashed.
584
+ */
585
+ function repeatedGeometry(meshes, plain) {
586
+ const plainOf = new Map();
587
+ const isPlain = (material) => {
588
+ let known = plainOf.get(material);
589
+ if (known === undefined)
590
+ plainOf.set(material, known = plain(material));
591
+ return known;
592
+ };
593
+ const bySize = new Map();
594
+ for (const mesh of meshes) {
595
+ if (mesh.primitive || !instanceable(mesh.matrixWorld) || triangleCountOf(mesh) < INSTANCE_MIN_TRIANGLES)
596
+ continue;
597
+ let visible = "";
598
+ let usable = true;
599
+ for (const group of groupsOf(mesh)) {
600
+ const material = mesh.materials[group.materialIndex] ?? mesh.materials[0];
601
+ const shown = isMaterialVisible(material);
602
+ visible += shown ? "1" : "0";
603
+ if (shown && !isPlain(material))
604
+ usable = false;
605
+ }
606
+ if (!usable)
607
+ continue;
608
+ const size = `${mesh.attributes.position.data.length}|${mesh.index?.length ?? -1}|${mesh.groups.length}|${visible}`;
609
+ const bucket = bySize.get(size);
610
+ if (bucket)
611
+ bucket.push({ mesh, visible });
612
+ else
613
+ bySize.set(size, [{ mesh, visible }]);
614
+ }
615
+ const byKey = new Map();
616
+ for (const bucket of bySize.values()) {
617
+ if (bucket.length < 2)
618
+ continue;
619
+ for (const { mesh, visible } of bucket) {
620
+ const key = geometryKey(mesh, visible);
621
+ const same = byKey.get(key);
622
+ if (same)
623
+ same.push(mesh);
624
+ else
625
+ byKey.set(key, [mesh]);
626
+ }
627
+ }
628
+ const repeated = new Map();
629
+ for (const [key, same] of byKey)
630
+ if (same.length > 1)
631
+ for (const mesh of same)
632
+ repeated.set(mesh, key);
633
+ return repeated;
634
+ }
635
+ /** The three vertex numbers of a stored triangle. */
636
+ export function triangleVertices(geometry, triangle) {
637
+ const at = triangle * 3;
638
+ return [geometry.corners[at], geometry.corners[at + 1], geometry.corners[at + 2]];
639
+ }
640
+ /** The normal at one corner of a stored triangle. */
641
+ export function cornerNormal(geometry, triangle, corner) {
642
+ const vertex = geometry.corners[triangle * 3 + corner];
643
+ return Array.from(geometry.normals.subarray(vertex * 3, vertex * 3 + 3));
644
+ }
645
+ /** The two UV sets at one corner of a stored triangle, four numbers. */
646
+ export function cornerUvs(geometry, triangle, corner) {
647
+ const vertex = geometry.corners[triangle * 3 + corner];
648
+ return Array.from(geometry.uvs.subarray(vertex * 4, vertex * 4 + 4));
649
+ }
650
+ /** The colour at one corner of a stored triangle, RGBA, or empty when the scene has no vertex colours. */
651
+ export function cornerColor(geometry, triangle, corner) {
652
+ if (!geometry.colors.length)
653
+ return [];
654
+ const vertex = geometry.corners[triangle * 3 + corner];
655
+ return Array.from(geometry.colors.subarray(vertex * 4, vertex * 4 + 4));
656
+ }
657
+ function plainVertices(mesh, matrixWorld = mesh.matrixWorld) {
384
658
  const position = mesh.attributes.position;
385
659
  const count = Math.floor(position.data.length / Math.max(1, position.itemSize));
386
660
  const world = new Float32Array(count * 3);
@@ -390,8 +664,8 @@ function plainVertices(mesh) {
390
664
  const color = new Float32Array(count * 4);
391
665
  // 0 not read yet, 1 usable, 2 refused.
392
666
  const state = new Uint8Array(count);
393
- const rotation = normalMatrix(mesh.matrixWorld);
394
- const matrix = mesh.matrixWorld;
667
+ const rotation = normalMatrix(matrixWorld);
668
+ const matrix = matrixWorld;
395
669
  const { normal: normals, uv: uv0, uv1, color: colors } = mesh.attributes;
396
670
  /** Component `c` of a vertex, as the reader's `Array.from(subarray(...))[c]` would see it. */
397
671
  const component = (attribute, vertex, c) => {
@@ -437,6 +711,7 @@ function plainVertices(mesh) {
437
711
  hasNormal,
438
712
  uv,
439
713
  color,
714
+ ids: new Int32Array(count).fill(-1),
440
715
  valid(vertex) {
441
716
  if (!(vertex >= 0 && vertex < count))
442
717
  return false;