@tinypoly/gpu-renderer 0.0.5 → 0.1.1

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 (115) hide show
  1. package/ARCHITECTURE.md +59 -3
  2. package/README.md +53 -4
  3. package/dist/atmosphere.d.ts +21 -0
  4. package/dist/atmosphere.d.ts.map +1 -0
  5. package/dist/atmosphere.js +72 -0
  6. package/dist/atmosphere.js.map +1 -0
  7. package/dist/denoise.d.ts +11 -1
  8. package/dist/denoise.d.ts.map +1 -1
  9. package/dist/denoise.js +34 -6
  10. package/dist/denoise.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +17 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts.map +1 -1
  15. package/dist/geometry.js +6 -3
  16. package/dist/geometry.js.map +1 -1
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/lensPass.d.ts +60 -0
  22. package/dist/lensPass.d.ts.map +1 -0
  23. package/dist/lensPass.js +235 -0
  24. package/dist/lensPass.js.map +1 -0
  25. package/dist/lightTree.d.ts +50 -0
  26. package/dist/lightTree.d.ts.map +1 -0
  27. package/dist/lightTree.js +146 -0
  28. package/dist/lightTree.js.map +1 -0
  29. package/dist/lights.d.ts +12 -0
  30. package/dist/lights.d.ts.map +1 -1
  31. package/dist/lights.js +56 -1
  32. package/dist/lights.js.map +1 -1
  33. package/dist/materialLayout.d.ts +24 -0
  34. package/dist/materialLayout.d.ts.map +1 -1
  35. package/dist/materialLayout.js +36 -1
  36. package/dist/materialLayout.js.map +1 -1
  37. package/dist/pathTracePass.d.ts +6 -3
  38. package/dist/pathTracePass.d.ts.map +1 -1
  39. package/dist/pathTracePass.js +16 -10
  40. package/dist/pathTracePass.js.map +1 -1
  41. package/dist/renderer.d.ts +27 -7
  42. package/dist/renderer.d.ts.map +1 -1
  43. package/dist/renderer.js +153 -16
  44. package/dist/renderer.js.map +1 -1
  45. package/dist/sceneUpload.d.ts +6 -0
  46. package/dist/sceneUpload.d.ts.map +1 -1
  47. package/dist/sceneUpload.js +5 -0
  48. package/dist/sceneUpload.js.map +1 -1
  49. package/dist/settings.d.ts +142 -0
  50. package/dist/settings.d.ts.map +1 -0
  51. package/dist/settings.js +100 -0
  52. package/dist/settings.js.map +1 -0
  53. package/dist/subsurface.d.ts +76 -0
  54. package/dist/subsurface.d.ts.map +1 -0
  55. package/dist/subsurface.js +70 -0
  56. package/dist/subsurface.js.map +1 -0
  57. package/dist/volumeContainment.d.ts +17 -0
  58. package/dist/volumeContainment.d.ts.map +1 -0
  59. package/dist/volumeContainment.js +61 -0
  60. package/dist/volumeContainment.js.map +1 -0
  61. package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
  62. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
  63. package/dist/wgsl/bsdf.wgsl.js +35 -3
  64. package/dist/wgsl/bsdf.wgsl.js.map +1 -1
  65. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  66. package/dist/wgsl/common.wgsl.js +4 -0
  67. package/dist/wgsl/common.wgsl.js.map +1 -1
  68. package/dist/wgsl/lens.wgsl.d.ts +17 -0
  69. package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
  70. package/dist/wgsl/lens.wgsl.js +301 -0
  71. package/dist/wgsl/lens.wgsl.js.map +1 -0
  72. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  73. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  74. package/dist/wgsl/lights.wgsl.js +237 -31
  75. package/dist/wgsl/lights.wgsl.js.map +1 -1
  76. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  77. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  78. package/dist/wgsl/pathTrace.wgsl.js +406 -25
  79. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  80. package/dist/wgsl/random.wgsl.d.ts +1 -1
  81. package/dist/wgsl/random.wgsl.d.ts.map +1 -1
  82. package/dist/wgsl/random.wgsl.js +28 -0
  83. package/dist/wgsl/random.wgsl.js.map +1 -1
  84. package/dist/wgsl/sampling.wgsl.d.ts +1 -1
  85. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
  86. package/dist/wgsl/sampling.wgsl.js +25 -0
  87. package/dist/wgsl/sampling.wgsl.js.map +1 -1
  88. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  89. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  90. package/dist/wgsl/surface.wgsl.js +21 -1
  91. package/dist/wgsl/surface.wgsl.js.map +1 -1
  92. package/package.json +1 -1
  93. package/src/atmosphere.ts +95 -0
  94. package/src/denoise.ts +44 -6
  95. package/src/frameLayout.ts +17 -0
  96. package/src/geometry.ts +6 -2
  97. package/src/index.ts +13 -0
  98. package/src/lensPass.ts +299 -0
  99. package/src/lightTree.ts +218 -0
  100. package/src/lights.ts +75 -1
  101. package/src/materialLayout.ts +52 -1
  102. package/src/pathTracePass.ts +19 -10
  103. package/src/renderer.ts +206 -24
  104. package/src/sceneUpload.ts +11 -0
  105. package/src/settings.ts +240 -0
  106. package/src/subsurface.ts +153 -0
  107. package/src/volumeContainment.ts +81 -0
  108. package/src/wgsl/bsdf.wgsl.ts +35 -3
  109. package/src/wgsl/common.wgsl.ts +4 -0
  110. package/src/wgsl/lens.wgsl.ts +300 -0
  111. package/src/wgsl/lights.wgsl.ts +237 -31
  112. package/src/wgsl/pathTrace.wgsl.ts +406 -25
  113. package/src/wgsl/random.wgsl.ts +28 -0
  114. package/src/wgsl/sampling.wgsl.ts +25 -0
  115. package/src/wgsl/surface.wgsl.ts +21 -1
