@tinypoly/gpu-renderer 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +48 -10
- package/README.md +2 -0
- package/dist/accumulator.d.ts.map +1 -1
- package/dist/accumulator.js +1 -0
- package/dist/accumulator.js.map +1 -1
- package/dist/denoise.js +2 -2
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +9 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +5 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts +48 -8
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +409 -134
- package/dist/geometry.js.map +1 -1
- package/dist/grass.d.ts.map +1 -1
- package/dist/grass.js +2 -1
- package/dist/grass.js.map +1 -1
- package/dist/imageResolve.js +2 -2
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/instancing.d.ts +100 -0
- package/dist/instancing.d.ts.map +1 -0
- package/dist/instancing.js +286 -0
- package/dist/instancing.js.map +1 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +8 -5
- package/dist/lights.js.map +1 -1
- package/dist/memoryProbe.d.ts +76 -0
- package/dist/memoryProbe.d.ts.map +1 -0
- package/dist/memoryProbe.js +197 -0
- package/dist/memoryProbe.js.map +1 -0
- package/dist/oidnPass.d.ts +6 -0
- package/dist/oidnPass.d.ts.map +1 -1
- package/dist/oidnPass.js +18 -1
- package/dist/oidnPass.js.map +1 -1
- package/dist/pathTracePass.d.ts +15 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +38 -16
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +13 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +248 -41
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +26 -8
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +97 -27
- package/dist/sceneUpload.js.map +1 -1
- package/dist/shaderModule.d.ts.map +1 -1
- package/dist/shaderModule.js +2 -1
- package/dist/shaderModule.js.map +1 -1
- package/dist/skyBake.d.ts.map +1 -1
- package/dist/skyBake.js +5 -3
- package/dist/skyBake.js.map +1 -1
- package/dist/texturePack.d.ts +43 -3
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +209 -37
- package/dist/texturePack.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +5 -1
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +105 -15
- package/dist/wgsl/bvh.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +92 -11
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +24 -14
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/dist/wgsl/texture.wgsl.d.ts +1 -1
- package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
- package/dist/wgsl/texture.wgsl.js +11 -0
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/accumulator.ts +1 -0
- package/src/denoise.ts +2 -2
- package/src/device.ts +12 -0
- package/src/frameLayout.ts +5 -0
- package/src/geometry.ts +527 -133
- package/src/grass.ts +2 -1
- package/src/imageResolve.ts +2 -2
- package/src/index.ts +1 -0
- package/src/instancing.ts +415 -0
- package/src/lights.ts +9 -5
- package/src/memoryProbe.ts +275 -0
- package/src/oidnPass.ts +21 -1
- package/src/pathTracePass.ts +42 -18
- package/src/renderer.ts +308 -47
- package/src/sceneUpload.ts +139 -36
- package/src/shaderModule.ts +2 -1
- package/src/skyBake.ts +5 -3
- package/src/texturePack.ts +274 -43
- package/src/wgsl/bvh.wgsl.ts +105 -15
- package/src/wgsl/common.wgsl.ts +92 -11
- package/src/wgsl/surface.wgsl.ts +24 -14
- package/src/wgsl/texture.wgsl.ts +11 -0
package/src/texturePack.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import type {
|
|
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.
|
|
9
|
-
*
|
|
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[] = []
|
|
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
|
|
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 +
|
|
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
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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,
|
|
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
|
-
|
|
445
|
+
if (extraWords.length) device.queue.writeBuffer(buffer, plan.extraOffset * 4, extraWords);
|
|
446
|
+
},
|
|
447
|
+
};
|
|
217
448
|
}
|
|
218
449
|
|
|
219
450
|
export { MIRRORED_REPEAT, REPEAT };
|
package/src/wgsl/bvh.wgsl.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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,
|
|
146
|
-
var enters: array<f32,
|
|
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 <
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
226
|
-
|
|
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,
|
|
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
|
|
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 <
|
|
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
|
}
|