@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.
Files changed (75) hide show
  1. package/ARCHITECTURE.md +22 -7
  2. package/README.md +1 -1
  3. package/dist/bvh.d.ts +11 -6
  4. package/dist/bvh.d.ts.map +1 -1
  5. package/dist/bvh.js +64 -11
  6. package/dist/bvh.js.map +1 -1
  7. package/dist/denoise.d.ts +7 -2
  8. package/dist/denoise.d.ts.map +1 -1
  9. package/dist/denoise.js +61 -21
  10. package/dist/denoise.js.map +1 -1
  11. package/dist/dispatchScheduler.d.ts +11 -4
  12. package/dist/dispatchScheduler.d.ts.map +1 -1
  13. package/dist/dispatchScheduler.js +14 -7
  14. package/dist/dispatchScheduler.js.map +1 -1
  15. package/dist/frameLayout.d.ts.map +1 -1
  16. package/dist/frameLayout.js +3 -0
  17. package/dist/frameLayout.js.map +1 -1
  18. package/dist/lights.d.ts +9 -1
  19. package/dist/lights.d.ts.map +1 -1
  20. package/dist/lights.js +9 -0
  21. package/dist/lights.js.map +1 -1
  22. package/dist/renderer.d.ts.map +1 -1
  23. package/dist/renderer.js +80 -22
  24. package/dist/renderer.js.map +1 -1
  25. package/dist/sceneUpload.d.ts +14 -1
  26. package/dist/sceneUpload.d.ts.map +1 -1
  27. package/dist/sceneUpload.js +21 -3
  28. package/dist/sceneUpload.js.map +1 -1
  29. package/dist/texturePack.d.ts +16 -4
  30. package/dist/texturePack.d.ts.map +1 -1
  31. package/dist/texturePack.js +61 -8
  32. package/dist/texturePack.js.map +1 -1
  33. package/dist/wgsl/bvh.wgsl.d.ts +8 -2
  34. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  35. package/dist/wgsl/bvh.wgsl.js +167 -96
  36. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  37. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  38. package/dist/wgsl/common.wgsl.js +20 -7
  39. package/dist/wgsl/common.wgsl.js.map +1 -1
  40. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  41. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  42. package/dist/wgsl/lights.wgsl.js +3 -2
  43. package/dist/wgsl/lights.wgsl.js.map +1 -1
  44. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  45. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  46. package/dist/wgsl/pathTrace.wgsl.js +15 -1
  47. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  48. package/dist/wgsl/random.wgsl.d.ts +1 -1
  49. package/dist/wgsl/random.wgsl.d.ts.map +1 -1
  50. package/dist/wgsl/random.wgsl.js +9 -17
  51. package/dist/wgsl/random.wgsl.js.map +1 -1
  52. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  53. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  54. package/dist/wgsl/surface.wgsl.js +26 -3
  55. package/dist/wgsl/surface.wgsl.js.map +1 -1
  56. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  57. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  58. package/dist/wgsl/texture.wgsl.js +10 -11
  59. package/dist/wgsl/texture.wgsl.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/bvh.ts +81 -12
  62. package/src/denoise.ts +66 -21
  63. package/src/dispatchScheduler.ts +16 -7
  64. package/src/frameLayout.ts +3 -0
  65. package/src/lights.ts +19 -1
  66. package/src/renderer.ts +92 -21
  67. package/src/sceneUpload.ts +32 -4
  68. package/src/texturePack.ts +80 -13
  69. package/src/wgsl/bvh.wgsl.ts +167 -96
  70. package/src/wgsl/common.wgsl.ts +20 -7
  71. package/src/wgsl/lights.wgsl.ts +3 -2
  72. package/src/wgsl/pathTrace.wgsl.ts +15 -1
  73. package/src/wgsl/random.wgsl.ts +9 -17
  74. package/src/wgsl/surface.wgsl.ts +26 -3
  75. package/src/wgsl/texture.wgsl.ts +10 -11
