@tinypoly/gpu-renderer 0.0.4 → 0.0.5
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 +22 -7
- package/README.md +1 -1
- package/dist/bvh.d.ts +11 -6
- package/dist/bvh.d.ts.map +1 -1
- package/dist/bvh.js +64 -11
- package/dist/bvh.js.map +1 -1
- package/dist/denoise.d.ts +7 -2
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +61 -21
- package/dist/denoise.js.map +1 -1
- package/dist/dispatchScheduler.d.ts +11 -4
- package/dist/dispatchScheduler.d.ts.map +1 -1
- package/dist/dispatchScheduler.js +14 -7
- package/dist/dispatchScheduler.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +3 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/lights.d.ts +9 -1
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +9 -0
- package/dist/lights.js.map +1 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +80 -22
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +14 -1
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +21 -3
- package/dist/sceneUpload.js.map +1 -1
- package/dist/texturePack.d.ts +16 -4
- package/dist/texturePack.d.ts.map +1 -1
- package/dist/texturePack.js +61 -8
- package/dist/texturePack.js.map +1 -1
- package/dist/wgsl/bvh.wgsl.d.ts +8 -2
- package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bvh.wgsl.js +167 -96
- 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 +20 -7
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +3 -2
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +15 -1
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/random.wgsl.d.ts +1 -1
- package/dist/wgsl/random.wgsl.d.ts.map +1 -1
- package/dist/wgsl/random.wgsl.js +9 -17
- package/dist/wgsl/random.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 +26 -3
- 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 +10 -11
- package/dist/wgsl/texture.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/bvh.ts +81 -12
- package/src/denoise.ts +66 -21
- package/src/dispatchScheduler.ts +16 -7
- package/src/frameLayout.ts +3 -0
- package/src/lights.ts +19 -1
- package/src/renderer.ts +92 -21
- package/src/sceneUpload.ts +32 -4
- package/src/texturePack.ts +80 -13
- package/src/wgsl/bvh.wgsl.ts +167 -96
- package/src/wgsl/common.wgsl.ts +20 -7
- package/src/wgsl/lights.wgsl.ts +3 -2
- package/src/wgsl/pathTrace.wgsl.ts +15 -1
- package/src/wgsl/random.wgsl.ts +9 -17
- package/src/wgsl/surface.wgsl.ts +26 -3
- package/src/wgsl/texture.wgsl.ts +10 -11
package/src/sceneUpload.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface SceneByteLength {
|
|
|
11
11
|
materials: number;
|
|
12
12
|
/** Vertex normals, the UV sets, vertex colours and shader varyings, in one buffer. */
|
|
13
13
|
attributes: number;
|
|
14
|
-
/** One material index per triangle
|
|
14
|
+
/** One material index per triangle, then the hierarchy's triangle order and its inverse, in one buffer. */
|
|
15
15
|
indices: number;
|
|
16
16
|
nodes: number;
|
|
17
17
|
lights: number;
|
|
@@ -37,11 +37,16 @@ export interface SceneBuffers {
|
|
|
37
37
|
varyingOffset: number;
|
|
38
38
|
hasColors: boolean;
|
|
39
39
|
orderOffset: number;
|
|
40
|
+
/** Where each triangle's position in the leaf order starts inside `indices`. */
|
|
41
|
+
leafOffset: number;
|
|
40
42
|
triangleCount: number;
|
|
41
43
|
/** Everything the integrator can aim a shadow ray at. */
|
|
42
44
|
lightCount: number;
|
|
43
45
|
/** How many of those are emissive triangles, which lead the buffer. */
|
|
44
46
|
areaLightCount: number;
|
|
47
|
+
/** Where the lights' cumulative probabilities and the emissive ones' triangles sit in `textures`. */
|
|
48
|
+
lightCdf: number;
|
|
49
|
+
lightTriangles: number;
|
|
45
50
|
/** Radiance an ambient or hemisphere light adds, which no shadow ray can reach. */
|
|
46
51
|
ambient: PackedLights["ambient"];
|
|
47
52
|
/** Where the surrounding image and its distributions ended up, for the frame uniform. */
|
|
@@ -76,7 +81,7 @@ export function sceneByteLength(
|
|
|
76
81
|
const lengths = {
|
|
77
82
|
positions: sized(geometry.positions.byteLength),
|
|
78
83
|
attributes: sized(attributeBytes(geometry)),
|
|
79
|
-
indices: sized(geometry.materials.byteLength + bvh.order.byteLength),
|
|
84
|
+
indices: sized(geometry.materials.byteLength + 2 * bvh.order.byteLength),
|
|
80
85
|
materials: sized(Math.max(1, geometry.materialCount) * MATERIAL_WORDS * 4),
|
|
81
86
|
nodes: sized(bvh.nodes.byteLength),
|
|
82
87
|
lights: sized(lights.entries.byteLength),
|
|
@@ -110,6 +115,25 @@ export function environmentPower(environment: PackedEnvironment | null): number
|
|
|
110
115
|
return environment ? environment.total * 2 * Math.PI ** 2 / (environment.width * environment.height) : 0;
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The positions in the hierarchy's leaf order, which is the order its traversal reads them in, so a leaf is one run of
|
|
120
|
+
* the buffer; and where each triangle went, for everything that reads a triangle by its own number.
|
|
121
|
+
*/
|
|
122
|
+
export function leafOrder(
|
|
123
|
+
positions: Float32Array, order: Uint32Array): { positions: Float32Array; inverse: Uint32Array } {
|
|
124
|
+
const ordered = new Float32Array(order.length * 9);
|
|
125
|
+
const inverse = new Uint32Array(order.length);
|
|
126
|
+
|
|
127
|
+
for (let at = 0; at < order.length; at++) {
|
|
128
|
+
const triangle = order[at];
|
|
129
|
+
|
|
130
|
+
ordered.set(positions.subarray(triangle * 9, triangle * 9 + 9), at * 9);
|
|
131
|
+
inverse[triangle] = at;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { positions: ordered, inverse };
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
export function uploadScene(
|
|
114
138
|
device: GPUDevice,
|
|
115
139
|
geometry: PackedGeometry,
|
|
@@ -147,11 +171,12 @@ export function uploadScene(
|
|
|
147
171
|
|
|
148
172
|
const colors = geometry.colors ?? new Float32Array(0);
|
|
149
173
|
const varyings = geometry.varyings ?? new Float32Array(0);
|
|
174
|
+
const leaves = leafOrder(geometry.positions, bvh.order);
|
|
150
175
|
|
|
151
176
|
const buffers = {
|
|
152
|
-
positions: write(
|
|
177
|
+
positions: write(leaves.positions, lengths.positions),
|
|
153
178
|
attributes: join([geometry.normals, geometry.uvs, colors, varyings], lengths.attributes),
|
|
154
|
-
indices: join([geometry.materials, bvh.order], lengths.indices),
|
|
179
|
+
indices: join([geometry.materials, bvh.order, leaves.inverse], lengths.indices),
|
|
155
180
|
materials: write(new Uint8Array(packMaterials(geometry)), lengths.materials),
|
|
156
181
|
nodes: write(new Uint8Array(bvh.nodes), lengths.nodes),
|
|
157
182
|
lights: write(new Uint8Array(lights.entries), lengths.lights),
|
|
@@ -166,9 +191,12 @@ export function uploadScene(
|
|
|
166
191
|
varyingOffset: geometry.normals.length + geometry.uvs.length + colors.length,
|
|
167
192
|
hasColors: colors.length > 0,
|
|
168
193
|
orderOffset: geometry.materials.length,
|
|
194
|
+
leafOffset: geometry.materials.length + bvh.order.length,
|
|
169
195
|
triangleCount: geometry.triangleCount,
|
|
170
196
|
lightCount: lights.count,
|
|
171
197
|
areaLightCount: lights.areaCount,
|
|
198
|
+
lightCdf: lights.cdfAt,
|
|
199
|
+
lightTriangles: lights.trianglesAt,
|
|
172
200
|
ambient: lights.ambient,
|
|
173
201
|
environment: {
|
|
174
202
|
texture: environment ? textures.indexOf.get(environment.texture.id) ?? NO_TEXTURE : NO_TEXTURE,
|
package/src/texturePack.ts
CHANGED
|
@@ -10,10 +10,10 @@ import type { SerializedScene, SerializedTexture } from "./upstream.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export interface PackedTextures {
|
|
12
12
|
/**
|
|
13
|
-
* One buffer for all of it: `TEXTURE_WORDS` words per texture
|
|
14
|
-
* the texels each entry points at. A byte texture takes one word per
|
|
15
|
-
* texture takes four, each the bits of one linear channel. Keeping
|
|
16
|
-
* inside the eight storage buffers a WebGPU device has to offer.
|
|
13
|
+
* One buffer for all of it: the lookup tables first (see `TEXTURE_TABLE`), then `TEXTURE_WORDS` words per texture, as
|
|
14
|
+
* `TextureInfo` in `wgsl/common.wgsl.ts`, then the texels each entry points at. A byte texture takes one word per
|
|
15
|
+
* texel, RGBA in the low to high bytes; a float texture takes four, each the bits of one linear channel. Keeping them
|
|
16
|
+
* in one binding is what holds the shader inside the eight storage buffers a WebGPU device has to offer.
|
|
17
17
|
*/
|
|
18
18
|
data: Uint32Array;
|
|
19
19
|
count: number;
|
|
@@ -32,6 +32,62 @@ export interface TextureExtras {
|
|
|
32
32
|
/** Sixteen words per entry: see `TextureInfo` in `wgsl/common.wgsl.ts`. */
|
|
33
33
|
export const TEXTURE_WORDS = 16;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Lookup tables that lead the buffer, at offsets the shader knows as constants: the second Sobol dimension one byte of
|
|
37
|
+
* the index at a time (four tables of 256), then the linear value of each of the 256 sRGB codes. Both replace work the
|
|
38
|
+
* kernel did on every read with the very numbers it computed, so nothing they feed changes.
|
|
39
|
+
*/
|
|
40
|
+
export const SOBOL_TABLE = 0;
|
|
41
|
+
export const SRGB_TABLE = SOBOL_TABLE + 4 * 256;
|
|
42
|
+
/** Where the texture table starts, after the lookup tables. */
|
|
43
|
+
export const TEXTURE_TABLE = SRGB_TABLE + 256;
|
|
44
|
+
|
|
45
|
+
/** The four byte tables of the Sobol sequence's second dimension, whose generator matrix is Pascal's triangle mod 2. */
|
|
46
|
+
function sobolTables(): Uint32Array {
|
|
47
|
+
const directions: number[] = [];
|
|
48
|
+
let direction = 0x80000000;
|
|
49
|
+
|
|
50
|
+
for (let bit = 0; bit < 32; bit++) {
|
|
51
|
+
directions.push(direction >>> 0);
|
|
52
|
+
direction = (direction ^ (direction >>> 1)) >>> 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tables = new Uint32Array(4 * 256);
|
|
56
|
+
|
|
57
|
+
for (let byte = 0; byte < 4; byte++) {
|
|
58
|
+
for (let value = 0; value < 256; value++) {
|
|
59
|
+
let xor = 0;
|
|
60
|
+
|
|
61
|
+
for (let bit = 0; bit < 8; bit++) if (value & (1 << bit)) xor ^= directions[byte * 8 + bit];
|
|
62
|
+
|
|
63
|
+
tables[byte * 256 + value] = xor >>> 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return tables;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The linear value of every sRGB code, worked out in double precision and rounded once. */
|
|
71
|
+
function srgbTable(): Float32Array {
|
|
72
|
+
return Float32Array.from({ length: 256 }, (_, code) => {
|
|
73
|
+
const value = Math.fround(code / 255);
|
|
74
|
+
|
|
75
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const LOOKUP = (() => {
|
|
80
|
+
const words = new Uint32Array(TEXTURE_TABLE);
|
|
81
|
+
|
|
82
|
+
words.set(sobolTables(), SOBOL_TABLE);
|
|
83
|
+
words.set(new Uint32Array(srgbTable().buffer), SRGB_TABLE);
|
|
84
|
+
|
|
85
|
+
return words;
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
/** Where a texture's entry starts in the buffer. */
|
|
89
|
+
export const textureEntry = (index: number) => TEXTURE_TABLE + index * TEXTURE_WORDS;
|
|
90
|
+
|
|
35
91
|
/** What a material's map index means when the material has no such map. */
|
|
36
92
|
export const NO_TEXTURE = 0xffffffff;
|
|
37
93
|
|
|
@@ -49,6 +105,9 @@ const MIRRORED_REPEAT = 1002;
|
|
|
49
105
|
/** How many UV sets the geometry carries, and so the highest channel a texture may ask for. */
|
|
50
106
|
export const UV_CHANNELS = 2;
|
|
51
107
|
|
|
108
|
+
/** Whether a word's first byte is its lowest, as it is on the device: then RGBA bytes are already RGBA words. */
|
|
109
|
+
const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
110
|
+
|
|
52
111
|
const texelCount = (texture: SerializedTexture) => Math.max(0, texture.width) * Math.max(0, texture.height);
|
|
53
112
|
|
|
54
113
|
const isFloat = (texture: SerializedTexture) => texture.data instanceof Float32Array;
|
|
@@ -66,6 +125,7 @@ const isEncoded = (texture: SerializedTexture) => !isFloat(texture) && texture.c
|
|
|
66
125
|
export interface TexturePlan {
|
|
67
126
|
textures: SerializedTexture[];
|
|
68
127
|
indexOf: Map<string, number>;
|
|
128
|
+
/** The lookup tables and the texture table together: where the texels begin. */
|
|
69
129
|
tableWords: number;
|
|
70
130
|
/** Where the words appended after the texels begin. */
|
|
71
131
|
extraOffset: number;
|
|
@@ -80,7 +140,7 @@ export function planTextures(scene: SerializedScene, extraTextures: readonly Ser
|
|
|
80
140
|
.filter(texture => !seen.has(texture.id) && !!seen.add(texture.id));
|
|
81
141
|
|
|
82
142
|
const indexOf = new Map(textures.map((texture, index) => [texture.id, index]));
|
|
83
|
-
const tableWords = Math.max(1, textures.length) * TEXTURE_WORDS;
|
|
143
|
+
const tableWords = TEXTURE_TABLE + Math.max(1, textures.length) * TEXTURE_WORDS;
|
|
84
144
|
const texelWords = textures.reduce((sum, texture) => sum + texelCount(texture) * (isFloat(texture) ? 4 : 1), 0);
|
|
85
145
|
|
|
86
146
|
return { textures, indexOf, tableWords, extraOffset: tableWords + texelWords };
|
|
@@ -100,8 +160,10 @@ export function writeTextures(plan: TexturePlan, extraWords: Uint32Array): Packe
|
|
|
100
160
|
const floats = new Float32Array(data.buffer);
|
|
101
161
|
let texelAt = tableWords;
|
|
102
162
|
|
|
163
|
+
data.set(LOOKUP, 0);
|
|
164
|
+
|
|
103
165
|
textures.forEach((texture, index) => {
|
|
104
|
-
const at = index
|
|
166
|
+
const at = textureEntry(index);
|
|
105
167
|
const texels = texelCount(texture);
|
|
106
168
|
const float = isFloat(texture);
|
|
107
169
|
|
|
@@ -131,13 +193,18 @@ export function writeTextures(plan: TexturePlan, extraWords: Uint32Array): Packe
|
|
|
131
193
|
} else {
|
|
132
194
|
const source = texture.data as Uint8Array;
|
|
133
195
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
208
|
}
|
|
142
209
|
|
|
143
210
|
texelAt += texels;
|
package/src/wgsl/bvh.wgsl.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Triangle intersection and the iterative traversal, with the fixed stack the builder caps its depth to.
|
|
3
|
+
*
|
|
4
|
+
* The positions are uploaded in the hierarchy's leaf order, so a leaf is a run of triangles read straight off the
|
|
5
|
+
* buffer; the scene's own triangle number is looked up once, for the hit a trace returns. Every other reader goes the
|
|
6
|
+
* other way, from a triangle number to where its corners sit.
|
|
7
|
+
*/
|
|
2
8
|
export const BVH_WGSL = /* wgsl */ `
|
|
3
|
-
|
|
4
|
-
|
|
9
|
+
/** A corner of the triangle at this position of the hierarchy's leaf order. */
|
|
10
|
+
fn leafVertex(at: u32, corner: u32) -> vec3<f32> {
|
|
11
|
+
let base = at * 9u + corner * 3u;
|
|
5
12
|
|
|
6
13
|
return vec3<f32>(positions[base], positions[base + 1u], positions[base + 2u]);
|
|
7
14
|
}
|
|
8
15
|
|
|
16
|
+
/** Where a triangle of the scene sits in the leaf order the positions are stored in. */
|
|
17
|
+
fn leafOf(triangle: u32) -> u32 {
|
|
18
|
+
return indices[frame.leafOffset + triangle];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {
|
|
22
|
+
return leafVertex(leafOf(triangle), corner);
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
/** Moller-Trumbore. Returns the distance in x and the barycentric coordinates in yz, or a negative distance. */
|
|
10
|
-
fn intersectTriangle(
|
|
26
|
+
fn intersectTriangle(at: u32, origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
|
|
11
27
|
let miss = vec3<f32>(-1.0, 0.0, 0.0);
|
|
12
|
-
let a =
|
|
13
|
-
let edge1 =
|
|
14
|
-
let edge2 =
|
|
28
|
+
let a = leafVertex(at, 0u);
|
|
29
|
+
let edge1 = leafVertex(at, 1u) - a;
|
|
30
|
+
let edge2 = leafVertex(at, 2u) - a;
|
|
15
31
|
let p = cross(direction, edge2);
|
|
16
32
|
let determinant = dot(edge1, p);
|
|
17
33
|
|
|
@@ -37,10 +53,10 @@ fn intersectTriangle(triangle: u32, origin: vec3<f32>, direction: vec3<f32>) ->
|
|
|
37
53
|
return vec3<f32>(dot(edge2, q) * inverse, u, v);
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
/** Where a ray enters a
|
|
41
|
-
fn enterBox(
|
|
42
|
-
let first = (
|
|
43
|
-
let second = (
|
|
56
|
+
/** Where a ray enters a box, or 1e30 when it misses it or only meets it past maxDistance. */
|
|
57
|
+
fn enterBox(low: vec3<f32>, high: vec3<f32>, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f32) -> f32 {
|
|
58
|
+
let first = (low - origin) * inverse;
|
|
59
|
+
let second = (high - origin) * inverse;
|
|
44
60
|
let near = min(first, second);
|
|
45
61
|
let far = max(first, second);
|
|
46
62
|
let enter = max(max(near.x, near.y), max(near.z, 0.0));
|
|
@@ -49,27 +65,67 @@ fn enterBox(node: BvhNode, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f
|
|
|
49
65
|
return select(1e30, enter, enter <= exit);
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
/** The triangles of a leaf, against the closest hit so far. */
|
|
53
|
-
fn intersectLeaf(
|
|
54
|
-
let start = u32(-1 -
|
|
68
|
+
/** The triangles of a leaf, against the closest hit so far, which records where in the leaf order it found one. */
|
|
69
|
+
fn intersectLeaf(link: i32, count: i32, origin: vec3<f32>, direction: vec3<f32>, hit: ptr<function, Hit>) {
|
|
70
|
+
let start = u32(-1 - link);
|
|
55
71
|
|
|
56
|
-
for (var i = 0u; i < u32(
|
|
57
|
-
let
|
|
58
|
-
let result = intersectTriangle(triangle, origin, direction);
|
|
72
|
+
for (var i = 0u; i < u32(count); i = i + 1u) {
|
|
73
|
+
let result = intersectTriangle(start + i, origin, direction);
|
|
59
74
|
|
|
60
75
|
if (result.x > RAY_EPSILON && result.x < (*hit).distance) {
|
|
61
76
|
(*hit).distance = result.x;
|
|
62
|
-
(*hit).triangle =
|
|
77
|
+
(*hit).triangle = start + i;
|
|
63
78
|
(*hit).u = result.y;
|
|
64
79
|
(*hit).v = result.z;
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
}
|
|
68
83
|
|
|
84
|
+
/** Whether any triangle of a leaf lies along the ray before maxDistance: its position in the leaf order, or NO_HIT. */
|
|
85
|
+
fn anyInLeaf(link: i32, count: i32, origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
|
|
86
|
+
let start = u32(-1 - link);
|
|
87
|
+
|
|
88
|
+
for (var i = 0u; i < u32(count); i = i + 1u) {
|
|
89
|
+
let result = intersectTriangle(start + i, origin, direction);
|
|
90
|
+
|
|
91
|
+
if (result.x > RAY_EPSILON && result.x < maxDistance) {
|
|
92
|
+
return start + i;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return NO_HIT;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** No node to go to: the traversal takes the next entry off the stack. */
|
|
100
|
+
const NO_NODE: u32 = 0xffffffffu;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A stack entry that is a leaf rather than a node: the node holding it and which of its two children it is. A leaf
|
|
104
|
+
* has no node of its own, so this is how one waits its turn.
|
|
105
|
+
*/
|
|
106
|
+
const LEAF_ENTRY: u32 = 0x80000000u;
|
|
107
|
+
|
|
108
|
+
fn leafEntry(parent: u32, right: bool) -> u32 {
|
|
109
|
+
return LEAF_ENTRY | (parent << 1u) | select(0u, 1u, right);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A waiting leaf's link and count, read back from the node that holds it. */
|
|
113
|
+
fn waitingLeaf(entry: u32) -> vec2<i32> {
|
|
114
|
+
let parent = (entry & ~LEAF_ENTRY) >> 1u;
|
|
115
|
+
|
|
116
|
+
if ((entry & 1u) != 0u) {
|
|
117
|
+
return vec2<i32>(nodes[parent].rightLink, nodes[parent].rightCount);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return vec2<i32>(nodes[parent].leftLink, nodes[parent].leftCount);
|
|
121
|
+
}
|
|
122
|
+
|
|
69
123
|
/**
|
|
70
|
-
* The closest hit along a ray.
|
|
71
|
-
* the closest hit shrinks the search early and whole subtrees behind it are skipped;
|
|
72
|
-
*
|
|
124
|
+
* The closest hit along a ray. A node holds both of its children's boxes, so one read tests them both. The nearer is
|
|
125
|
+
* visited first, so the closest hit shrinks the search early and whole subtrees behind it are skipped; the farther one
|
|
126
|
+
* waits on the stack with where the ray entered it, and is dropped unopened if something closer turned up meanwhile.
|
|
127
|
+
* A leaf is tested at the top of the next step, the one place the loop tests triangles, which keeps the kernel small.
|
|
128
|
+
* This visits what the binary tree's traversal visited, in the same order, so it finds the same hit.
|
|
73
129
|
*/
|
|
74
130
|
fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit {
|
|
75
131
|
var hit: Hit;
|
|
@@ -85,74 +141,81 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
|
|
|
85
141
|
// A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf.
|
|
86
142
|
let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
87
143
|
let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
|
|
88
|
-
let rootEnter = enterBox(nodes[0], origin, inverse, hit.distance);
|
|
89
|
-
|
|
90
|
-
if (rootEnter >= 1e30) {
|
|
91
|
-
return hit;
|
|
92
|
-
}
|
|
93
144
|
|
|
94
145
|
var stack: array<u32, 32>;
|
|
95
146
|
var enters: array<f32, 32>;
|
|
96
|
-
var depth =
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
var depth = 0u;
|
|
148
|
+
var current = 0u;
|
|
149
|
+
// A leaf to test before anything else: its link, and its triangle count, zero when there is none.
|
|
150
|
+
var leaf = vec2<i32>(0, 0);
|
|
99
151
|
|
|
100
152
|
loop {
|
|
101
|
-
if (
|
|
102
|
-
|
|
153
|
+
if (leaf.y > 0) {
|
|
154
|
+
intersectLeaf(leaf.x, leaf.y, origin, direction, &hit);
|
|
155
|
+
leaf.y = 0;
|
|
103
156
|
}
|
|
104
157
|
|
|
105
|
-
|
|
158
|
+
if (current == NO_NODE) {
|
|
159
|
+
if (depth == 0u) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
106
162
|
|
|
107
|
-
|
|
108
|
-
if (enters[depth] > hit.distance) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
163
|
+
depth = depth - 1u;
|
|
111
164
|
|
|
112
|
-
|
|
165
|
+
// Something closer was found since this was pushed.
|
|
166
|
+
if (enters[depth] > hit.distance) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
113
169
|
|
|
114
|
-
|
|
115
|
-
intersectLeaf(node, origin, direction, &hit);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
170
|
+
let entry = stack[depth];
|
|
118
171
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
let leftHit = leftEnter < 1e30;
|
|
124
|
-
let rightHit = rightEnter < 1e30;
|
|
172
|
+
if ((entry & LEAF_ENTRY) != 0u) {
|
|
173
|
+
leaf = waitingLeaf(entry);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
125
176
|
|
|
126
|
-
|
|
127
|
-
if (depth + 2u > 32u) {
|
|
128
|
-
continue;
|
|
177
|
+
current = entry;
|
|
129
178
|
}
|
|
130
179
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
let node = nodes[current];
|
|
181
|
+
let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, hit.distance);
|
|
182
|
+
let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, hit.distance);
|
|
183
|
+
let leftFirst = leftEnter <= rightEnter;
|
|
184
|
+
let nearLink = select(node.rightLink, node.leftLink, leftFirst);
|
|
185
|
+
let farEnter = select(leftEnter, rightEnter, leftFirst);
|
|
186
|
+
let farLink = select(node.leftLink, node.rightLink, leftFirst);
|
|
187
|
+
let parent = current;
|
|
188
|
+
|
|
189
|
+
current = NO_NODE;
|
|
190
|
+
|
|
191
|
+
// The nearer child: a miss means both missed.
|
|
192
|
+
if (min(leftEnter, rightEnter) < 1e30) {
|
|
193
|
+
if (nearLink < 0) {
|
|
194
|
+
leaf = vec2<i32>(nearLink, select(node.rightCount, node.leftCount, leftFirst));
|
|
195
|
+
} else {
|
|
196
|
+
current = u32(nearLink);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 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) {
|
|
202
|
+
stack[depth] = select(u32(farLink), leafEntry(parent, leftFirst), farLink < 0);
|
|
203
|
+
enters[depth] = farEnter;
|
|
146
204
|
depth = depth + 1u;
|
|
147
205
|
}
|
|
148
206
|
}
|
|
149
207
|
|
|
208
|
+
if (hit.triangle != NO_HIT) {
|
|
209
|
+
hit.triangle = orderedTriangle(hit.triangle);
|
|
210
|
+
}
|
|
211
|
+
|
|
150
212
|
return hit;
|
|
151
213
|
}
|
|
152
214
|
|
|
153
215
|
/**
|
|
154
216
|
* Whether anything at all lies along a ray before maxDistance, stopping at the first triangle it finds rather than the
|
|
155
|
-
* closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT.
|
|
217
|
+
* closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT. The
|
|
218
|
+
* right child goes first and the left one waits, the order the binary tree's traversal took.
|
|
156
219
|
*/
|
|
157
220
|
fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
|
|
158
221
|
if (frame.triangleCount == 0u) {
|
|
@@ -162,52 +225,60 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
|
|
|
162
225
|
let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
|
|
163
226
|
let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
|
|
164
227
|
|
|
165
|
-
if (enterBox(nodes[0], origin, inverse, maxDistance) >= 1e30) {
|
|
166
|
-
return NO_HIT;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
228
|
var stack: array<u32, 32>;
|
|
170
|
-
var depth =
|
|
171
|
-
|
|
229
|
+
var depth = 0u;
|
|
230
|
+
var current = 0u;
|
|
231
|
+
var leaf = vec2<i32>(0, 0);
|
|
172
232
|
|
|
173
233
|
loop {
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
}
|
|
234
|
+
if (leaf.y > 0) {
|
|
235
|
+
let found = anyInLeaf(leaf.x, leaf.y, origin, direction, maxDistance);
|
|
177
236
|
|
|
178
|
-
|
|
237
|
+
if (found != NO_HIT) {
|
|
238
|
+
return orderedTriangle(found);
|
|
239
|
+
}
|
|
179
240
|
|
|
180
|
-
|
|
241
|
+
leaf.y = 0;
|
|
242
|
+
}
|
|
181
243
|
|
|
182
|
-
if (
|
|
183
|
-
|
|
244
|
+
if (current == NO_NODE) {
|
|
245
|
+
if (depth == 0u) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
depth = depth - 1u;
|
|
184
250
|
|
|
185
|
-
|
|
186
|
-
let triangle = orderedTriangle(start + i);
|
|
187
|
-
let result = intersectTriangle(triangle, origin, direction);
|
|
251
|
+
let entry = stack[depth];
|
|
188
252
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
253
|
+
if ((entry & LEAF_ENTRY) != 0u) {
|
|
254
|
+
leaf = waitingLeaf(entry);
|
|
255
|
+
continue;
|
|
192
256
|
}
|
|
193
257
|
|
|
194
|
-
|
|
258
|
+
current = entry;
|
|
195
259
|
}
|
|
196
260
|
|
|
197
|
-
let
|
|
198
|
-
let
|
|
261
|
+
let node = nodes[current];
|
|
262
|
+
let rightHit = enterBox(node.rightLow, node.rightHigh, origin, inverse, maxDistance) < 1e30;
|
|
263
|
+
let leftHit = enterBox(node.leftLow, node.leftHigh, origin, inverse, maxDistance) < 1e30;
|
|
264
|
+
let parent = current;
|
|
199
265
|
|
|
200
|
-
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
266
|
+
current = NO_NODE;
|
|
203
267
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
268
|
+
// The right child is taken now and the left one waits; with only one of them hit, that one is taken now.
|
|
269
|
+
let takeRight = rightHit;
|
|
270
|
+
let takenLink = select(node.leftLink, node.rightLink, takeRight);
|
|
271
|
+
|
|
272
|
+
if (rightHit || leftHit) {
|
|
273
|
+
if (takenLink < 0) {
|
|
274
|
+
leaf = vec2<i32>(takenLink, select(node.leftCount, node.rightCount, takeRight));
|
|
275
|
+
} else {
|
|
276
|
+
current = u32(takenLink);
|
|
277
|
+
}
|
|
207
278
|
}
|
|
208
279
|
|
|
209
|
-
if (
|
|
210
|
-
stack[depth] =
|
|
280
|
+
if (rightHit && leftHit && depth < 32u) {
|
|
281
|
+
stack[depth] = select(u32(node.leftLink), leafEntry(parent, false), node.leftLink < 0);
|
|
211
282
|
depth = depth + 1u;
|
|
212
283
|
}
|
|
213
284
|
}
|
package/src/wgsl/common.wgsl.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { FRAME_LAYOUT } from "../frameLayout.js";
|
|
2
2
|
import { MATERIAL_KIND, MATERIAL_LAYOUT } from "../materialLayout.js";
|
|
3
|
+
import { SOBOL_TABLE, SRGB_TABLE, TEXTURE_TABLE } from "../texturePack.js";
|
|
3
4
|
|
|
4
5
|
/** Bindings and structs shared by every stage of the path tracing module. */
|
|
5
6
|
export const COMMON_WGSL = /* wgsl */ `
|
|
6
7
|
${FRAME_LAYOUT.wgsl}
|
|
7
|
-
//
|
|
8
|
+
// Sixty four bytes: the boxes of a node's two children, each a corner and a link, then a corner and a count. A link of
|
|
9
|
+
// zero or more is the child's own node; a leaf stores -1 - start in it, into the hierarchy's leaf order. See bvh.ts.
|
|
8
10
|
struct BvhNode {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
leftLow: vec3<f32>,
|
|
12
|
+
leftLink: i32,
|
|
13
|
+
leftHigh: vec3<f32>,
|
|
14
|
+
leftCount: i32,
|
|
15
|
+
rightLow: vec3<f32>,
|
|
16
|
+
rightLink: i32,
|
|
17
|
+
rightHigh: vec3<f32>,
|
|
18
|
+
rightCount: i32,
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
/** As materialLayout.ts packs it. A map index of NO_TEXTURE means the slot is empty. */
|
|
@@ -119,14 +125,21 @@ const BLEND_MULTIPLY: u32 = 4u;
|
|
|
119
125
|
const NO_TEXTURE: u32 = 0xffffffffu;
|
|
120
126
|
const NO_PROGRAM: u32 = 0xffffffffu;
|
|
121
127
|
|
|
128
|
+
// The lookup tables that lead the texture buffer, and where its table of textures starts after them: see texturePack.ts.
|
|
129
|
+
const SOBOL_TABLE: u32 = ${SOBOL_TABLE}u;
|
|
130
|
+
const SRGB_TABLE: u32 = ${SRGB_TABLE}u;
|
|
131
|
+
const TEXTURE_TABLE: u32 = ${TEXTURE_TABLE}u;
|
|
132
|
+
|
|
122
133
|
// A WebGPU device only has to offer eight storage buffers per stage, so arrays of the same kind share one binding and
|
|
123
134
|
// the uniform says where the second one starts. See sceneUpload.ts, which joins them.
|
|
124
135
|
@group(0) @binding(0) var<uniform> frame: Frame;
|
|
136
|
+
// Nine coordinates per triangle, in the hierarchy's leaf order: see leafOf in bvh.wgsl.ts.
|
|
125
137
|
@group(0) @binding(1) var<storage, read> positions: array<f32>;
|
|
126
138
|
// Normals, then the UV sets from frame.uvOffset, vertex colours from frame.colorOffset and shader varyings from
|
|
127
139
|
// frame.varyingOffset.
|
|
128
140
|
@group(0) @binding(2) var<storage, read> attributes: array<f32>;
|
|
129
|
-
// One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on
|
|
141
|
+
// One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on (the triangle at each
|
|
142
|
+
// position of the leaf order), then from frame.leafOffset on the other way (the position of each triangle).
|
|
130
143
|
@group(0) @binding(3) var<storage, read> indices: array<u32>;
|
|
131
144
|
@group(0) @binding(4) var<storage, read> materials: array<Material>;
|
|
132
145
|
@group(0) @binding(5) var<storage, read> nodes: array<BvhNode>;
|
|
@@ -174,7 +187,7 @@ fn extraVec4(at: u32) -> vec4<f32> {
|
|
|
174
187
|
|
|
175
188
|
/** One entry of the texture table, unpacked from the words it shares with the texels. */
|
|
176
189
|
fn textureInfo(index: u32) -> TextureInfo {
|
|
177
|
-
let at = index * 16u;
|
|
190
|
+
let at = TEXTURE_TABLE + index * 16u;
|
|
178
191
|
var info: TextureInfo;
|
|
179
192
|
|
|
180
193
|
info.size = vec2<u32>(textures[at], textures[at + 1u]);
|
package/src/wgsl/lights.wgsl.ts
CHANGED
|
@@ -41,7 +41,8 @@ fn lightByProbability(u: f32) -> Light {
|
|
|
41
41
|
|
|
42
42
|
let middle = (low + high) / 2u;
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// The probabilities packed tight in the texture buffer: the same floats as lights[].cdf, a word apart.
|
|
45
|
+
if (extraFloat(frame.lightCdf + middle) > u) {
|
|
45
46
|
high = middle;
|
|
46
47
|
} else {
|
|
47
48
|
low = middle + 1u;
|
|
@@ -65,7 +66,7 @@ fn lightPdfOfTriangle(triangle: u32) -> f32 {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
let middle = (low + high) / 2u;
|
|
68
|
-
let candidate =
|
|
69
|
+
let candidate = textures[frame.lightTriangles + middle];
|
|
69
70
|
|
|
70
71
|
if (candidate == triangle) {
|
|
71
72
|
return lights[middle].pdf;
|