@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.
Files changed (101) 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/lights.d.ts.map +1 -1
  32. package/dist/lights.js +8 -5
  33. package/dist/lights.js.map +1 -1
  34. package/dist/memoryProbe.d.ts +76 -0
  35. package/dist/memoryProbe.d.ts.map +1 -0
  36. package/dist/memoryProbe.js +197 -0
  37. package/dist/memoryProbe.js.map +1 -0
  38. package/dist/oidnPass.d.ts +6 -0
  39. package/dist/oidnPass.d.ts.map +1 -1
  40. package/dist/oidnPass.js +18 -1
  41. package/dist/oidnPass.js.map +1 -1
  42. package/dist/pathTracePass.d.ts +15 -2
  43. package/dist/pathTracePass.d.ts.map +1 -1
  44. package/dist/pathTracePass.js +38 -16
  45. package/dist/pathTracePass.js.map +1 -1
  46. package/dist/renderer.d.ts +13 -0
  47. package/dist/renderer.d.ts.map +1 -1
  48. package/dist/renderer.js +248 -41
  49. package/dist/renderer.js.map +1 -1
  50. package/dist/sceneUpload.d.ts +26 -8
  51. package/dist/sceneUpload.d.ts.map +1 -1
  52. package/dist/sceneUpload.js +97 -27
  53. package/dist/sceneUpload.js.map +1 -1
  54. package/dist/shaderModule.d.ts.map +1 -1
  55. package/dist/shaderModule.js +2 -1
  56. package/dist/shaderModule.js.map +1 -1
  57. package/dist/skyBake.d.ts.map +1 -1
  58. package/dist/skyBake.js +5 -3
  59. package/dist/skyBake.js.map +1 -1
  60. package/dist/texturePack.d.ts +43 -3
  61. package/dist/texturePack.d.ts.map +1 -1
  62. package/dist/texturePack.js +209 -37
  63. package/dist/texturePack.js.map +1 -1
  64. package/dist/wgsl/bvh.wgsl.d.ts +5 -1
  65. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  66. package/dist/wgsl/bvh.wgsl.js +105 -15
  67. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  68. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  69. package/dist/wgsl/common.wgsl.js +92 -11
  70. package/dist/wgsl/common.wgsl.js.map +1 -1
  71. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  72. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  73. package/dist/wgsl/surface.wgsl.js +24 -14
  74. package/dist/wgsl/surface.wgsl.js.map +1 -1
  75. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  76. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  77. package/dist/wgsl/texture.wgsl.js +11 -0
  78. package/dist/wgsl/texture.wgsl.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/accumulator.ts +1 -0
  81. package/src/denoise.ts +2 -2
  82. package/src/device.ts +12 -0
  83. package/src/frameLayout.ts +5 -0
  84. package/src/geometry.ts +527 -133
  85. package/src/grass.ts +2 -1
  86. package/src/imageResolve.ts +2 -2
  87. package/src/index.ts +1 -0
  88. package/src/instancing.ts +415 -0
  89. package/src/lights.ts +9 -5
  90. package/src/memoryProbe.ts +275 -0
  91. package/src/oidnPass.ts +21 -1
  92. package/src/pathTracePass.ts +42 -18
  93. package/src/renderer.ts +308 -47
  94. package/src/sceneUpload.ts +139 -36
  95. package/src/shaderModule.ts +2 -1
  96. package/src/skyBake.ts +5 -3
  97. package/src/texturePack.ts +274 -43
  98. package/src/wgsl/bvh.wgsl.ts +105 -15
  99. package/src/wgsl/common.wgsl.ts +92 -11
  100. package/src/wgsl/surface.wgsl.ts +24 -14
  101. package/src/wgsl/texture.wgsl.ts +11 -0
@@ -1,12 +1,14 @@
1
- import type { SerializedScene, SerializedTexture } from "./upstream.js";
1
+ import type { ShaderPrograms } from "./shaderProgram.js";
2
+ import type { SerializedMaterial, SerializedScene, SerializedTexture } from "./upstream.js";
2
3
 