@@ -0,0 +1,300 @@
1
+ /**
2
+ * What the lens and the sensor do to the light before it is exposed: a halo around bright light (bloom), colour
3
+ * fringes towards the corners (lateral chromatic aberration) and the camera's white balance. Everything here is linear
4
+ * light, between the traced (or denoised) radiance and the presentation pass, which exposes and tone maps the result.
5
+ *
6
+ * The halo is built over a pyramid of half sized copies of the image, as Jimenez's "Next generation post processing in
7
+ * Call of Duty" (2014) builds it: each level is a 13 tap filter of the one above, and the levels are folded back from
8
+ * the smallest up with a tent filter, each mixed with the one below by the radius. The result is a wide, smooth spread
9
+ * whose weights sum to one, so a flat image is left as it was and the halo takes light from where it is rather than
10
+ * adding any: the image keeps its exact mean.
11
+ *
12
+ * The rays a bright light throws through the aperture are gathered the same way, from a quarter sized level: each
13
+ * texel takes the light of the texels behind it along every ray's direction, fading with the distance, with weights
14
+ * that again sum to one.
15
+ */
16
+ export const LENS_WGSL = /* wgsl */ `
17
+ struct Lens {
18
+ resolution: vec2<u32>,
19
+ // Whether the colour comes from the denoiser's output rather than straight from the accumulation.
20
+ denoised: u32,
21
+ // Upsampling only: the level read from sits in the downsampled pyramid (the smallest one) instead of the folded one.
22
+ fromDown: u32,
23
+ sourceSize: vec2<u32>,
24
+ // Where the level read from starts in its pyramid; a size of the full resolution reads the image itself.
25
+ sourceOffset: u32,
26
+ // The first downsample reads the image itself, and averages its taps by luminance against fireflies.
27
+ fromImage: u32,
28
+ targetSize: vec2<u32>,
29
+ targetOffset: u32,
30
+ // Where the largest level of the folded pyramid starts, which the composite reads the halo from.
31
+ haloOffset: u32,
32
+ haloSize: vec2<u32>,
33
+ // How much of the light the halo takes, and how much each level takes from the one below it.
34
+ intensity: f32,
35
+ radius: f32,
36
+ // How far red and blue move apart at the corners, as a share of the distance from the centre.
37
+ aberration: f32,
38
+ // How much of the light the aperture's rays take, how many there are, and how far they reach in streak texels.
39
+ glare: f32,
40
+ rays: u32,
41
+ glareLength: f32,
42
+ // The white balance, a linear sRGB matrix, row by row.
43
+ white0: vec4<f32>,
44
+ white1: vec4<f32>,
45
+ white2: vec4<f32>,
46
+ // The size of the level the rays are gathered at, and the angle of the first ray, in radians.
47
+ streakSize: vec2<u32>,
48
+ glareAngle: f32,
49
+ pad0: f32,
50
+ };
51
+
52
+ const PI: f32 = 3.14159265358979;
53
+
54
+ /** Taps along each of the aperture's rays. */
55
+ const STREAK_TAPS: i32 = 24;
56
+
57
+ @group(0) @binding(0) var<uniform> lens: Lens;
58
+ @group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;
59
+ @group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;
60
+ @group(0) @binding(3) var<storage, read_write> down: array<vec4<f32>>;
61
+ @group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;
62
+ @group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;
63
+ @group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;
64
+
65
+ /** The image's own texel: its mean so far, or nothing where no path has landed yet. */
66
+ fn imageTexel(x: i32, y: i32) -> vec3<f32> {
67
+ let size = vec2<i32>(lens.resolution);
68
+ let index = u32(clamp(y, 0, size.y - 1) * size.x + clamp(x, 0, size.x - 1));
69
+
70
+ if (lens.denoised != 0u) {
71
+ return denoised[index].rgb;
72
+ }
73
+
74
+ let stored = accumulation[index];
75
+
76
+ return select(stored.rgb / stored.w, vec3<f32>(0.0), stored.w <= 0.0);
77
+ }
78
+
79
+ fn downTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {
80
+ let s = vec2<i32>(size);
81
+
82
+ return down[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;
83
+ }
84
+
85
+ fn upTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {
86
+ let s = vec2<i32>(size);
87
+
88
+ return up[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;
89
+ }
90
+
91
+ /** Bilinear weights and the texel below and left of a point given in texels, edges clamped. */
92
+ fn bilinearAt(position: vec2<f32>) -> vec4<f32> {
93
+ let p = position - 0.5;
94
+ let base = floor(p);
95
+
96
+ return vec4<f32>(base, p - base);
97
+ }
98
+
99
+ fn imageBilinear(position: vec2<f32>) -> vec3<f32> {
100
+ let b = bilinearAt(position);
101
+ let x = i32(b.x);
102
+ let y = i32(b.y);
103
+
104
+ return mix(
105
+ mix(imageTexel(x, y), imageTexel(x + 1, y), b.z),
106
+ mix(imageTexel(x, y + 1), imageTexel(x + 1, y + 1), b.z), b.w);
107
+ }
108
+
109
+ fn downBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {
110
+ let b = bilinearAt(position);
111
+ let x = i32(b.x);
112
+ let y = i32(b.y);
113
+
114
+ return mix(
115
+ mix(downTexel(offset, size, x, y), downTexel(offset, size, x + 1, y), b.z),
116
+ mix(downTexel(offset, size, x, y + 1), downTexel(offset, size, x + 1, y + 1), b.z), b.w);
117
+ }
118
+
119
+ fn upBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {
120
+ let b = bilinearAt(position);
121
+ let x = i32(b.x);
122
+ let y = i32(b.y);
123
+
124
+ return mix(
125
+ mix(upTexel(offset, size, x, y), upTexel(offset, size, x + 1, y), b.z),
126
+ mix(upTexel(offset, size, x, y + 1), upTexel(offset, size, x + 1, y + 1), b.z), b.w);
127
+ }
128
+
129
+ /** The level being filtered, read bilinearly at a point given in its own texels. */
130
+ fn sourceAt(position: vec2<f32>) -> vec3<f32> {
131
+ if (lens.fromImage != 0u) {
132
+ return imageBilinear(position);
133
+ }
134
+
135
+ return downBilinear(lens.sourceOffset, lens.sourceSize, position);
136
+ }
137
+
138
+ fn luminance(color: vec3<f32>) -> f32 {
139
+ return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
140
+ }
141
+
142
+ /** Karis' average: a group of taps weighed by one over one plus its luminance, so a lone firefly cannot flare. */
143
+ fn karis(sum: vec3<f32>) -> vec4<f32> {
144
+ let weight = 1.0 / (1.0 + luminance(sum));
145
+
146
+ return vec4<f32>(sum * weight, weight);
147
+ }
148
+
149
+ /** One level of the pyramid from the one above it: Jimenez's 13 taps, four overlapping boxes and a centre one. */
150
+ @compute @workgroup_size(8, 8, 1)
151
+ fn downsample(@builtin(global_invocation_id) id: vec3<u32>) {
152
+ if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {
153
+ return;
154
+ }
155
+
156
+ let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);
157
+ let centre = (vec2<f32>(id.xy) + 0.5) * scale;
158
+ let a = sourceAt(centre + vec2<f32>(-2.0, -2.0));
159
+ let b = sourceAt(centre + vec2<f32>(0.0, -2.0));
160
+ let c = sourceAt(centre + vec2<f32>(2.0, -2.0));
161
+ let d = sourceAt(centre + vec2<f32>(-2.0, 0.0));
162
+ let e = sourceAt(centre);
163
+ let f = sourceAt(centre + vec2<f32>(2.0, 0.0));
164
+ let g = sourceAt(centre + vec2<f32>(-2.0, 2.0));
165
+ let h = sourceAt(centre + vec2<f32>(0.0, 2.0));
166
+ let i = sourceAt(centre + vec2<f32>(2.0, 2.0));
167
+ let j = sourceAt(centre + vec2<f32>(-1.0, -1.0));
168
+ let k = sourceAt(centre + vec2<f32>(1.0, -1.0));
169
+ let l = sourceAt(centre + vec2<f32>(-1.0, 1.0));
170
+ let m = sourceAt(centre + vec2<f32>(1.0, 1.0));
171
+ var result: vec3<f32>;
172
+
173
+ if (lens.fromImage != 0u) {
174
+ let groups = karis((j + k + l + m) * 0.25) * 0.5
175
+ + karis((a + b + d + e) * 0.25) * 0.125
176
+ + karis((b + c + e + f) * 0.25) * 0.125
177
+ + karis((d + e + g + h) * 0.25) * 0.125
178
+ + karis((e + f + h + i) * 0.25) * 0.125;
179
+
180
+ result = groups.rgb / max(groups.w, 1e-6);
181
+ } else {
182
+ result = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
183
+ }
184
+
185
+ down[lens.targetOffset + id.y * lens.targetSize.x + id.x] = vec4<f32>(max(result, vec3<f32>(0.0)), 1.0);
186
+ }
187
+
188
+ /** A 3 x 3 tent over the smaller level, read at a point given in its texels. */
189
+ fn tent(position: vec2<f32>) -> vec3<f32> {
190
+ var sum = vec3<f32>(0.0);
191
+
192
+ for (var dy = -1; dy <= 1; dy = dy + 1) {
193
+ for (var dx = -1; dx <= 1; dx = dx + 1) {
194
+ let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;
195
+ let at = position + vec2<f32>(f32(dx), f32(dy));
196
+
197
+ if (lens.fromDown != 0u) {
198
+ sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;
199
+ } else {
200
+ sum = sum + upBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;
201
+ }
202
+ }
203
+ }
204
+
205
+ return sum;
206
+ }
207
+
208
+ /** One level folded back: its own light, mixed by the radius with the wider spread of every level below it. */
209
+ @compute @workgroup_size(8, 8, 1)
210
+ fn upsample(@builtin(global_invocation_id) id: vec3<u32>) {
211
+ if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {
212
+ return;
213
+ }
214
+
215
+ let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);
216
+ let wider = tent((vec2<f32>(id.xy) + 0.5) * scale);
217
+ let index = lens.targetOffset + id.y * lens.targetSize.x + id.x;
218
+
219
+ up[index] = vec4<f32>(mix(down[index].rgb, wider, lens.radius), 1.0);
220
+ }
221
+
222
+ /** The aperture's rays at one texel of the streak level: the light behind it along each ray, fading with distance. */
223
+ @compute @workgroup_size(8, 8, 1)
224
+ fn streaks(@builtin(global_invocation_id) id: vec3<u32>) {
225
+ if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {
226
+ return;
227
+ }
228
+
229
+ let centre = vec2<f32>(id.xy) + 0.5;
230
+ var sum = vec3<f32>(0.0);
231
+ var total = 0.0;
232
+
233
+ for (var r = 0u; r < lens.rays; r = r + 1u) {
234
+ let angle = lens.glareAngle + f32(r) * 2.0 * PI / f32(lens.rays);
235
+ let along = vec2<f32>(cos(angle), sin(angle));
236
+
237
+ for (var j = 1; j <= STREAK_TAPS; j = j + 1) {
238
+ let t = f32(j) / f32(STREAK_TAPS);
239
+ let weight = exp(-4.0 * t);
240
+
241
+ sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength)) * weight;
242
+ total = total + weight;
243
+ }
244
+ }
245
+
246
+ streaked[id.y * lens.targetSize.x + id.x] = vec4<f32>(sum / max(total, 1e-6), 1.0);
247
+ }
248
+
249
+ fn streakTexel(x: i32, y: i32) -> vec3<f32> {
250
+ let s = vec2<i32>(lens.streakSize);
251
+
252
+ return streaked[u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;
253
+ }
254
+
255
+ fn streakBilinear(position: vec2<f32>) -> vec3<f32> {
256
+ let b = bilinearAt(position);
257
+ let x = i32(b.x);
258
+ let y = i32(b.y);
259
+
260
+ return mix(mix(streakTexel(x, y), streakTexel(x + 1, y), b.z), mix(streakTexel(x, y + 1), streakTexel(x + 1, y + 1), b.z), b.w);
261
+ }
262
+
263
+ /** The image as the lens delivers it: fringed towards the corners, with its halo, balanced for the camera's white. */
264
+ @compute @workgroup_size(8, 8, 1)
265
+ fn composite(@builtin(global_invocation_id) id: vec3<u32>) {
266
+ if (id.x >= lens.resolution.x || id.y >= lens.resolution.y) {
267
+ return;
268
+ }
269
+
270
+ let size = vec2<f32>(lens.resolution);
271
+ let position = vec2<f32>(id.xy) + 0.5;
272
+ var color = imageTexel(i32(id.x), i32(id.y));
273
+
274
+ // Lateral chromatic aberration: each colour is magnified a little differently, red outwards and blue inwards,
275
+ // in proportion to the distance from the centre, which is how a lens's dispersion moves them.
276
+ if (lens.aberration > 0.0) {
277
+ let centre = size * 0.5;
278
+ let away = position - centre;
279
+ let red = imageBilinear(centre + away * (1.0 - lens.aberration));
280
+ let blue = imageBilinear(centre + away * (1.0 + lens.aberration));
281
+
282
+ color = vec3<f32>(red.r, color.g, blue.b);
283
+ }
284
+
285
+ if (lens.intensity > 0.0) {
286
+ // The largest folded level is already a tent over the levels below it, so one bilinear read of it is smooth.
287
+ let halo = upBilinear(lens.haloOffset, lens.haloSize, position * vec2<f32>(lens.haloSize) / size);
288
+
289
+ color = mix(color, halo, lens.intensity);
290
+ }
291
+
292
+ if (lens.glare > 0.0) {
293
+ color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);
294
+ }
295
+
296
+ let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));
297
+
298
+ graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);
299
+ }
300
+ `;
@@ -52,11 +52,177 @@ fn lightByProbability(u: f32) -> Light {
52
52
  return lights[low];
53
53
  }