@@ -1,17 +1,33 @@
1
- /** Triangle intersection and the iterative traversal, with the fixed stack the builder caps its depth to. */
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
- fn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {
4
- let base = triangle * 9u + corner * 3u;
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(triangle: u32, origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
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 = triangleVertex(triangle, 0u);
13
- let edge1 = triangleVertex(triangle, 1u) - a;
14
- let edge2 = triangleVertex(triangle, 2u) - a;
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 node's box, or 1e30 when it misses it or only meets it past maxDistance. */
41
- fn enterBox(node: BvhNode, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f32) -> f32 {
42
- let first = (node.low - origin) * inverse;
43
- let second = (node.high - origin) * inverse;
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(node: BvhNode, origin: vec3<f32>, direction: vec3<f32>, hit: ptr<function, Hit>) {
54
- let start = u32(-1 - node.link);
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(node.count); i = i + 1u) {
57
- let triangle = orderedTriangle(start + i);
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 = 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. Both children of a node are tested together and the nearer one is visited first, so
71
- * the closest hit shrinks the search early and whole subtrees behind it are skipped; each node remembers where the ray
72
- * entered it, and one whose entry is already past the closest hit is dropped without being opened.
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 = 1u;
97
- stack[0] = 0u;
98
- enters[0] = rootEnter;
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 (depth == 0u) {
102
- break;
153
+ if (leaf.y > 0) {
154
+ intersectLeaf(leaf.x, leaf.y, origin, direction, &hit);
155
+ leaf.y = 0;
103
156
  }
104
157
 
105
- depth = depth - 1u;
158
+ if (current == NO_NODE) {
159
+ if (depth == 0u) {
160
+ break;
161
+ }
106
162
 
107
- // Something closer was found since this node was pushed.
108
- if (enters[depth] > hit.distance) {
109
- continue;
110
- }
163
+ depth = depth - 1u;
111
164
 
112
- let node = nodes[stack[depth]];
165
+ // Something closer was found since this was pushed.
166
+ if (enters[depth] > hit.distance) {
167
+ continue;
168
+ }
113
169
 
114
- if (node.link < 0) {
115
- intersectLeaf(node, origin, direction, &hit);
116
- continue;
117
- }
170
+ let entry = stack[depth];
118
171
 
119
- let leftIndex = u32(node.link);
120
- let rightIndex = u32(node.count);
121
- let leftEnter = enterBox(nodes[leftIndex], origin, inverse, hit.distance);
122
- let rightEnter = enterBox(nodes[rightIndex], origin, inverse, hit.distance);
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
- // The builder caps its depth so both children always fit; the guard keeps a malformed tree inside the stack.
127
- if (depth + 2u > 32u) {
128
- continue;
177
+ current = entry;
129
178
  }
130
179
 
131
- if (leftHit && rightHit) {
132
- // The farther first, so the nearer comes off the stack next.
133
- let leftNearer = leftEnter <= rightEnter;
134
- stack[depth] = select(leftIndex, rightIndex, leftNearer);
135
- enters[depth] = select(leftEnter, rightEnter, leftNearer);
136
- stack[depth + 1u] = select(rightIndex, leftIndex, leftNearer);
137
- enters[depth + 1u] = select(rightEnter, leftEnter, leftNearer);
138
- depth = depth + 2u;
139
- } else if (leftHit) {
140
- stack[depth] = leftIndex;
141
- enters[depth] = leftEnter;
142
- depth = depth + 1u;
143
- } else if (rightHit) {
144
- stack[depth] = rightIndex;
145
- enters[depth] = rightEnter;
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 = 1u;
171
- stack[0] = 0u;
229
+ var depth = 0u;
230
+ var current = 0u;
231
+ var leaf = vec2<i32>(0, 0);
172
232
 
173
233
  loop {
174
- if (depth == 0u) {
175
- break;
176
- }
234
+ if (leaf.y > 0) {
235
+ let found = anyInLeaf(leaf.x, leaf.y, origin, direction, maxDistance);
177
236
 
178
- depth = depth - 1u;
237
+ if (found != NO_HIT) {
238
+ return orderedTriangle(found);
239
+ }
179
240
 
180
- let node = nodes[stack[depth]];
241
+ leaf.y = 0;
242
+ }
181
243
 
182
- if (node.link < 0) {
183
- let start = u32(-1 - node.link);
244
+ if (current == NO_NODE) {
245
+ if (depth == 0u) {
246
+ break;
247
+ }
248
+
249
+ depth = depth - 1u;
184
250
 
185
- for (var i = 0u; i < u32(node.count); i = i + 1u) {
186
- let triangle = orderedTriangle(start + i);
187
- let result = intersectTriangle(triangle, origin, direction);
251
+ let entry = stack[depth];
188
252
 
189
- if (result.x > RAY_EPSILON && result.x < maxDistance) {
190
- return triangle;
191
- }
253
+ if ((entry & LEAF_ENTRY) != 0u) {
254
+ leaf = waitingLeaf(entry);
255
+ continue;
192
256
  }
193
257
 
194
- continue;
258
+ current = entry;
195
259
  }
196
260
 
197
- let leftIndex = u32(node.link);
198
- let rightIndex = u32(node.count);
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
- if (depth + 2u > 32u) {
201
- continue;
202
- }
266
+ current = NO_NODE;
203
267
 
204
- if (enterBox(nodes[leftIndex], origin, inverse, maxDistance) < 1e30) {
205
- stack[depth] = leftIndex;
206
- depth = depth + 1u;
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 (enterBox(nodes[rightIndex], origin, inverse, maxDistance) < 1e30) {
210
- stack[depth] = rightIndex;
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bvh.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuNlC,CAAC"}
1
+ {"version":3,"file":"bvh.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwRlC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAGA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QA4LvB,CAAC"}
1
+ {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAwMvB,CAAC"}
@@ -1,14 +1,20 @@
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
  /** Bindings and structs shared by every stage of the path tracing module. */
4
5
  export const COMMON_WGSL = /* wgsl */ `
5
6
  ${FRAME_LAYOUT.wgsl}
6
- // Thirty two bytes: a corner and a link, twice. A leaf stores -1 - start in place of its left child.
7
+ // 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
8
+ // 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.
7
9
  struct BvhNode {
8
- low: vec3<f32>,
9
- link: i32,
10
- high: vec3<f32>,
11
- count: i32,
10
+ leftLow: vec3<f32>,
11
+ leftLink: i32,
12
+ leftHigh: vec3<f32>,
13
+ leftCount: i32,
14
+ rightLow: vec3<f32>,
15
+ rightLink: i32,
16
+ rightHigh: vec3<f32>,
17
+ rightCount: i32,
12
18
  };
13
19
 
14
20
  /** As materialLayout.ts packs it. A map index of NO_TEXTURE means the slot is empty. */
@@ -118,14 +124,21 @@ const BLEND_MULTIPLY: u32 = 4u;
118
124
  const NO_TEXTURE: u32 = 0xffffffffu;
119
125
  const NO_PROGRAM: u32 = 0xffffffffu;
120
126
 
127
+ // The lookup tables that lead the texture buffer, and where its table of textures starts after them: see texturePack.ts.
128
+ const SOBOL_TABLE: u32 = ${SOBOL_TABLE}u;
129
+ const SRGB_TABLE: u32 = ${SRGB_TABLE}u;
130
+ const TEXTURE_TABLE: u32 = ${TEXTURE_TABLE}u;
131
+
121
132
  // A WebGPU device only has to offer eight storage buffers per stage, so arrays of the same kind share one binding and
122
133
  // the uniform says where the second one starts. See sceneUpload.ts, which joins them.
123
134
  @group(0) @binding(0) var<uniform> frame: Frame;
135
+ // Nine coordinates per triangle, in the hierarchy's leaf order: see leafOf in bvh.wgsl.ts.
124
136
  @group(0) @binding(1) var<storage, read> positions: array<f32>;
125
137
  // Normals, then the UV sets from frame.uvOffset, vertex colours from frame.colorOffset and shader varyings from
126
138
  // frame.varyingOffset.
127
139
  @group(0) @binding(2) var<storage, read> attributes: array<f32>;
128
- // One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on.
140
+ // One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on (the triangle at each
141
+ // position of the leaf order), then from frame.leafOffset on the other way (the position of each triangle).
129
142
  @group(0) @binding(3) var<storage, read> indices: array<u32>;
130
143
  @group(0) @binding(4) var<storage, read> materials: array<Material>;
131
144
  @group(0) @binding(5) var<storage, read> nodes: array<BvhNode>;
@@ -173,7 +186,7 @@ fn extraVec4(at: u32) -> vec4<f32> {
173
186
 
174
187
  /** One entry of the texture table, unpacked from the words it shares with the texels. */
175
188
  fn textureInfo(index: u32) -> TextureInfo {
176
- let at = index * 16u;
189
+ let at = TEXTURE_TABLE + index * 16u;
177
190
  var info: TextureInfo;
178
191
 
179
192
  info.size = vec2<u32>(textures[at], textures[at + 1u]);
@@ -1 +1 @@
1
- {"version":3,"file":"common.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEtE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;EACpC,YAAY,CAAC,IAAI;;;;;;;;;;EAUjB,eAAe,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAiFO,aAAa,CAAC,QAAQ;6BACtB,aAAa,CAAC,QAAQ;4BACvB,aAAa,CAAC,OAAO;0BACvB,aAAa,CAAC,KAAK;yBACpB,aAAa,CAAC,IAAI;0BACjB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;0BACrB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsF9C,CAAC"}
1
+ {"version":3,"file":"common.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE3E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;EACpC,YAAY,CAAC,IAAI;;;;;;;;;;;;;;;EAejB,eAAe,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAiFO,aAAa,CAAC,QAAQ;6BACtB,aAAa,CAAC,QAAQ;4BACvB,aAAa,CAAC,OAAO;0BACvB,aAAa,CAAC,KAAK;yBACpB,aAAa,CAAC,IAAI;0BACjB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;0BACrB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;;;;;;;;;;;;;;;;2BAgBpB,WAAW;0BACZ,UAAU;6BACP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2EzC,CAAC"}
@@ -6,5 +6,5 @@
6
6
  * The falloff, the spot cone and the rectangle's projected area follow `material.js` in the CPU renderer, so direct
7
7
  * light from an analytic light agrees with it.
8
8
  */
9
- export declare const LIGHTS_WGSL = "\nstruct LightSample {\n // Unit vector from the shaded point towards the light.\n direction: vec3<f32>,\n // How far an occlusion ray may travel before it is past the light.\n distance: f32,\n // What the light delivers, already divided by the density it was sampled with.\n value: vec3<f32>,\n // Solid angle density of this direction, for the power heuristic. Zero when no bounce ray could find this light,\n // which is every light that occupies no geometry, and then the light strategy keeps the whole contribution.\n misPdf: f32,\n valid: bool,\n occlude: bool,\n kind: u32,\n};\n\nfn noLight() -> LightSample {\n var sample: LightSample;\n sample.valid = false;\n\n return sample;\n}\n\n/** The light whose cumulative probability range contains u. */\nfn lightByProbability(u: f32) -> Light {\n var low = 0u;\n var high = frame.lightCount - 1u;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n\n if (lights[middle].cdf > u) {\n high = middle;\n } else {\n low = middle + 1u;\n }\n }\n\n return lights[low];\n}\n\n/**\n * The area density with which direct sampling would have picked a point on this triangle, or zero if it never would.\n * Only the leading entries are emissive triangles, and they are in triangle order, so one binary search answers it.\n */\nfn lightPdfOfTriangle(triangle: u32) -> f32 {\n var low = 0u;\n var high = frame.areaLightCount;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n let candidate = lights[middle].triangle;\n\n if (candidate == triangle) {\n return lights[middle].pdf;\n }\n\n if (candidate < triangle) {\n low = middle + 1u;\n } else {\n high = middle;\n }\n }\n\n return 0.0;\n}\n\n/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */\nfn misWeight(pdf: f32, other: f32) -> f32 {\n let a = pdf * pdf;\n let b = other * other;\n\n return select(a / (a + b), 0.0, a + b <= 0.0);\n}\n\n/**\n * Spreads a direction over a cone, which is how a light of some apparent size softens its own shadow. The settings give\n * the angular diameter; a rasterizer would blur the shadow map by the same amount.\n */\nfn spreadDirection(direction: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n if (frame.shadowSpread <= 0.0) {\n return direction;\n }\n\n let basis = orthonormalBasis(direction);\n let radius = sqrt(u.x) * frame.shadowSpread;\n let angle = 2.0 * PI * u.y;\n\n return normalize(direction + basis[0] * (radius * cos(angle)) + basis[1] * (radius * sin(angle)));\n}\n\n/** Three's getDistanceAttenuation: the inverse power law, cut off smoothly at the light's range. */\nfn distanceFalloff(light: Light, distance: f32) -> f32 {\n var falloff = 1.0 / max(pow(distance, light.decay), 0.01);\n\n if (light.cutoff > 0.0) {\n let edge = clamp(1.0 - pow(distance / light.cutoff, 4.0), 0.0, 1.0);\n\n falloff = falloff * edge * edge;\n }\n\n return falloff;\n}\n\n/** A point uniformly distributed over a rectangular light, and the axis it emits along. */\nfn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {\n return light.position + light.tangent * (u.x * 2.0 - 1.0) + light.bitangent * (u.y * 2.0 - 1.0);\n}\n\n/**\n * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a\n * point light's shadow, so the same two dimensions serve every kind.\n */\nfn sampleLight(position: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {\n if (frame.lightCount == 0u) {\n return noLight();\n }\n\n return sampleLightEntry(lightByProbability(pick), position, u);\n}\n\n/** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */\nfn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {\n if ((light.flags & LIGHT_HAS_MAP) == 0u) {\n return vec3<f32>(1.0);\n }\n\n let at = light.triangle;\n let map = textures[at];\n let matrix = mat4x4<f32>(extraVec4(at + 1u), extraVec4(at + 5u), extraVec4(at + 9u), extraVec4(at + 13u));\n let coordinate = matrix * vec4<f32>(position, 1.0);\n\n if (coordinate.w <= 0.0) {\n return vec3<f32>(1.0);\n }\n\n let projected = coordinate.xyz / coordinate.w;\n\n if (any(projected <= vec3<f32>(0.0)) || any(projected >= vec3<f32>(1.0))) {\n return vec3<f32>(1.0);\n }\n\n return sampleTextureRaw(map, projected.xy).rgb;\n}\n\nfn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSample {\n var sample: LightSample;\n sample.valid = true;\n sample.kind = light.kind;\n // Emissive geometry is always tested: a panel that lit the room through its own wall would not be a path tracer.\n sample.occlude = light.kind == LIGHT_AREA\n || ((light.flags & LIGHT_CASTS_SHADOW) != 0u && frame.shadowsEnabled != 0u);\n sample.misPdf = 0.0;\n\n if (light.kind == LIGHT_ENVIRONMENT) {\n let drawn = sampleEnvironmentDirection(u);\n\n if (drawn.pdf <= 0.0) {\n return noLight();\n }\n\n let density = light.pdf * drawn.pdf;\n\n sample.direction = drawn.direction;\n sample.distance = 1e30;\n sample.value = environmentRadiance(drawn.direction) / density;\n // A bounce ray that escapes finds the same light, so the two strategies share it.\n sample.misPdf = density;\n\n return sample;\n }\n\n if (light.kind == LIGHT_DIRECTIONAL) {\n sample.direction = spreadDirection(light.direction, u);\n sample.distance = 1e30;\n // The colour already carries the intensity, which for a directional light is the irradiance it delivers.\n sample.value = light.color / light.pdf;\n\n return sample;\n }\n\n if (light.kind == LIGHT_AREA || light.kind == LIGHT_RECT) {\n let area = light.kind == LIGHT_AREA;\n let bary = triangleBarycentric(u);\n let point = select(rectanglePoint(light, u), trianglePointAt(light.triangle, bary), area);\n let axis = select(normalize(cross(light.tangent, light.bitangent)), triangleNormal(light.triangle), area);\n let offset = point - position;\n let distanceSquared = dot(offset, offset);\n let distance = sqrt(distanceSquared);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n sample.direction = offset / distance;\n // An emissive triangle radiates from both sides in this milestone; a rectangular light only from the side it faces.\n let cosine = dot(axis, sample.direction);\n let cosLight = select(max(cosine, 0.0), abs(cosine), area);\n\n if (cosLight <= 1e-6) {\n return noLight();\n }\n\n // Stop just short of the light so its own surface is not reported as the blocker.\n sample.distance = distance * (1.0 - 1e-3);\n let density = light.pdf * distanceSquared / cosLight;\n // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.\n let emission = select(light.color, emissionAt(light.triangle, bary), area);\n\n sample.value = emission / density;\n // Only a triangle of the scene's geometry can also be found by a bounce ray, so only it needs the heuristic.\n sample.misPdf = select(0.0, density, area);\n\n return sample;\n }\n\n // A point or a spot light: both fall off with distance and are sampled at their centre.\n let offset = light.position - position;\n let distance = length(offset);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n let towards = offset / distance;\n var power = distanceFalloff(light, distance);\n\n if (light.kind == LIGHT_SPOT) {\n let cosine = dot(light.direction, towards);\n\n if (light.innerCos <= light.outerCos) {\n power = power * select(0.0, 1.0, cosine >= light.outerCos);\n } else {\n let t = clamp((cosine - light.outerCos) / (light.innerCos - light.outerCos), 0.0, 1.0);\n\n power = power * t * t * (3.0 - 2.0 * t);\n }\n\n if (power <= 0.0) {\n return noLight();\n }\n }\n\n sample.direction = spreadDirection(towards, u);\n sample.distance = distance;\n sample.value = light.color * (power / light.pdf);\n\n if (light.kind == LIGHT_SPOT) {\n sample.value = sample.value * spotLightTint(light, position);\n }\n\n return sample;\n}\n\n/**\n * Whether a material stops every shadow ray that meets it, wherever: opaque, no cutout, no transmission, no clipping\n * or wireframe, no shader that might discard, and normal blending. Only then can the first triangle found decide.\n */\nfn blocksAll(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME)) == 0u\n && materials[m].alphaTest <= 0.0\n && materials[m].transmission <= 0.0\n && materials[m].clipCount == 0u\n && materials[m].program == NO_PROGRAM\n && materials[m].kind != KIND_SHADOW\n && (blending == BLEND_NORMAL || blending == BLEND_NONE);\n}\n\n/** How many surfaces a shadow ray may pass through before it gives up and calls the light blocked. */\nconst MAX_SHADOW_SKIPS: u32 = 8u;\n\n/** No material to skip: every blocker counts. */\nconst NO_MATERIAL: u32 = 0xffffffffu;\n\n/**\n * What a shadow ray keeps of a thin transmissive wall it meets: the share Three lets through, tinted by the base colour,\n * less what the interface reflects. It is the expectation of the straight crossing in sampleDielectric, so a light\n * seen through a window is found the same by both estimators. A volume bends the ray it lets through, so it blocks.\n */\nfn thinCrossing(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n if (materials[m].transmission <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n let metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n let transmission = transmissionAt(m, triangle, bary, metalness);\n let thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n\n if (transmission <= 0.0 || thickness > 0.0) {\n return vec3<f32>(0.0);\n }\n\n let albedo = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n let reflectance = fresnelDielectric(abs(dot(authoredNormal(triangle, bary), direction)), 1.0 / max(materials[m].ior, 1.0));\n\n return albedo * (transmission * (1.0 - reflectance));\n}\n\n/** The colour a blended surface filters what is behind it by: its base colour, with the map and vertex colours. */\nfn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n}\n\n/**\n * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle\n * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall\n * lets its own share through, tinted.\n */\nfn lightTransmittance(\n position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32) -> vec3<f32> {\n if (!chosen.occlude) {\n return vec3<f32>(1.0);\n }\n\n // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the first\n // one found instead of looking for the closest. Anything that lets light through goes the long way below.\n let first = traceAny(position, chosen.direction, chosen.distance);\n\n if (first == NO_HIT) {\n return vec3<f32>(1.0);\n }\n\n let firstMaterial = materialIndexOf(first);\n\n if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {\n return vec3<f32>(0.0);\n }\n\n var origin = position;\n var remaining = chosen.distance;\n var through = vec3<f32>(1.0);\n\n for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {\n let blocker = traceScene(origin, chosen.direction, remaining);\n\n if (blocker.triangle == NO_HIT) {\n return through;\n }\n\n let bary = vec2<f32>(blocker.u, blocker.v);\n let m = materialIndexOf(blocker.triangle);\n let blending = materials[m].blending;\n\n // A shadow catcher catches shadows rather than casting them, and a surface that adds light blocks none.\n if (m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {\n let coverage = coverageAt(blocker.triangle, bary, chosen.direction);\n\n if (blending == BLEND_MULTIPLY) {\n through = through * mix(vec3<f32>(1.0), blendColor(blocker.triangle, bary), coverage);\n } else if (blending == BLEND_SUBTRACTIVE) {\n through = through * (vec3<f32>(1.0) - blendColor(blocker.triangle, bary) * coverage);\n } else if (coverage >= 1.0 || sample2D(rng).x < coverage) {\n through = through * thinCrossing(blocker.triangle, bary, chosen.direction);\n }\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n }\n\n // The ray carries on from just past the surface it saw through.\n origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);\n remaining = remaining - blocker.distance - RAY_EPSILON;\n\n if (remaining <= RAY_EPSILON) {\n return through;\n }\n }\n\n return vec3<f32>(0.0);\n}\n\n/**\n * How shadowed a point is, as Three's ShadowMaterial reads it: for every light that casts a shadow, whether something\n * stands between it and the point, the darkest of them winning as in the CPU renderer. A rectangular light casts no\n * shadow in Three, so it takes no part.\n */\nfn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {\n if (frame.shadowsEnabled == 0u) {\n return 0.0;\n }\n\n var shadow = 0.0;\n\n for (var i = frame.areaLightCount; i < frame.lightCount; i = i + 1u) {\n let light = lights[i];\n\n if (light.kind == LIGHT_ENVIRONMENT || light.kind == LIGHT_RECT || (light.flags & LIGHT_CASTS_SHADOW) == 0u) {\n continue;\n }\n\n let chosen = sampleLightEntry(light, position, sample2D(rng));\n\n if (!chosen.valid) {\n continue;\n }\n\n let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL));\n\n shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));\n }\n\n return shadow;\n}\n";
9
+ export declare const LIGHTS_WGSL = "\nstruct LightSample {\n // Unit vector from the shaded point towards the light.\n direction: vec3<f32>,\n // How far an occlusion ray may travel before it is past the light.\n distance: f32,\n // What the light delivers, already divided by the density it was sampled with.\n value: vec3<f32>,\n // Solid angle density of this direction, for the power heuristic. Zero when no bounce ray could find this light,\n // which is every light that occupies no geometry, and then the light strategy keeps the whole contribution.\n misPdf: f32,\n valid: bool,\n occlude: bool,\n kind: u32,\n};\n\nfn noLight() -> LightSample {\n var sample: LightSample;\n sample.valid = false;\n\n return sample;\n}\n\n/** The light whose cumulative probability range contains u. */\nfn lightByProbability(u: f32) -> Light {\n var low = 0u;\n var high = frame.lightCount - 1u;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n\n // The probabilities packed tight in the texture buffer: the same floats as lights[].cdf, a word apart.\n if (extraFloat(frame.lightCdf + middle) > u) {\n high = middle;\n } else {\n low = middle + 1u;\n }\n }\n\n return lights[low];\n}\n\n/**\n * The area density with which direct sampling would have picked a point on this triangle, or zero if it never would.\n * Only the leading entries are emissive triangles, and they are in triangle order, so one binary search answers it.\n */\nfn lightPdfOfTriangle(triangle: u32) -> f32 {\n var low = 0u;\n var high = frame.areaLightCount;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n let candidate = textures[frame.lightTriangles + middle];\n\n if (candidate == triangle) {\n return lights[middle].pdf;\n }\n\n if (candidate < triangle) {\n low = middle + 1u;\n } else {\n high = middle;\n }\n }\n\n return 0.0;\n}\n\n/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */\nfn misWeight(pdf: f32, other: f32) -> f32 {\n let a = pdf * pdf;\n let b = other * other;\n\n return select(a / (a + b), 0.0, a + b <= 0.0);\n}\n\n/**\n * Spreads a direction over a cone, which is how a light of some apparent size softens its own shadow. The settings give\n * the angular diameter; a rasterizer would blur the shadow map by the same amount.\n */\nfn spreadDirection(direction: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n if (frame.shadowSpread <= 0.0) {\n return direction;\n }\n\n let basis = orthonormalBasis(direction);\n let radius = sqrt(u.x) * frame.shadowSpread;\n let angle = 2.0 * PI * u.y;\n\n return normalize(direction + basis[0] * (radius * cos(angle)) + basis[1] * (radius * sin(angle)));\n}\n\n/** Three's getDistanceAttenuation: the inverse power law, cut off smoothly at the light's range. */\nfn distanceFalloff(light: Light, distance: f32) -> f32 {\n var falloff = 1.0 / max(pow(distance, light.decay), 0.01);\n\n if (light.cutoff > 0.0) {\n let edge = clamp(1.0 - pow(distance / light.cutoff, 4.0), 0.0, 1.0);\n\n falloff = falloff * edge * edge;\n }\n\n return falloff;\n}\n\n/** A point uniformly distributed over a rectangular light, and the axis it emits along. */\nfn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {\n return light.position + light.tangent * (u.x * 2.0 - 1.0) + light.bitangent * (u.y * 2.0 - 1.0);\n}\n\n/**\n * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a\n * point light's shadow, so the same two dimensions serve every kind.\n */\nfn sampleLight(position: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {\n if (frame.lightCount == 0u) {\n return noLight();\n }\n\n return sampleLightEntry(lightByProbability(pick), position, u);\n}\n\n/** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */\nfn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {\n if ((light.flags & LIGHT_HAS_MAP) == 0u) {\n return vec3<f32>(1.0);\n }\n\n let at = light.triangle;\n let map = textures[at];\n let matrix = mat4x4<f32>(extraVec4(at + 1u), extraVec4(at + 5u), extraVec4(at + 9u), extraVec4(at + 13u));\n let coordinate = matrix * vec4<f32>(position, 1.0);\n\n if (coordinate.w <= 0.0) {\n return vec3<f32>(1.0);\n }\n\n let projected = coordinate.xyz / coordinate.w;\n\n if (any(projected <= vec3<f32>(0.0)) || any(projected >= vec3<f32>(1.0))) {\n return vec3<f32>(1.0);\n }\n\n return sampleTextureRaw(map, projected.xy).rgb;\n}\n\nfn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSample {\n var sample: LightSample;\n sample.valid = true;\n sample.kind = light.kind;\n // Emissive geometry is always tested: a panel that lit the room through its own wall would not be a path tracer.\n sample.occlude = light.kind == LIGHT_AREA\n || ((light.flags & LIGHT_CASTS_SHADOW) != 0u && frame.shadowsEnabled != 0u);\n sample.misPdf = 0.0;\n\n if (light.kind == LIGHT_ENVIRONMENT) {\n let drawn = sampleEnvironmentDirection(u);\n\n if (drawn.pdf <= 0.0) {\n return noLight();\n }\n\n let density = light.pdf * drawn.pdf;\n\n sample.direction = drawn.direction;\n sample.distance = 1e30;\n sample.value = environmentRadiance(drawn.direction) / density;\n // A bounce ray that escapes finds the same light, so the two strategies share it.\n sample.misPdf = density;\n\n return sample;\n }\n\n if (light.kind == LIGHT_DIRECTIONAL) {\n sample.direction = spreadDirection(light.direction, u);\n sample.distance = 1e30;\n // The colour already carries the intensity, which for a directional light is the irradiance it delivers.\n sample.value = light.color / light.pdf;\n\n return sample;\n }\n\n if (light.kind == LIGHT_AREA || light.kind == LIGHT_RECT) {\n let area = light.kind == LIGHT_AREA;\n let bary = triangleBarycentric(u);\n let point = select(rectanglePoint(light, u), trianglePointAt(light.triangle, bary), area);\n let axis = select(normalize(cross(light.tangent, light.bitangent)), triangleNormal(light.triangle), area);\n let offset = point - position;\n let distanceSquared = dot(offset, offset);\n let distance = sqrt(distanceSquared);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n sample.direction = offset / distance;\n // An emissive triangle radiates from both sides in this milestone; a rectangular light only from the side it faces.\n let cosine = dot(axis, sample.direction);\n let cosLight = select(max(cosine, 0.0), abs(cosine), area);\n\n if (cosLight <= 1e-6) {\n return noLight();\n }\n\n // Stop just short of the light so its own surface is not reported as the blocker.\n sample.distance = distance * (1.0 - 1e-3);\n let density = light.pdf * distanceSquared / cosLight;\n // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.\n let emission = select(light.color, emissionAt(light.triangle, bary), area);\n\n sample.value = emission / density;\n // Only a triangle of the scene's geometry can also be found by a bounce ray, so only it needs the heuristic.\n sample.misPdf = select(0.0, density, area);\n\n return sample;\n }\n\n // A point or a spot light: both fall off with distance and are sampled at their centre.\n let offset = light.position - position;\n let distance = length(offset);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n let towards = offset / distance;\n var power = distanceFalloff(light, distance);\n\n if (light.kind == LIGHT_SPOT) {\n let cosine = dot(light.direction, towards);\n\n if (light.innerCos <= light.outerCos) {\n power = power * select(0.0, 1.0, cosine >= light.outerCos);\n } else {\n let t = clamp((cosine - light.outerCos) / (light.innerCos - light.outerCos), 0.0, 1.0);\n\n power = power * t * t * (3.0 - 2.0 * t);\n }\n\n if (power <= 0.0) {\n return noLight();\n }\n }\n\n sample.direction = spreadDirection(towards, u);\n sample.distance = distance;\n sample.value = light.color * (power / light.pdf);\n\n if (light.kind == LIGHT_SPOT) {\n sample.value = sample.value * spotLightTint(light, position);\n }\n\n return sample;\n}\n\n/**\n * Whether a material stops every shadow ray that meets it, wherever: opaque, no cutout, no transmission, no clipping\n * or wireframe, no shader that might discard, and normal blending. Only then can the first triangle found decide.\n */\nfn blocksAll(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME)) == 0u\n && materials[m].alphaTest <= 0.0\n && materials[m].transmission <= 0.0\n && materials[m].clipCount == 0u\n && materials[m].program == NO_PROGRAM\n && materials[m].kind != KIND_SHADOW\n && (blending == BLEND_NORMAL || blending == BLEND_NONE);\n}\n\n/** How many surfaces a shadow ray may pass through before it gives up and calls the light blocked. */\nconst MAX_SHADOW_SKIPS: u32 = 8u;\n\n/** No material to skip: every blocker counts. */\nconst NO_MATERIAL: u32 = 0xffffffffu;\n\n/**\n * What a shadow ray keeps of a thin transmissive wall it meets: the share Three lets through, tinted by the base colour,\n * less what the interface reflects. It is the expectation of the straight crossing in sampleDielectric, so a light\n * seen through a window is found the same by both estimators. A volume bends the ray it lets through, so it blocks.\n */\nfn thinCrossing(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n if (materials[m].transmission <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n let metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n let transmission = transmissionAt(m, triangle, bary, metalness);\n let thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n\n if (transmission <= 0.0 || thickness > 0.0) {\n return vec3<f32>(0.0);\n }\n\n let albedo = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n let reflectance = fresnelDielectric(abs(dot(authoredNormal(triangle, bary), direction)), 1.0 / max(materials[m].ior, 1.0));\n\n return albedo * (transmission * (1.0 - reflectance));\n}\n\n/** The colour a blended surface filters what is behind it by: its base colour, with the map and vertex colours. */\nfn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n}\n\n/**\n * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle\n * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall\n * lets its own share through, tinted.\n */\nfn lightTransmittance(\n position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32) -> vec3<f32> {\n if (!chosen.occlude) {\n return vec3<f32>(1.0);\n }\n\n // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the first\n // one found instead of looking for the closest. Anything that lets light through goes the long way below.\n let first = traceAny(position, chosen.direction, chosen.distance);\n\n if (first == NO_HIT) {\n return vec3<f32>(1.0);\n }\n\n let firstMaterial = materialIndexOf(first);\n\n if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {\n return vec3<f32>(0.0);\n }\n\n var origin = position;\n var remaining = chosen.distance;\n var through = vec3<f32>(1.0);\n\n for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {\n let blocker = traceScene(origin, chosen.direction, remaining);\n\n if (blocker.triangle == NO_HIT) {\n return through;\n }\n\n let bary = vec2<f32>(blocker.u, blocker.v);\n let m = materialIndexOf(blocker.triangle);\n let blending = materials[m].blending;\n\n // A shadow catcher catches shadows rather than casting them, and a surface that adds light blocks none.\n if (m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {\n let coverage = coverageAt(blocker.triangle, bary, chosen.direction);\n\n if (blending == BLEND_MULTIPLY) {\n through = through * mix(vec3<f32>(1.0), blendColor(blocker.triangle, bary), coverage);\n } else if (blending == BLEND_SUBTRACTIVE) {\n through = through * (vec3<f32>(1.0) - blendColor(blocker.triangle, bary) * coverage);\n } else if (coverage >= 1.0 || sample2D(rng).x < coverage) {\n through = through * thinCrossing(blocker.triangle, bary, chosen.direction);\n }\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n }\n\n // The ray carries on from just past the surface it saw through.\n origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);\n remaining = remaining - blocker.distance - RAY_EPSILON;\n\n if (remaining <= RAY_EPSILON) {\n return through;\n }\n }\n\n return vec3<f32>(0.0);\n}\n\n/**\n * How shadowed a point is, as Three's ShadowMaterial reads it: for every light that casts a shadow, whether something\n * stands between it and the point, the darkest of them winning as in the CPU renderer. A rectangular light casts no\n * shadow in Three, so it takes no part.\n */\nfn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {\n if (frame.shadowsEnabled == 0u) {\n return 0.0;\n }\n\n var shadow = 0.0;\n\n for (var i = frame.areaLightCount; i < frame.lightCount; i = i + 1u) {\n let light = lights[i];\n\n if (light.kind == LIGHT_ENVIRONMENT || light.kind == LIGHT_RECT || (light.flags & LIGHT_CASTS_SHADOW) == 0u) {\n continue;\n }\n\n let chosen = sampleLightEntry(light, position, sample2D(rng));\n\n if (!chosen.valid) {\n continue;\n }\n\n let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL));\n\n shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));\n }\n\n return shadow;\n}\n";
10
10
  //# sourceMappingURL=lights.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lights.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,6pcAwavB,CAAC"}
1
+ {"version":3,"file":"lights.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,2ycAyavB,CAAC"}
@@ -41,7 +41,8 @@ fn lightByProbability(u: f32) -> Light {
41
41
 
42
42
  let middle = (low + high) / 2u;
43
43
 
44
- if (lights[middle].cdf > u) {
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 = lights[middle].triangle;
69
+ let candidate = textures[frame.lightTriangles + middle];
69
70
 
70
71
  if (candidate == triangle) {
71
72
  return lights[middle].pdf;
@@ -1 +1 @@
1
- {"version":3,"file":"lights.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwarC,CAAC"}
1
+ {"version":3,"file":"lights.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyarC,CAAC"}