3
4
  /**
4
5
  * Every texture of a scene in one storage buffer, led by a table saying where each one lives. Core WebGPU has no binding
5
6
  * arrays and `texture_2d_array` wants one size and one format for every layer, which a Three.js scene does not provide,
6
7
  * so the texels are read by hand. See the decision in ARCHITECTURE.md.
7
8
  *
8
- * Byte textures keep their eight bits per channel, four to a word, and are decoded from sRGB as they are read. Float
9
- * textures keep all four, because their range is the reason they are float at all.
9
+ * Byte textures keep their eight bits per channel, four to a word, and are decoded from sRGB as they are read. A byte
10
+ * texture of which only one channel is ever read (a roughness or a bump map) keeps that channel alone, four texels to a
11
+ * word. Float textures keep all four, because their range is the reason they are float at all.
10
12
  */
11
13
  export interface PackedTextures {
12
14
  /**
@@ -97,6 +99,8 @@ const TEXTURE_NEAREST = 4;
97
99
  const TEXTURE_FLIP_Y = 8;
98
100
  /** An environment map whose mapping bends the view ray instead of mirroring it. */
99
101
  const TEXTURE_REFRACTION = 16;
102
+ /** One byte a texel: the only channel the scene reads of it, which the shader hands back in every channel. */
103
+ const TEXTURE_SINGLE = 32;
100
104
 
101
105
  /** Three's own wrap constants, which the shader compares against. */
102
106
  const REPEAT = 1000;
@@ -118,6 +122,139 @@ const isFloat = (texture: SerializedTexture) => texture.data instanceof Float32A
118
122
  */
119
123
  const isEncoded = (texture: SerializedTexture) => !isFloat(texture) && texture.colorSpace === "srgb";
120
124
 
125
+ /**
126
+ * The channels the kernel reads from each material map slot, as the WGSL reads them: roughness from green, metalness
127
+ * from blue, and so on. A slot missing here counts as reading all four.
128
+ */
129
+ export const SLOT_CHANNELS: Readonly<Record<string, string>> = {
130
+ map: "rgba", emissiveMap: "rgb", roughnessMap: "g", metalnessMap: "b", normalMap: "rgb", clearcoatNormalMap: "rgb",
131
+ alphaMap: "g", transmissionMap: "r", thicknessMap: "g", clearcoatMap: "r", clearcoatRoughnessMap: "g",
132
+ sheenColorMap: "rgb", sheenRoughnessMap: "a", iridescenceMap: "r", iridescenceThicknessMap: "g", anisotropyMap: "rgb",
133
+ specularMap: "r", specularIntensityMap: "a", specularColorMap: "rgb", bumpMap: "r", displacementMap: "r",
134
+ lightMap: "rgb", gradientMap: "r", matcap: "rgb", envMap: "rgb",
135
+ };
136
+
137
+ /** Every string inside a value, with the first path it was found at, without walking into typed arrays. */
138
+ function collectStrings(value: unknown, path: string, into: Map<string, string>, seen = new Set<unknown>()) {
139
+ if (typeof value === "string") {
140
+ if (!into.has(value)) into.set(value, path);
141
+
142
+ return;
143
+ }
144
+
145
+ if (!value || typeof value !== "object" || seen.has(value) || ArrayBuffer.isView(value)) return;
146
+
147
+ seen.add(value);
148
+
149
+ for (const [key, entry] of Object.entries(value)) collectStrings(entry, `${path}.${key}`, into, seen);
150
+ }
151
+
152
+ /** How each texture of a scene is stored: one channel, or all four and why. */
153
+ export interface TextureChannels {
154
+ /** The channel kept (0 for red to 3 for alpha) of each texture stored one byte a texel. */
155
+ single: Map<string, number>;
156
+ /** Why every other texture keeps all four. */
157
+ whole: Map<string, string>;
158
+ }
159
+
160
+ /** Texture indices no table reaches, one per texture, which a program's packed uniforms give back where it reads one. */
161
+ const PROBE = 0x7f000000;
162
+
163
+ /**
164
+ * The textures a material's translated programs read: their uniforms are packed with a probe index for every texture,
165
+ * and the probes that come back are the textures the programs declare. A texture a hook's uniforms name but its
166
+ * program never declares (Three hands a patched material's every map to its shader) is not read by it.
167
+ */
168
+ function programTextures(material: SerializedMaterial, programs: Pick<ShaderPrograms, "fragment" | "after">,
169
+ textures: readonly SerializedTexture[]): Set<string> {
170
+ const probes = new Map(textures.map((texture, index) => [texture.id, PROBE + index]));
171
+ const uniforms = { ...material.shader?.uniforms, ...material.time !== undefined ? { time: material.time } : {} };
172
+ const read = new Set<string>();
173
+
174
+ for (const program of [programs.fragment(material), programs.after(material)]) {
175
+ if (!program) continue;
176
+
177
+ for (const word of program.packUniforms(uniforms, probes))
178
+ if (word >= PROBE && word < PROBE + textures.length) read.add(textures[word - PROBE].id);
179
+ }
180
+
181
+ return read;
182
+ }
183
+
184
+ /**
185
+ * Which byte textures of a scene are read through one channel only: every reference to the texture is a material map
186
+ * slot, and every such slot reads the same channel. A texture named anywhere else (a light's projected map, the sky, a
187
+ * planar capture) is read in ways this cannot see, and keeps all four. With the scene's translated programs, a
188
+ * shader's uniforms count only for the textures its program reads; without them, any texture they name keeps all four.
189
+ */
190
+ export function textureChannels(scene: SerializedScene,
191
+ programs?: Pick<ShaderPrograms, "fragment" | "after">): TextureChannels {
192
+ const read = new Map<string, Set<string>>();
193
+ const elsewhere = new Map<string, string>();
194
+ const materials = new Set(scene.meshes.flatMap(mesh => mesh.materials));
195
+
196
+ for (const material of materials) {
197
+ if (!material) continue;
198
+
199
+ const { maps, ...everything } = material;
200
+ const name = `material "${material.name || material.type}"`;
201
+ // The shader's uniforms are read only through its translated programs, which say what they sample.
202
+ const { shader, ...rest } = everything;
203
+
204
+ if (programs && shader) {
205
+ for (const id of programTextures(material, programs, scene.textures ?? []))
206
+ if (!elsewhere.has(id)) elsewhere.set(id, `the shader of ${name}`);
207
+ } else if (shader) {
208
+ collectStrings(shader, `${name}.shader`, elsewhere);
209
+ }
210
+
211
+ for (const [slot, id] of Object.entries(maps ?? {})) {
212
+ if (typeof id !== "string") continue;
213
+
214
+ let channels = read.get(id);
215
+
216
+ if (!channels) read.set(id, channels = new Set());
217
+
218
+ for (const channel of SLOT_CHANNELS[slot] ?? "rgba") channels.add(channel);
219
+ }
220
+
221
+ collectStrings(rest, name, elsewhere);
222
+ }
223
+
224
+ // The rest of the scene: its lights, its sky and whatever else names a texture.
225
+ for (const [key, value] of Object.entries(scene))
226
+ if (key !== "meshes" && key !== "textures") collectStrings(value, key, elsewhere);
227
+
228
+ const single = new Map<string, number>();
229
+ const whole = new Map<string, string>();
230
+
231
+ for (const texture of scene.textures ?? []) {
232
+ const channels = read.get(texture.id);
233
+ const named = elsewhere.get(texture.id);
234
+
235
+ if (isFloat(texture)) whole.set(texture.id, "float");
236
+ else if (!channels) whole.set(texture.id, named ? `named by ${named}` : "not read by any map");
237
+ else if (channels.size !== 1) whole.set(texture.id, `reads ${[...channels].join("")}`);
238
+ else if (named) whole.set(texture.id, `named by ${named}`);
239
+ else single.set(texture.id, "rgba".indexOf([...channels][0]));
240
+ }
241
+
242
+ return { single, whole };
243
+ }
244
+
245
+ /** The byte textures of a scene read through one channel only, with that channel: see `textureChannels`. */
246
+ export function singleChannelTextures(scene: SerializedScene,
247
+ programs?: Pick<ShaderPrograms, "fragment" | "after">): Map<string, number> {
248
+ return textureChannels(scene, programs).single;
249
+ }
250
+
251
+ /** Words a texture's texels take in the buffer. */
252
+ function texelWords(texture: SerializedTexture, single: boolean): number {
253
+ const texels = texelCount(texture);
254
+
255
+ return isFloat(texture) ? texels * 4 : single ? Math.ceil(texels / 4) : texels;
256
+ }
257
+
121
258
  /**
122
259
  * Where everything will go, before anything is written: which index each texture gets and where the words after the
123
260
  * texels begin. Materials and lights point into that tail by absolute offset, so they need it before the buffer exists.
@@ -125,13 +262,16 @@ const isEncoded = (texture: SerializedTexture) => !isFloat(texture) && texture.c
125
262
  export interface TexturePlan {
126
263
  textures: SerializedTexture[];
127
264
  indexOf: Map<string, number>;
265
+ /** The channel kept of each texture stored one byte a texel, by id. */
266
+ channelOf: ReadonlyMap<string, number>;
128
267
  /** The lookup tables and the texture table together: where the texels begin. */
129
268
  tableWords: number;
130
269
  /** Where the words appended after the texels begin. */
131
270
  extraOffset: number;
132
271
  }