54
54
 
55
+ const TREE_NODE_WORDS: u32 = 16u;
56
+ const TREE_LEAF: u32 = 0x80000000u;
57
+ const TREE_ROOT_PARENT: u32 = 0xffffffffu;
58
+ const TREE_ENTRY_WORDS: u32 = 4u;
59
+ const NO_LIGHT: u32 = 0xffffffffu;
60
+
61
+ /**
62
+ * How much the lights under a node of the light tree can deliver to a point: their power over the square of the
63
+ * distance, times how well their cone of directions and the receiving surface face each other, each widened by the
64
+ * angle the node's box takes from the point (Conty Estevez and Kulla's importance). A point inside the box rules
65
+ * nothing out. A zero normal is a receiver that takes light from everywhere, such as a point in a volume.
66
+ */
67
+ fn lightNodeImportance(node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
68
+ let at = frame.lightTree + node * TREE_NODE_WORDS;
69
+ let low = extraVec4(at);
70
+ let high = extraVec4(at + 4u);
71
+
72
+ if (low.w <= 0.0) {
73
+ return 0.0;
74
+ }
75
+
76
+ let centre = (low.xyz + high.xyz) * 0.5;
77
+ let radius2 = dot(high.xyz - low.xyz, high.xyz - low.xyz) * 0.25;
78
+ let offset = position - centre;
79
+ let distance2 = dot(offset, offset);
80
+
81
+ if (distance2 <= radius2) {
82
+ return low.w / max(radius2, 1e-12);
83
+ }
84
+
85
+ let away = offset * inverseSqrt(distance2);
86
+ // The angle the box takes from the point, as its sine and cosine: the angles below are widened by it without a
87
+ // single inverse trigonometric function, by the sum and difference formulas.
88
+ let sinU2 = radius2 / distance2;
89
+ let cosU = sqrt(1.0 - sinU2);
90
+ let sinU = sqrt(sinU2);
91
+ var orientation = 1.0;
92
+
93
+ // A cone of minus one is the whole sphere: such lights shine towards every point alike.
94
+ if (high.w > -1.0) {
95
+ let cone = extraVec4(at + 8u);
96
+ let cosO = high.w;
97
+ let sinO = sqrt(max(0.0, 1.0 - cosO * cosO));
98
+ // The cone widened by the box: cos and sin of theta_o + theta_u, the whole sphere once that passes pi.
99
+ let cosWide = cosO * cosU - sinO * sinU;
100
+ let sinWide = sinO * cosU + cosO * sinU;
101
+ let cosTheta = dot(cone.xyz, away);
102
+
103
+ // cos(max(0, theta - theta_o - theta_u)), which is one while the point lies within the widened cone.
104
+ if (sinWide >= 0.0 && cosTheta < cosWide) {
105
+ let sinTheta = sqrt(max(0.0, 1.0 - cosTheta * cosTheta));
106
+ let cosPast = cosTheta * cosWide + sinTheta * sinWide;
107
+
108
+ orientation = select(0.0, cosPast, cosPast >= cone.w);
109
+ }
110
+ }
111
+
112
+ var facing = 1.0;
113
+
114
+ if (any(normal != vec3<f32>(0.0))) {
115
+ let cosN = -dot(normal, away);
116
+
117
+ // cos(max(0, theta_n - theta_u)), and nothing once that reaches a right angle.
118
+ if (cosN < cosU) {
119
+ let sinN = sqrt(max(0.0, 1.0 - cosN * cosN));
120
+
121
+ facing = max(0.0, cosN * cosU + sinN * sinU);
122
+ }
123
+ }
124
+
125
+ return low.w * orientation * facing / distance2;
126
+ }
127
+
128
+ /** The chance of stepping from a node of the tree to each of its children, from this point. */
129
+ fn lightChildOdds(left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {
130
+ var importance = vec2<f32>(0.0);
131
+
132
+ // One loop over the two children, so the importance is written into the kernel once per walk.
133
+ for (var k = 0u; k < 2u; k = k + 1u) {
134
+ importance[k] = lightNodeImportance(select(right, left, k == 0u), position, normal);
135
+ }
136
+
137
+ let total = importance.x + importance.y;
138
+
139
+ return select(importance / total, vec2<f32>(0.0), total <= 0.0);
140
+ }
141
+
142
+ /** A light picked down the tree by importance from this point, with the chance of picking it; NO_LIGHT for none. */
143
+ fn sampleLightTree(position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<function, f32>,
144
+ density: ptr<function, f32>) -> u32 {
145
+ var node = 0u;
146
+ var chance = 1.0;
147
+ var x = u;
148
+
149
+ for (var depth = 0u; depth < 64u; depth = depth + 1u) {
150
+ let at = frame.lightTree + node * TREE_NODE_WORDS;
151
+ let first = textures[at + 12u];
152
+
153
+ // A leaf lists its lights, too close together to tell apart from here: one is picked by its share of the power.
154
+ if ((first & TREE_LEAF) != 0u) {
155
+ let start = frame.lightList + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;
156
+ let count = textures[at + 13u];
157
+ var slot = start + (count - 1u) * TREE_ENTRY_WORDS;
158
+
159
+ for (var j = 0u; j + 1u < count; j = j + 1u) {
160
+ if (x < extraFloat(start + j * TREE_ENTRY_WORDS + 3u)) {
161
+ slot = start + j * TREE_ENTRY_WORDS;
162
+ break;
163
+ }
164
+ }
165
+
166
+ *pdf = chance * extraFloat(slot + 1u);
167
+ *density = extraFloat(slot + 2u);
168
+
169
+ return textures[slot];
170
+ }
171
+
172
+ let second = textures[at + 13u];
173
+ let odds = lightChildOdds(first, second, position, normal);
174
+
175
+ if (odds.x + odds.y <= 0.0) {
176
+ return NO_LIGHT;
177
+ }
178
+
179
+ if (x < odds.x) {
180
+ node = first;
181
+ chance = chance * odds.x;
182
+ x = x / odds.x;
183
+ } else {
184
+ node = second;
185
+ chance = chance * odds.y;
186
+ x = (x - odds.x) / max(odds.y, 1e-20);
187
+ }
188
+
189
+ x = min(x, 0.99999994);
190
+ }
191
+
192
+ return NO_LIGHT;
193
+ }
194
+
195
+ /** The chance the tree would have picked the local light at this index from this point, walked up from its leaf. */
196
+ fn lightTreePdf(index: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
197
+ var node = textures[frame.lightLeaves + index * 2u];
198
+ let slot = frame.lightList + textures[frame.lightLeaves + index * 2u + 1u] * TREE_ENTRY_WORDS;
199
+ // Its share within its leaf, and the area density of a point on it.
200
+ var chance = extraFloat(slot + 1u) * extraFloat(slot + 2u);
201
+
202
+ for (var depth = 0u; depth < 64u; depth = depth + 1u) {
203
+ let parent = textures[frame.lightTree + node * TREE_NODE_WORDS + 14u];
204
+
205
+ if (parent == TREE_ROOT_PARENT) {
206
+ return chance;
207
+ }
208
+
209
+ let at = frame.lightTree + parent * TREE_NODE_WORDS;
210
+ let first = textures[at + 12u];
211
+ let odds = lightChildOdds(first, textures[at + 13u], position, normal);
212
+
213
+ chance = chance * select(odds.y, odds.x, node == first);
214
+ node = parent;
215
+ }
216
+
217
+ return 0.0;
218
+ }
219
+
55
220
  /**
56
- * The area density with which direct sampling would have picked a point on this triangle, or zero if it never would.
57
- * Only the leading entries are emissive triangles, and they are in triangle order, so one binary search answers it.
221
+ * The area density with which direct sampling from this point would have picked a point on this triangle, or zero if
222
+ * it never would. Only the leading entries are emissive triangles, and they are in triangle order, so one binary search
223
+ * finds the entry; the tree, when there is one, says how likely it was from here.
58
224
  */
59
- fn lightPdfOfTriangle(triangle: u32) -> f32 {
225
+ fn lightPdfOfTriangle(triangle: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
60
226
  var low = 0u;
61
227
  var high = frame.areaLightCount;
62
228
 
@@ -69,6 +235,10 @@ fn lightPdfOfTriangle(triangle: u32) -> f32 {
69
235
  let candidate = textures[frame.lightTriangles + middle];
70
236
 
71
237
  if (candidate == triangle) {
238
+ if (frame.lightTree != 0u) {
239
+ return frame.localLightPick * lightTreePdf(middle, position, normal);
240
+ }
241
+
72
242
  return lights[middle].pdf;
73
243
  }
74
244
 
@@ -128,12 +298,27 @@ fn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {
128
298
  * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a
129
299
  * point light's shadow, so the same two dimensions serve every kind.
130
300
  */
131
- fn sampleLight(position: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {
301
+ fn sampleLight(position: vec3<f32>, normal: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {
132
302
  if (frame.lightCount == 0u) {
133
303
  return noLight();
134
304
  }
135
305
 
136
- return sampleLightEntry(lightByProbability(pick), position, u);
306
+ // The lights in the scene go through the tree, by what they can deliver here; what lies outside it by its weight.
307
+ if (frame.lightTree != 0u && pick < frame.localLightPick) {
308
+ var chance = 0.0;
309
+ var density = 1.0;
310
+ let index = sampleLightTree(position, normal, pick / frame.localLightPick, &chance, &density);
311
+
312
+ if (index == NO_LIGHT || chance <= 0.0) {
313
+ return noLight();
314
+ }
315
+
316
+ return sampleLightEntry(lights[index], frame.localLightPick * chance * density, position, u);
317
+ }
318
+
319
+ let light = lightByProbability(pick);
320
+
321
+ return sampleLightEntry(light, light.pdf, position, u);
137
322
  }
138
323
 
139
324
  /** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */
@@ -160,7 +345,8 @@ fn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {
160
345
  return sampleTextureRaw(map, projected.xy).rgb;
161
346
  }
162
347
 
163
- fn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSample {
348
+ /** pdf is the chance of picking this light, times the area density of a point on it where it has an area. */
349
+ fn sampleLightEntry(light: Light, pdf: f32, position: vec3<f32>, u: vec2<f32>) -> LightSample {
164
350
  var sample: LightSample;
165
351
  sample.valid = true;
166
352
  sample.kind = light.kind;
@@ -176,7 +362,7 @@ fn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSam
176
362
  return noLight();
177
363
  }
178
364
 
179
- let density = light.pdf * drawn.pdf;
365
+ let density = pdf * drawn.pdf;
180
366
 
181
367
  sample.direction = drawn.direction;
182
368
  sample.distance = 1e30;
@@ -191,7 +377,7 @@ fn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSam
191
377
  sample.direction = spreadDirection(light.direction, u);
192
378
  sample.distance = 1e30;
193
379
  // The colour already carries the intensity, which for a directional light is the irradiance it delivers.
194
- sample.value = light.color / light.pdf;
380
+ sample.value = light.color / pdf;
195
381
 
196
382
  return sample;
197
383
  }
@@ -220,7 +406,7 @@ fn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSam
220
406
 
221
407
  // Stop just short of the light so its own surface is not reported as the blocker.
222
408
  sample.distance = distance * (1.0 - 1e-3);
223
- let density = light.pdf * distanceSquared / cosLight;
409
+ let density = pdf * distanceSquared / cosLight;
224
410
  // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.
225
411
  let emission = select(light.color, emissionAt(light.triangle, bary), area);
226
412
 
@@ -260,7 +446,7 @@ fn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSam
260
446
 
261
447
  sample.direction = spreadDirection(towards, u);
262
448
  sample.distance = distance;
263
- sample.value = light.color * (power / light.pdf);
449
+ sample.value = light.color * (power / pdf);
264
450
 
265
451
  if (light.kind == LIGHT_SPOT) {
266
452
  sample.value = sample.value * spotLightTint(light, position);
@@ -277,7 +463,7 @@ fn blocksAll(m: u32) -> bool {
277
463
  let flags = materials[m].flags;
278
464
  let blending = materials[m].blending;
279
465
 
280
- return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME)) == 0u
466
+ return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME | MATERIAL_VOLUME)) == 0u
281
467
  && materials[m].alphaTest <= 0.0
282
468
  && materials[m].transmission <= 0.0
283
469
  && materials[m].clipCount == 0u
@@ -333,45 +519,63 @@ fn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {
333
519
  /**
334
520
  * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle
335
521
  * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall
336
- * lets its own share through, tinted.
522
+ * lets its own share through, tinted. A volume's boundary lets the ray in or out, and the medium inside takes its
523
+ * share over the distance between; medium is the density at the shaded point itself, zero outside any volume.
337
524
  */
338
- fn lightTransmittance(
339
- position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32) -> vec3<f32> {
340
- if (!chosen.occlude) {
525
+ fn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32,
526
+ medium: f32) -> vec3<f32> {
527
+ // A light that casts no shadow is blocked by nothing, but a medium on the way still takes its share: then the walk
528
+ // below only looks for volumes and passes every other surface.
529
+ if (!chosen.occlude && !VOLUMES) {
341
530
  return vec3<f32>(1.0);
342
531
  }
343
532
 
344
- // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the first
345
- // one found instead of looking for the closest. Anything that lets light through goes the long way below.
346
- let first = traceAny(position, chosen.direction, chosen.distance);
533
+ if (chosen.occlude) {
534
+ // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the
535
+ // first one found instead of looking for the closest. Anything that lets light through goes the long way below.
536
+ let first = traceAny(position, chosen.direction, chosen.distance);
347
537
 
348
- if (first == NO_HIT) {
349
- return vec3<f32>(1.0);
350
- }
538
+ if (first == NO_HIT) {
539
+ return vec3<f32>(exp(-medium * chosen.distance));
540
+ }
351
541
 
352
- let firstMaterial = materialIndexOf(first);
542
+ let firstMaterial = materialIndexOf(first);
353
543
 
354
- if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {
355
- return vec3<f32>(0.0);
544
+ if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {
545
+ return vec3<f32>(0.0);
546
+ }
356
547
  }
357
548
 
358
549
  var origin = position;
359
550
  var remaining = chosen.distance;
360
551
  var through = vec3<f32>(1.0);
552
+ // The medium the ray is in, and how far along the ray it entered it.
553
+ var density = medium;
554
+ var travelled = 0.0;
555
+ var enteredAt = 0.0;
361
556
 
362
557
  for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {
363
558
  let blocker = traceScene(origin, chosen.direction, remaining);
364
559
 
365
560
  if (blocker.triangle == NO_HIT) {
366
- return through;
561
+ return through * exp(-density * (travelled + remaining - enteredAt));
367
562
  }
368
563
 
369
564
  let bary = vec2<f32>(blocker.u, blocker.v);
370
565
  let m = materialIndexOf(blocker.triangle);
371
566
  let blending = materials[m].blending;
372
567
 
373
- // A shadow catcher catches shadows rather than casting them, and a surface that adds light blocks none.
374
- if (m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {
568
+ if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {
569
+ let at = travelled + blocker.distance;
570
+
571
+ if (dot(authoredNormal(blocker.triangle, bary), chosen.direction) < 0.0) {
572
+ density = select(0.0, materials[m].volume.w, frame.volumesEnabled != 0u);
573
+ enteredAt = at;
574
+ } else {
575
+ through = through * exp(-density * (at - enteredAt));
576
+ density = 0.0;
577
+ }
578
+ } else if (chosen.occlude && m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {
375
579
  let coverage = coverageAt(blocker.triangle, bary, chosen.direction);
376
580
 
377
581
  if (blending == BLEND_MULTIPLY) {
@@ -390,13 +594,15 @@ fn lightTransmittance(
390
594
  // The ray carries on from just past the surface it saw through.
391
595
  origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);
392
596
  remaining = remaining - blocker.distance - RAY_EPSILON;
597
+ travelled = travelled + blocker.distance + RAY_EPSILON;
393
598
 
394
599
  if (remaining <= RAY_EPSILON) {
395
- return through;
600
+ return through * exp(-density * (travelled - enteredAt));
396
601
  }
397
602
  }
398
603
 
399
- return vec3<f32>(0.0);
604
+ // Too many surfaces to see through: a shadow ray gives up dark, a ray that only counts the media keeps what it has.
605
+ return select(vec3<f32>(0.0), through, !chosen.occlude);
400
606
  }
401
607
 
402
608
  /**
@@ -418,13 +624,13 @@ fn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {
418
624
  continue;
419
625
  }
420
626
 
421
- let chosen = sampleLightEntry(light, position, sample2D(rng));
627
+ let chosen = sampleLightEntry(light, light.pdf, position, sample2D(rng));
422
628
 
423
629
  if (!chosen.valid) {
424
630
  continue;
425
631
  }
426
632
 
427
- let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL));
633
+ let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL, 0.0));
428
634
 
429
635
  shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));
430
636
  }