133
272
 
134
- export function planTextures(scene: SerializedScene, extraTextures: readonly SerializedTexture[] = []): TexturePlan {
273
+ export function planTextures(scene: SerializedScene, extraTextures: readonly SerializedTexture[] = [],
274
+ channelOf: ReadonlyMap<string, number> = singleChannelTextures(scene)): TexturePlan {
135
275
  const seen = new Set<string>();
136
276
 
137
277
  // A texture id appears once, whoever refers to it; the first copy wins.
@@ -141,40 +281,43 @@ export function planTextures(scene: SerializedScene, extraTextures: readonly Ser
141
281
 
142
282
  const indexOf = new Map(textures.map((texture, index) => [texture.id, index]));
143
283
  const tableWords = TEXTURE_TABLE + Math.max(1, textures.length) * TEXTURE_WORDS;
144
- const texelWords = textures.reduce((sum, texture) => sum + texelCount(texture) * (isFloat(texture) ? 4 : 1), 0);
284
+ const kept = new Map([...channelOf].filter(([id]) => indexOf.has(id)));
285
+ const words = textures.reduce((sum, texture) => sum + texelWords(texture, kept.has(texture.id)), 0);
145
286
 
146
- return { textures, indexOf, tableWords, extraOffset: tableWords + texelWords };
287
+ return { textures, indexOf, channelOf: kept, tableWords, extraOffset: tableWords + words };
147
288
  }
148
289
 
149
290
  export function packTextures(scene: SerializedScene, extras: TextureExtras = {}): PackedTextures {
150
291
  return writeTextures(planTextures(scene, extras.textures), extras.words ?? new Uint32Array(0));
151
292
  }
152
293
 
153
- /** The buffer a plan describes, with `extraWords` appended at its `extraOffset`. */
154
- export function writeTextures(plan: TexturePlan, extraWords: Uint32Array): PackedTextures {
155
- const { textures, indexOf, tableWords } = plan;
156
-
157
- // WebGPU rejects an empty binding, and a scene with no textures at all is ordinary.
158
- const data = new Uint32Array(Math.max(4, plan.extraOffset + extraWords.length));
159
- const words = data;
160
- const floats = new Float32Array(data.buffer);
161
- let texelAt = tableWords;
294
+ /**
295
+ * The lookup tables and the texture table a plan describes, written at the head of `words`. Returns where each
296
+ * texture's texels start, in words.
297
+ */
298
+ function writeTable(words: Uint32Array, plan: TexturePlan): number[] {
299
+ const floats = new Float32Array(words.buffer, words.byteOffset, words.length);
300
+ const offsets: number[] = [];
301
+ let texelAt = plan.tableWords;
162
302
 
163
- data.set(LOOKUP, 0);
303
+ words.set(LOOKUP, 0);
164
304
 
165
- textures.forEach((texture, index) => {
305
+ plan.textures.forEach((texture, index) => {
166
306
  const at = textureEntry(index);
167
- const texels = texelCount(texture);
168
307
  const float = isFloat(texture);
169
308
 
309
+ offsets.push(texelAt);
170
310
  words[at] = texture.width;
171
311
  words[at + 1] = texture.height;
172
312
  words[at + 2] = texelAt;
313
+
173
314
  words[at + 3] = (float ? TEXTURE_FLOAT : 0)
174
315
  | (isEncoded(texture) ? TEXTURE_SRGB : 0)
175
316
  | (texture.nearest ? TEXTURE_NEAREST : 0)
176
317
  | (texture.flipY ? TEXTURE_FLIP_Y : 0)
177
- | (texture.refraction ? TEXTURE_REFRACTION : 0);
318
+ | (texture.refraction ? TEXTURE_REFRACTION : 0)
319
+ | (plan.channelOf.has(texture.id) ? TEXTURE_SINGLE : 0);
320
+
178
321
  words[at + 4] = texture.wrapS;
179
322
  words[at + 5] = texture.wrapT;
180
323
  words[at + 6] = Math.min(Math.max(0, texture.channel | 0), UV_CHANNELS - 1);
@@ -185,35 +328,123 @@ export function writeTextures(plan: TexturePlan, extraWords: Uint32Array): Packe
185
328
 
186
329
  floats.set([matrix[0], matrix[3], matrix[6], 0], at + 8);
187
330
  floats.set([matrix[1], matrix[4], matrix[7], 0], at + 12);
331
+ texelAt += texelWords(texture, plan.channelOf.has(texture.id));
332
+ });
333
+
334
+ return offsets;
335
+ }
336
+
337
+ /**
338
+ * A texture's texels as the buffer holds them: viewed straight from the snapshot where the bytes already agree, the one
339
+ * channel kept copied out four texels to a word, or null for a byte texture a big endian page has to turn around.
340
+ */
341
+ function texelView(texture: SerializedTexture, channel: number | undefined): ArrayBufferView | null {
342
+ const texels = texelCount(texture);
343
+
344
+ if (isFloat(texture)) return (texture.data as Float32Array).subarray(0, texels * 4);
345
+
346
+ if (channel !== undefined) {
347
+ const source = texture.data as Uint8Array;
348
+ // Bytes in the order the device reads a word's, lowest first, so this needs no turning around on any page.
349
+ const kept = new Uint8Array(Math.ceil(texels / 4) * 4);
350
+
351
+ for (let texel = 0; texel < texels; texel++) kept[texel] = source[texel * 4 + channel];
352
+
353
+ return kept;
354
+ }
355
+
356
+ return LITTLE_ENDIAN ? (texture.data as Uint8Array).subarray(0, texels * 4) : null;
357
+ }
358
+
359
+ /** The buffer a plan describes, with `extraWords` appended at its `extraOffset`. */
360
+ export function writeTextures(plan: TexturePlan, extraWords: Uint32Array): PackedTextures {
361
+ const { textures, indexOf } = plan;
362
+ // WebGPU rejects an empty binding, and a scene with no textures at all is ordinary.
363
+ const data = new Uint32Array(Math.max(4, plan.extraOffset + extraWords.length));
364
+ const offsets = writeTable(data, plan);
365
+
366
+ textures.forEach((texture, index) => {
367
+ const texelAt = offsets[index];
368
+ const view = texelView(texture, plan.channelOf.get(texture.id));
369
+
370
+ if (view) {
371
+ // The channels keep their bits, and bytes already sit in the order the words want them: copied whole.
372
+ new Uint8Array(data.buffer, texelAt * 4, view.byteLength)
373
+ .set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
374
+
375
+ return;
376
+ }
188
377
 
189
- if (float) {
190
- // The channels keep their bits and the shader reads them back as floats.
191
- floats.set((texture.data as Float32Array).subarray(0, texels * 4), texelAt);
192
- texelAt += texels * 4;
193
- } else {
194
- const source = texture.data as Uint8Array;
195
-
196
- if (LITTLE_ENDIAN) {
197
- // The bytes already sit in the order the words want them: copied whole, as the GPU reads them.
198
- new Uint8Array(data.buffer, texelAt * 4, texels * 4).set(source.subarray(0, texels * 4));
199
- } else {
200
- for (let texel = 0; texel < texels; texel++) {
201
- const offset = texel * 4;
202
-
203
- words[texelAt + texel] = source[offset]
204
- | (source[offset + 1] << 8)
205
- | (source[offset + 2] << 16)
206
- | (source[offset + 3] << 24);
207
- }
208
- }
209
-
210
- texelAt += texels;
378
+ const source = texture.data as Uint8Array;
379
+
380
+ for (let texel = 0; texel < texelCount(texture); texel++) {
381
+ const offset = texel * 4;
382
+
383
+ data[texelAt + texel] = source[offset]
384
+ | (source[offset + 1] << 8)
385
+ | (source[offset + 2] << 16)
386
+ | (source[offset + 3] << 24);
211
387
  }
212
388
  });
213
389
 
214
- data.set(extraWords, texelAt);
390
+ data.set(extraWords, plan.extraOffset);
391
+
392
+ return { data, count: textures.length, indexOf, extraOffset: plan.extraOffset };
393
+ }
394
+
395
+ /**
396
+ * The same buffer `writeTextures` builds, written to the device a piece at a time: the tables, then each texture's
397
+ * texels straight from the snapshot, then the extra words. The page never holds the whole buffer, which for a scene of
398
+ * many large textures is hundreds of megabytes it would only hold to hand over.
399
+ */
400
+ export interface TextureUpload {
401
+ byteLength: number;
402
+ count: number;
403
+ indexOf: Map<string, number>;
404
+ extraOffset: number;
405
+ /** Where each texture's texels start, in words, by its index in the table. */
406
+ offsets: number[];
407
+ write(device: GPUDevice, buffer: GPUBuffer): void;
408
+ }
409
+
410
+ export function textureUpload(plan: TexturePlan, extraWords: Uint32Array): TextureUpload {
411
+ const byteLength = Math.max(4, plan.extraOffset + extraWords.length) * 4;
412
+
413
+ // A big endian page has to turn every byte texel around, which needs the whole buffer anyway.
414
+ if (!LITTLE_ENDIAN) {
415
+ const packed = writeTextures(plan, extraWords);
416
+
417
+ return {
418
+ byteLength,
419
+ count: packed.count,
420
+ indexOf: packed.indexOf,
421
+ extraOffset: packed.extraOffset,
422
+ offsets: plan.textures.map((_, index) => packed.data[textureEntry(index) + 2]),
423
+ write: (device, buffer) => device.queue.writeBuffer(buffer, 0, packed.data),
424
+ };
425
+ }
426
+
427
+ const header = new Uint32Array(plan.tableWords);
428
+ const offsets = writeTable(header, plan);
429
+
430
+ return {
431
+ byteLength,
432
+ count: plan.textures.length,
433
+ indexOf: plan.indexOf,
434
+ extraOffset: plan.extraOffset,
435
+ offsets,
436
+ write(device, buffer) {
437
+ device.queue.writeBuffer(buffer, 0, header);
438
+
439
+ plan.textures.forEach((texture, index) => {
440
+ const view = texelView(texture, plan.channelOf.get(texture.id)) as ArrayBufferView;
441
+
442
+ if (view.byteLength) device.queue.writeBuffer(buffer, offsets[index] * 4, view);
443
+ });
215
444
 
216
- return { data, count: textures.length, indexOf, extraOffset: texelAt };
445
+ if (extraWords.length) device.queue.writeBuffer(buffer, plan.extraOffset * 4, extraWords);
446
+ },
447
+ };
217
448
  }
218
449
 
219
450
  export { MIRRORED_REPEAT, REPEAT };
@@ -4,6 +4,10 @@
4
4
  * The positions are uploaded in the hierarchy's leaf order, so a leaf is a run of triangles read straight off the
5
5
  * buffer; the scene's own triangle number is looked up once, for the hit a trace returns. Every other reader goes the
6
6
  * other way, from a triangle number to where its corners sit.
7
+ *
8
+ * With INSTANCES compiled in, a leaf with a count of -1 is an instance: the ray is taken into its object space and
9
+ * the traversal goes on in its prototype's hierarchy, with a marker on the stack that brings it back to world space.
10
+ * The distance along the ray is the same in both spaces, since the direction is transformed and not normalized.
7
11
  */
8
12
  export const BVH_WGSL = /* wgsl */ `
9
13
  /** A corner of the triangle at this position of the hierarchy's leaf order. */
@@ -18,8 +22,37 @@ fn leafOf(triangle: u32) -> u32 {
18
22
  return indices[frame.leafOffset + triangle];
19
23
  }
20
24
 
25
+ /** A corner of a triangle a hit named, in world space. */
21
26
  fn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {
22
- return leafVertex(leafOf(triangle), corner);
27
+ let instance = instanceOf(triangle);
28
+ let p = leafVertex(leafOf(storedTriangle(triangle, instance)), corner);
29
+
30
+ if (instance != NO_INSTANCE) {
31
+ return instancePoint(instance, p);
32
+ }
33
+
34
+ return p;
35
+ }
36
+
37
+ /** The stack entry that leaves an instance: the traversal goes back to world space. */
38
+ const RETURN_ENTRY: u32 = 0xffffffffu;
39
+
40
+ /** A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf. */
41
+ fn rayInverse(direction: vec3<f32>) -> vec3<f32> {
42
+ let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
43
+
44
+ return signs / max(abs(direction), vec3<f32>(1e-20));
45
+ }
46
+
47
+ /** The number a hit goes by, from where it sits in the leaf order and the instance it was found in. */
48
+ fn namedTriangle(at: u32, instance: u32) -> u32 {
49
+ let stored = orderedTriangle(at);
50
+
51
+ if (INSTANCES && instance != NO_INSTANCE) {
52
+ return instanceWord(instance, 26u) + stored - instanceWord(instance, 25u);
53
+ }
54
+
55
+ return stored;
23
56
  }
24
57
 
25
58
  /** Moller-Trumbore. Returns the distance in x and the barycentric coordinates in yz, or a negative distance. */
@@ -127,7 +160,7 @@ fn waitingLeaf(entry: u32) -> vec2<i32> {
127
160
  * A leaf is tested at the top of the next step, the one place the loop tests triangles, which keeps the kernel small.
128
161
  * This visits what the binary tree's traversal visited, in the same order, so it finds the same hit.
129
162
  */
130
- fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit {
163
+ fn traceScene(worldOrigin: vec3<f32>, worldDirection: vec3<f32>, maxDistance: f32) -> Hit {
131
164
  var hit: Hit;
132
165
  hit.distance = maxDistance;
133
166
  hit.triangle = NO_HIT;
@@ -138,12 +171,15 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
138
171
  return hit;
139
172
  }
140
173
 
141
- // A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf.
142
- let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
143
- let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
174
+ var origin = worldOrigin;
175
+ var direction = worldDirection;
176
+ var inverse = rayInverse(direction);
177
+ // The instance the traversal is inside, and the one the closest hit so far was found in.
178
+ var instance = NO_INSTANCE;
179
+ var hitInstance = NO_INSTANCE;
144
180
 
145
- var stack: array<u32, 32>;
146
- var enters: array<f32, 32>;
181
+ var stack: array<u32, BVH_STACK>;
182
+ var enters: array<f32, BVH_STACK>;
147
183
  var depth = 0u;
148
184
  var current = 0u;
149
185
  // A leaf to test before anything else: its link, and its triangle count, zero when there is none.
@@ -151,10 +187,32 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
151
187
 
152
188
  loop {
153
189
  if (leaf.y > 0) {
190
+ let before = hit.distance;
191
+
154
192
  intersectLeaf(leaf.x, leaf.y, origin, direction, &hit);
193
+
194
+ if (INSTANCES && hit.distance < before) {
195
+ hitInstance = instance;
196
+ }
197
+
155
198
  leaf.y = 0;
156
199
  }
157
200
 
201
+ if (INSTANCES && leaf.y < 0) {
202
+ leaf.y = 0;
203
+
204
+ if (depth < BVH_STACK) {
205
+ instance = u32(-1 - leaf.x);
206
+ stack[depth] = RETURN_ENTRY;
207
+ enters[depth] = -1.0;
208
+ depth = depth + 1u;
209
+ origin = instanceToObject(instance, worldOrigin, 1.0);
210
+ direction = instanceToObject(instance, worldDirection, 0.0);
211
+ inverse = rayInverse(direction);
212
+ current = instanceWord(instance, 24u);
213
+ }
214
+ }
215
+
158
216
  if (current == NO_NODE) {
159
217
  if (depth == 0u) {
160
218
  break;
@@ -169,6 +227,14 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
169
227
 
170
228
  let entry = stack[depth];
171
229
 
230
+ if (INSTANCES && entry == RETURN_ENTRY) {
231
+ instance = NO_INSTANCE;
232
+ origin = worldOrigin;
233
+ direction = worldDirection;
234
+ inverse = rayInverse(direction);
235
+ continue;
236
+ }
237
+
172
238
  if ((entry & LEAF_ENTRY) != 0u) {
173
239
  leaf = waitingLeaf(entry);
174
240
  continue;
@@ -198,7 +264,7 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
198
264
  }
199
265
 
200
266
  // The builder caps its depth so the far children always fit; the guard keeps a malformed tree inside the stack.
201
- if (farEnter < 1e30 && depth < 32u) {
267
+ if (farEnter < 1e30 && depth < BVH_STACK) {
202
268
  stack[depth] = select(u32(farLink), leafEntry(parent, leftFirst), farLink < 0);
203
269
  enters[depth] = farEnter;
204
270
  depth = depth + 1u;
@@ -206,7 +272,7 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
206
272
  }
207
273
 
208
274
  if (hit.triangle != NO_HIT) {
209
- hit.triangle = orderedTriangle(hit.triangle);
275
+ hit.triangle = namedTriangle(hit.triangle, hitInstance);
210
276
  }
211
277
 
212
278
  return hit;
@@ -217,15 +283,17 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
217
283
  * closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT. The
218
284
  * right child goes first and the left one waits, the order the binary tree's traversal took.
219
285
  */
220
- fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
286
+ fn traceAny(worldOrigin: vec3<f32>, worldDirection: vec3<f32>, maxDistance: f32) -> u32 {
221
287
  if (frame.triangleCount == 0u) {
222
288
  return NO_HIT;
223
289
  }
224
290
 
225
- let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
226
- let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
291
+ var origin = worldOrigin;
292
+ var direction = worldDirection;
293
+ var inverse = rayInverse(direction);
294
+ var instance = NO_INSTANCE;
227
295
 
228
- var stack: array<u32, 32>;
296
+ var stack: array<u32, BVH_STACK>;
229
297
  var depth = 0u;
230
298
  var current = 0u;
231
299
  var leaf = vec2<i32>(0, 0);
@@ -235,12 +303,26 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
235
303
  let found = anyInLeaf(leaf.x, leaf.y, origin, direction, maxDistance);
236
304
 
237
305
  if (found != NO_HIT) {
238
- return orderedTriangle(found);
306
+ return namedTriangle(found, instance);
239
307
  }
240
308
 
241
309
  leaf.y = 0;
242
310
  }
243
311
 
312
+ if (INSTANCES && leaf.y < 0) {
313
+ leaf.y = 0;
314
+
315
+ if (depth < BVH_STACK) {
316
+ instance = u32(-1 - leaf.x);
317
+ stack[depth] = RETURN_ENTRY;
318
+ depth = depth + 1u;
319
+ origin = instanceToObject(instance, worldOrigin, 1.0);
320
+ direction = instanceToObject(instance, worldDirection, 0.0);
321
+ inverse = rayInverse(direction);
322
+ current = instanceWord(instance, 24u);
323
+ }
324
+ }
325
+
244
326
  if (current == NO_NODE) {
245
327
  if (depth == 0u) {
246
328
  break;
@@ -250,6 +332,14 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
250
332
 
251
333
  let entry = stack[depth];
252
334
 
335
+ if (INSTANCES && entry == RETURN_ENTRY) {
336
+ instance = NO_INSTANCE;
337
+ origin = worldOrigin;
338
+ direction = worldDirection;
339
+ inverse = rayInverse(direction);
340
+ continue;
341
+ }
342
+
253
343
  if ((entry & LEAF_ENTRY) != 0u) {
254
344
  leaf = waitingLeaf(entry);
255
345
  continue;
@@ -277,7 +367,7 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
277
367
  }
278
368
  }
279
369
 
280
- if (rightHit && leftHit && depth < 32u) {
370
+ if (rightHit && leftHit && depth < BVH_STACK) {
281
371
  stack[depth] = select(u32(node.leftLink), leafEntry(parent, false), node.leftLink < 0);
282
372
  depth = depth + 1u;
283
373
  }