@tinypoly/gpu-renderer 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/ARCHITECTURE.md +334 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -2
  4. package/dist/accumulator.d.ts +27 -0
  5. package/dist/accumulator.d.ts.map +1 -0
  6. package/dist/accumulator.js +57 -0
  7. package/dist/accumulator.js.map +1 -0
  8. package/dist/bvh.d.ts +26 -0
  9. package/dist/bvh.d.ts.map +1 -0
  10. package/dist/bvh.js +207 -0
  11. package/dist/bvh.js.map +1 -0
  12. package/dist/camera.d.ts +29 -0
  13. package/dist/camera.d.ts.map +1 -0
  14. package/dist/camera.js +48 -0
  15. package/dist/camera.js.map +1 -0
  16. package/dist/canvas.d.ts +9 -0
  17. package/dist/canvas.d.ts.map +1 -0
  18. package/dist/canvas.js +11 -0
  19. package/dist/canvas.js.map +1 -0
  20. package/dist/denoise.d.ts +69 -0
  21. package/dist/denoise.d.ts.map +1 -0
  22. package/dist/denoise.js +455 -0
  23. package/dist/denoise.js.map +1 -0
  24. package/dist/device.d.ts +37 -0
  25. package/dist/device.d.ts.map +1 -0
  26. package/dist/device.js +52 -0
  27. package/dist/device.js.map +1 -0
  28. package/dist/dispatchScheduler.d.ts +43 -0
  29. package/dist/dispatchScheduler.d.ts.map +1 -0
  30. package/dist/dispatchScheduler.js +49 -0
  31. package/dist/dispatchScheduler.js.map +1 -0
  32. package/dist/environment.d.ts +39 -0
  33. package/dist/environment.d.ts.map +1 -0
  34. package/dist/environment.js +168 -0
  35. package/dist/environment.js.map +1 -0
  36. package/dist/frameLayout.d.ts +4 -0
  37. package/dist/frameLayout.d.ts.map +1 -0
  38. package/dist/frameLayout.js +58 -0
  39. package/dist/frameLayout.js.map +1 -0
  40. package/dist/geometry.d.ts +75 -0
  41. package/dist/geometry.d.ts.map +1 -0
  42. package/dist/geometry.js +671 -0
  43. package/dist/geometry.js.map +1 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +17 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lights.d.ts +64 -0
  49. package/dist/lights.d.ts.map +1 -0
  50. package/dist/lights.js +264 -0
  51. package/dist/lights.js.map +1 -0
  52. package/dist/materialLayout.d.ts +143 -0
  53. package/dist/materialLayout.d.ts.map +1 -0
  54. package/dist/materialLayout.js +297 -0
  55. package/dist/materialLayout.js.map +1 -0
  56. package/dist/math.d.ts +10 -0
  57. package/dist/math.d.ts.map +1 -0
  58. package/dist/math.js +40 -0
  59. package/dist/math.js.map +1 -0
  60. package/dist/pathTracePass.d.ts +27 -0
  61. package/dist/pathTracePass.d.ts.map +1 -0
  62. package/dist/pathTracePass.js +83 -0
  63. package/dist/pathTracePass.js.map +1 -0
  64. package/dist/planarCapture.d.ts +38 -0
  65. package/dist/planarCapture.d.ts.map +1 -0
  66. package/dist/planarCapture.js +163 -0
  67. package/dist/planarCapture.js.map +1 -0
  68. package/dist/presentPass.d.ts +31 -0
  69. package/dist/presentPass.d.ts.map +1 -0
  70. package/dist/presentPass.js +78 -0
  71. package/dist/presentPass.js.map +1 -0
  72. package/dist/renderer.d.ts +196 -0
  73. package/dist/renderer.d.ts.map +1 -0
  74. package/dist/renderer.js +839 -0
  75. package/dist/renderer.js.map +1 -0
  76. package/dist/sceneAssets.d.ts +24 -0
  77. package/dist/sceneAssets.d.ts.map +1 -0
  78. package/dist/sceneAssets.js +77 -0
  79. package/dist/sceneAssets.js.map +1 -0
  80. package/dist/sceneUpload.d.ts +61 -0
  81. package/dist/sceneUpload.d.ts.map +1 -0
  82. package/dist/sceneUpload.js +98 -0
  83. package/dist/sceneUpload.js.map +1 -0
  84. package/dist/shaderModule.d.ts +7 -0
  85. package/dist/shaderModule.d.ts.map +1 -0
  86. package/dist/shaderModule.js +19 -0
  87. package/dist/shaderModule.js.map +1 -0
  88. package/dist/shaderProgram.d.ts +124 -0
  89. package/dist/shaderProgram.d.ts.map +1 -0
  90. package/dist/shaderProgram.js +1030 -0
  91. package/dist/shaderProgram.js.map +1 -0
  92. package/dist/structLayout.d.ts +28 -0
  93. package/dist/structLayout.d.ts.map +1 -0
  94. package/dist/structLayout.js +58 -0
  95. package/dist/structLayout.js.map +1 -0
  96. package/dist/texturePack.d.ts +54 -0
  97. package/dist/texturePack.d.ts.map +1 -0
  98. package/dist/texturePack.js +86 -0
  99. package/dist/texturePack.js.map +1 -0
  100. package/dist/upstream.d.ts +7 -0
  101. package/dist/upstream.d.ts.map +1 -0
  102. package/dist/upstream.js +11 -0
  103. package/dist/upstream.js.map +1 -0
  104. package/dist/volumetric.d.ts +10 -0
  105. package/dist/volumetric.d.ts.map +1 -0
  106. package/dist/volumetric.js +34 -0
  107. package/dist/volumetric.js.map +1 -0
  108. package/dist/wgsl/bsdf.wgsl.d.ts +13 -0
  109. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -0
  110. package/dist/wgsl/bsdf.wgsl.js +619 -0
  111. package/dist/wgsl/bsdf.wgsl.js.map +1 -0
  112. package/dist/wgsl/bvh.wgsl.d.ts +3 -0
  113. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -0
  114. package/dist/wgsl/bvh.wgsl.js +218 -0
  115. package/dist/wgsl/bvh.wgsl.js.map +1 -0
  116. package/dist/wgsl/common.wgsl.d.ts +3 -0
  117. package/dist/wgsl/common.wgsl.d.ts.map +1 -0
  118. package/dist/wgsl/common.wgsl.js +193 -0
  119. package/dist/wgsl/common.wgsl.js.map +1 -0
  120. package/dist/wgsl/environment.wgsl.d.ts +10 -0
  121. package/dist/wgsl/environment.wgsl.d.ts.map +1 -0
  122. package/dist/wgsl/environment.wgsl.js +145 -0
  123. package/dist/wgsl/environment.wgsl.js.map +1 -0
  124. package/dist/wgsl/lights.wgsl.d.ts +10 -0
  125. package/dist/wgsl/lights.wgsl.d.ts.map +1 -0
  126. package/dist/wgsl/lights.wgsl.js +434 -0
  127. package/dist/wgsl/lights.wgsl.js.map +1 -0
  128. package/dist/wgsl/pathTrace.wgsl.d.ts +14 -0
  129. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -0
  130. package/dist/wgsl/pathTrace.wgsl.js +1020 -0
  131. package/dist/wgsl/pathTrace.wgsl.js.map +1 -0
  132. package/dist/wgsl/present.wgsl.d.ts +7 -0
  133. package/dist/wgsl/present.wgsl.d.ts.map +1 -0
  134. package/dist/wgsl/present.wgsl.js +154 -0
  135. package/dist/wgsl/present.wgsl.js.map +1 -0
  136. package/dist/wgsl/random.wgsl.d.ts +9 -0
  137. package/dist/wgsl/random.wgsl.d.ts.map +1 -0
  138. package/dist/wgsl/random.wgsl.js +91 -0
  139. package/dist/wgsl/random.wgsl.js.map +1 -0
  140. package/dist/wgsl/sampling.wgsl.d.ts +3 -0
  141. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -0
  142. package/dist/wgsl/sampling.wgsl.js +31 -0
  143. package/dist/wgsl/sampling.wgsl.js.map +1 -0
  144. package/dist/wgsl/shaderSupport.wgsl.d.ts +6 -0
  145. package/dist/wgsl/shaderSupport.wgsl.d.ts.map +1 -0
  146. package/dist/wgsl/shaderSupport.wgsl.js +120 -0
  147. package/dist/wgsl/shaderSupport.wgsl.js.map +1 -0
  148. package/dist/wgsl/surface.wgsl.d.ts +10 -0
  149. package/dist/wgsl/surface.wgsl.d.ts.map +1 -0
  150. package/dist/wgsl/surface.wgsl.js +794 -0
  151. package/dist/wgsl/surface.wgsl.js.map +1 -0
  152. package/dist/wgsl/texture.wgsl.d.ts +11 -0
  153. package/dist/wgsl/texture.wgsl.d.ts.map +1 -0
  154. package/dist/wgsl/texture.wgsl.js +132 -0
  155. package/dist/wgsl/texture.wgsl.js.map +1 -0
  156. package/package.json +76 -3
  157. package/src/accumulator.ts +64 -0
  158. package/src/bvh.ts +272 -0
  159. package/src/camera.ts +77 -0
  160. package/src/canvas.ts +13 -0
  161. package/src/denoise.ts +514 -0
  162. package/src/device.ts +81 -0
  163. package/src/dispatchScheduler.ts +71 -0
  164. package/src/environment.ts +233 -0
  165. package/src/frameLayout.ts +59 -0
  166. package/src/geometry.ts +899 -0
  167. package/src/index.ts +61 -0
  168. package/src/lights.ts +365 -0
  169. package/src/materialLayout.ts +408 -0
  170. package/src/math.ts +48 -0
  171. package/src/pathTracePass.ts +97 -0
  172. package/src/planarCapture.ts +225 -0
  173. package/src/presentPass.ts +99 -0
  174. package/src/renderer.ts +1117 -0
  175. package/src/sceneAssets.ts +118 -0
  176. package/src/sceneUpload.ts +185 -0
  177. package/src/shaderModule.ts +19 -0
  178. package/src/shaderProgram.ts +1370 -0
  179. package/src/structLayout.ts +93 -0
  180. package/src/texturePack.ts +152 -0
  181. package/src/upstream.ts +56 -0
  182. package/src/volumetric.ts +42 -0
  183. package/src/wgsl/bsdf.wgsl.ts +618 -0
  184. package/src/wgsl/bvh.wgsl.ts +217 -0
  185. package/src/wgsl/common.wgsl.ts +193 -0
  186. package/src/wgsl/environment.wgsl.ts +144 -0
  187. package/src/wgsl/lights.wgsl.ts +433 -0
  188. package/src/wgsl/pathTrace.wgsl.ts +1019 -0
  189. package/src/wgsl/present.wgsl.ts +153 -0
  190. package/src/wgsl/random.wgsl.ts +90 -0
  191. package/src/wgsl/sampling.wgsl.ts +30 -0
  192. package/src/wgsl/shaderSupport.wgsl.ts +119 -0
  193. package/src/wgsl/surface.wgsl.ts +793 -0
  194. package/src/wgsl/texture.wgsl.ts +131 -0
@@ -0,0 +1,1020 @@
1
+ /**
2
+ * The integrator: camera rays, the bounce loop and the accumulation buffer.
3
+ *
4
+ * Light reaches a path two ways, and the two are combined with multiple importance sampling: a shadow ray towards a
5
+ * light at every vertex, and whatever the sampled bounce direction happens to run into. Each contribution is weighted by
6
+ * the power heuristic, which keeps the pair unbiased while each strategy covers the other's worst case. A small bright
7
+ * emitter is found by the shadow ray; a sharp reflection of a large one is found by the bounce.
8
+ *
9
+ * What only the camera sees is handled on the camera's own line of sight, the part of a path before its first
10
+ * reflection: culled faces, the background, Three's fog, a light map's baked light and the coverage a transparent
11
+ * background turns into alpha. Transmission keeps a path on that line, as Three's transmission shows what is behind.
12
+ */
13
+ export const PATH_TRACE_WGSL = /* wgsl */ `
14
+ /** Light from an ambient or hemisphere light, which this renderer turns into an equally bright surrounding. */
15
+ fn ambientRadiance(direction: vec3<f32>) -> vec3<f32> {
16
+ let latitude = clamp(dot(direction, frame.ambientAxis.xyz) * 0.5 + 0.5, 0.0, 1.0);
17
+
18
+ return mix(frame.ambientGround.rgb, frame.ambientSky.rgb, latitude) + probeRadiance(direction);
19
+ }
20
+
21
+ /**
22
+ * A LightProbe's light. Three's coefficients are radiance projected on the SH3 basis, which its shader convolves with
23
+ * the cosine to get irradiance; evaluated directly they are the surrounding that delivers exactly that irradiance.
24
+ */
25
+ fn probeRadiance(d: vec3<f32>) -> vec3<f32> {
26
+ let radiance = frame.probe0.rgb * 0.282095
27
+ + frame.probe1.rgb * (0.488603 * d.y)
28
+ + frame.probe2.rgb * (0.488603 * d.z)
29
+ + frame.probe3.rgb * (0.488603 * d.x)
30
+ + frame.probe4.rgb * (1.092548 * d.x * d.y)
31
+ + frame.probe5.rgb * (1.092548 * d.y * d.z)
32
+ + frame.probe6.rgb * (0.315392 * (3.0 * d.z * d.z - 1.0))
33
+ + frame.probe7.rgb * (1.092548 * d.x * d.z)
34
+ + frame.probe8.rgb * (0.546274 * (d.x * d.x - d.y * d.y));
35
+
36
+ return max(radiance, vec3<f32>(0.0));
37
+ }
38
+
39
+ /** What the camera sees where nothing is in the way, as the background setting says. */
40
+ fn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {
41
+ if (frame.background.w == 1.0) {
42
+ return frame.background.rgb;
43
+ }
44
+
45
+ // A procedural sky paints the background without the strength it lights with, and its foreground over it.
46
+ if (frame.skyProgram != NO_PROGRAM || frame.skyIntensity > 0.0) {
47
+ if (frame.skyBackground == 0u) {
48
+ return vec3<f32>(0.0);
49
+ }
50
+
51
+ if (frame.skyProgram != NO_PROGRAM) {
52
+ var input: ShaderInput;
53
+ input.data = frame.skyData;
54
+ input.direction = direction;
55
+ input.diffuseColor = vec4<f32>(1.0);
56
+
57
+ return runProgram(frame.skyProgram, input).color.rgb;
58
+ }
59
+
60
+ return environmentRadiance(direction) / frame.skyIntensity;
61
+ }
62
+
63
+ return environmentRadiance(direction);
64
+ }
65
+
66
+ /**
67
+ * How much of the sky a bounce ray that escaped keeps. Direct sampling aims at the bright parts of an image too, so the
68
+ * two share it; when direct sampling took all of it, as it does from a toon surface, the bounce keeps none.
69
+ */
70
+ fn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f32 {
71
+ if (frame.environmentPick <= 0.0) {
72
+ return 1.0;
73
+ }
74
+
75
+ if (lightsOwnDirect) {
76
+ return 0.0;
77
+ }
78
+
79
+ if (bsdfPdf <= 0.0) {
80
+ return 1.0;
81
+ }
82
+
83
+ return misWeight(bsdfPdf, frame.environmentPick * environmentPdf(direction));
84
+ }
85
+
86
+ /** Scales a contribution down to the luminance ceiling, which trades a little bias for the fireflies it removes. */
87
+ fn clampIndirect(contribution: vec3<f32>) -> vec3<f32> {
88
+ let luminance = luminanceOf(contribution);
89
+
90
+ if (frame.clampIndirect <= 0.0 || luminance <= frame.clampIndirect) {
91
+ return contribution;
92
+ }
93
+
94
+ return contribution * (frame.clampIndirect / luminance);
95
+ }
96
+
97
+ /**
98
+ * How much of the emission a bounce ray found the renderer keeps. Direct sampling had its own chance at this triangle,
99
+ * and the heuristic splits the two so neither counts it twice.
100
+ */
101
+ fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f32 {
102
+ if (frame.areaLightCount == 0u) {
103
+ return 1.0;
104
+ }
105
+
106
+ let areaPdf = lightPdfOfTriangle(hit.triangle);
107
+
108
+ if (areaPdf <= 0.0) {
109
+ return 1.0;
110
+ }
111
+
112
+ if (lightsOwnDirect) {
113
+ return 0.0;
114
+ }
115
+
116
+ if (bsdfPdf <= 0.0) {
117
+ return 1.0;
118
+ }
119
+
120
+ let cosLight = max(abs(dot(triangleNormal(hit.triangle), direction)), 1e-6);
121
+
122
+ return misWeight(bsdfPdf, areaPdf * hit.distance * hit.distance / cosLight);
123
+ }
124
+
125
+ /** One shadow ray from the surface towards a light, weighted against the chance the bounce would have found it too. */
126
+ fn directLight(
127
+ surface: Surface,
128
+ position: vec3<f32>,
129
+ view: vec3<f32>,
130
+ pick: f32,
131
+ u: vec2<f32>,
132
+ rng: ptr<function, PathSampler>,
133
+ diffusePart: ptr<function, vec3<f32>>) -> vec3<f32> {
134
+ *diffusePart = vec3<f32>(0.0);
135
+
136
+ let chosen = sampleLight(position, pick, u);
137
+
138
+ if (!chosen.valid) {
139
+ return vec3<f32>(0.0);
140
+ }
141
+
142
+ // A material with its own environment map sees that map in place of the scene's surroundings.
143
+ if (chosen.kind == LIGHT_ENVIRONMENT && surface.envMap != NO_TEXTURE) {
144
+ return vec3<f32>(0.0);
145
+ }
146
+
147
+ let reflected = evaluateBsdf(surface, view, chosen.direction);
148
+
149
+ if (all(reflected.value <= vec3<f32>(0.0))) {
150
+ return vec3<f32>(0.0);
151
+ }
152
+
153
+ let toon = surface.kind == KIND_TOON;
154
+ // Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.
155
+ let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);
156
+ let through = lightTransmittance(position, chosen, rng, skip);
157
+
158
+ if (all(through <= vec3<f32>(0.0))) {
159
+ return vec3<f32>(0.0);
160
+ }
161
+
162
+ // A light that no bounce ray could have found keeps its whole contribution; an emissive triangle shares it. A toon
163
+ // surface takes all of its direct light this way, because its bounce is not the lobe that lit it.
164
+ let weight = select(1.0, misWeight(chosen.misPdf, reflected.pdf), chosen.misPdf > 0.0 && reflected.pdf > 0.0 && !toon);
165
+
166
+ let light = chosen.value * through * weight;
167
+
168
+ // The share of it the diffuse lobe reflects, which the denoiser filters apart from the specular one.
169
+ *diffusePart = min(diffuseValue(surface, view, chosen.direction), reflected.value) * light;
170
+
171
+ return reflected.value * light;
172
+ }
173
+
174
+ /** Three's fog factor at a view depth: linear between near and far, or exponential squared. */
175
+ fn fogFactor(depth: f32, kind: f32) -> f32 {
176
+ if (kind == 2.0) {
177
+ let density = frame.fogParams.z;
178
+
179
+ return clamp(1.0 - exp(-density * density * depth * depth), 0.0, 1.0);
180
+ }
181
+
182
+ return smoothstep(frame.fogParams.x, frame.fogParams.y, depth);
183
+ }
184
+
185
+ /** The colour the diffuse signal is divided by at this surface; white where the surface shows what it emits. */
186
+ fn diffuseGuide(surface: Surface) -> vec3<f32> {
187
+ if ((surface.flags & MATERIAL_LIT) == 0u) {
188
+ return vec3<f32>(1.0);
189
+ }
190
+
191
+ return surface.albedo * (1.0 - surface.metalness) * (1.0 - clamp(surface.transmission, 0.0, 1.0));
192
+ }
193
+
194
+ struct PathResult {
195
+ radiance: vec3<f32>,
196
+ alpha: f32,
197
+ // How far along the camera ray the first surface it stopped at is, or 1e30 when it stopped at none.
198
+ depth: f32,
199
+ // The denoiser's two signals: what of the radiance came through the first surface's diffuse lobe (and what the camera
200
+ // sees directly), the rest being specular, and what guides each at that surface.
201
+ diffuse: vec3<f32>,
202
+ diffuseAlbedo: vec3<f32>,
203
+ specularAlbedo: vec3<f32>,
204
+ normal: vec3<f32>,
205
+ // Along the camera ray to the first surface, its roughness, and how far the specular continuation went from it.
206
+ guideDistance: f32,
207
+ roughness: f32,
208
+ specularDistance: f32,
209
+ };
210
+
211
+ /** How many surfaces a path may pass through without scattering (cutouts, culled faces, glass) before it gives up. */
212
+ const MAX_STEPS: u32 = 64u;
213
+
214
+ fn traceRadiance(
215
+ rayOrigin: vec3<f32>, rayDirection: vec3<f32>, rayLength: f32, rng: ptr<function, PathSampler>) -> PathResult {
216
+ var radiance = vec3<f32>(0.0);
217
+ var throughput = vec3<f32>(1.0);
218
+ var origin = rayOrigin;
219
+ var direction = rayDirection;
220
+ var limit = rayLength;
221
+ // Solid angle density of the current direction; zero for the camera ray, which no light sampling competes with.
222
+ var bsdfPdf = 0.0;
223
+ // What the volume the ray is travelling through absorbs per unit of distance; zero outside any.
224
+ var absorption = vec3<f32>(0.0);
225
+ // Still on the camera's own line of sight, which transmission keeps a path on.
226
+ var primary = true;
227
+ // Still the camera ray itself, in a straight line: only this one is culled and clipped by the far plane.
228
+ var straight = true;
229
+ var alpha = 0.0;
230
+ var bounce = 0u;
231
+ // The vertex the path last scattered at took all of its direct light by sampling lights: a toon surface.
232
+ var lightsOwnDirect = false;
233
+ // That vertex's own environment map, which a ray escaping from it sees instead of the scene's.
234
+ var escapeMap = NO_TEXTURE;
235
+ var escapeIntensity = 1.0;
236
+ // Which colour a dispersive path carries, once a dispersive volume has split it; all three before that.
237
+ var channel = vec3<f32>(1.0);
238
+ // Three's fog, laid over everything the camera sees through the first surface.
239
+ var fogged = false;
240
+ var fogAmount = 0.0;
241
+ var fogStart = vec3<f32>(0.0);
242
+ var fogThroughput = vec3<f32>(0.0);
243
+ // A built-in material's hooks after <opaque_fragment> edit the colour the camera sees of it, which is everything the
244
+ // path gathers from that surface on.
245
+ var afterMaterial = NO_MATERIAL;
246
+ var afterTriangle = 0u;
247
+ var afterBary = vec2<f32>(0.0);
248
+ var afterDirection = vec3<f32>(0.0);
249
+ var afterAlbedo = vec4<f32>(0.0);
250
+ var afterStart = vec3<f32>(0.0);
251
+ var afterThroughput = vec3<f32>(0.0);
252
+ var depth = 1e30;
253
+ // The denoiser's guides, taken at the first surface the camera ray stops at.
254
+ var guided = false;
255
+ var diffuseAlbedo = vec3<f32>(1.0);
256
+ var specularAlbedoGuide = vec3<f32>(1.0);
257
+ var guideNormal = vec3<f32>(0.0);
258
+ var guideDistance = 1e6;
259
+ var guideRoughness = 1.0;
260
+ // Where the path first scattered, which decides which of the two signals the rest of it feeds.
261
+ var split = false;
262
+ var splitBase = vec3<f32>(0.0);
263
+ var splitDirect = vec3<f32>(0.0);
264
+ var splitDirectDiffuse = vec3<f32>(0.0);
265
+ var splitDiffuse = true;
266
+ var waitingForSpecular = false;
267
+ var specularDistance = 1e6;
268
+
269
+ for (var step = 0u; step < MAX_STEPS; step = step + 1u) {
270
+ let hit = traceScene(origin, direction, limit);
271
+
272
+ // The specular continuation's first hit: how far the reflection the denoiser filters actually is.
273
+ if (waitingForSpecular) {
274
+ specularDistance = select(min(hit.distance, 1e6), 1e6, hit.triangle == NO_HIT);
275
+ waitingForSpecular = false;
276
+ }
277
+
278
+ if (hit.triangle == NO_HIT) {
279
+ if (primary) {
280
+ // A transparent background leaves the pixel empty, which its alpha says.
281
+ if (frame.background.w != 2.0) {
282
+ alpha = 1.0;
283
+ radiance = radiance + throughput * backgroundRadiance(direction);
284
+ }
285
+ } else {
286
+ var sky = ambientRadiance(direction);
287
+
288
+ if (escapeMap != NO_TEXTURE) {
289
+ sky = sky + sampleEquirect(escapeMap, direction).rgb * escapeIntensity;
290
+ } else {
291
+ sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect);
292
+ }
293
+
294
+ let contribution = throughput * sky;
295
+
296
+ radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);
297
+ }
298
+
299
+ break;
300
+ }
301
+
302
+ throughput = throughput * exp(-absorption * hit.distance);
303
+
304
+ let bary = vec2<f32>(hit.u, hit.v);
305
+ let point = origin + direction * hit.distance;
306
+ let m = materialIndexOf(hit.triangle);
307
+
308
+ // Every vertex draws the same dimensions in the same order, whatever the path then does with them.
309
+ let coverage = sample2D(rng);
310
+ let pick = sample2D(rng);
311
+ let lightSample = sample2D(rng);
312
+ let lobe = sample2D(rng);
313
+ let bounceSample = sample2D(rng);
314
+
315
+ // Three culls the side a material does not draw. Only the camera's rays are culled: a mesh still casts shadows and
316
+ // holds light in from both sides.
317
+ // A planar capture drops what lies behind its plane, for the camera's own ray.
318
+ let clipped = any(frame.clipPlane != vec4<f32>(0.0)) && dot(frame.clipPlane.xyz, point) + frame.clipPlane.w < 0.0;
319
+
320
+ if (straight && (clipped || isCulled(m, hit.triangle, direction))) {
321
+ origin = point + direction * RAY_EPSILON;
322
+ limit = limit - hit.distance;
323
+ continue;
324
+ }
325
+
326
+ if (isClipped(m, point)
327
+ || ((materials[m].flags & MATERIAL_WIREFRAME) != 0u && !onWireframe(hit.triangle, point))) {
328
+ origin = point + direction * RAY_EPSILON;
329
+ limit = select(1e30, limit - hit.distance, straight);
330
+ continue;
331
+ }
332
+
333
+ let surface = resolveSurface(hit.triangle, bary, direction);
334
+
335
+ // Blending other than normal: the surface adds its light to, or filters, whatever is behind it, and the path goes on.
336
+ if (surface.blending == BLEND_ADDITIVE || surface.blending == BLEND_MULTIPLY || surface.blending == BLEND_SUBTRACTIVE) {
337
+ // Three blends these with the fragment's alpha whether or not the material is transparent.
338
+ let strength = surface.opacity * clamp(surface.alpha, 0.0, 1.0);
339
+ let color = select(surface.albedo, surface.emissive, (surface.flags & MATERIAL_LIT) == 0u);
340
+
341
+ if (surface.blending == BLEND_ADDITIVE) {
342
+ var added = surface.emissive;
343
+
344
+ if ((surface.flags & MATERIAL_LIT) != 0u) {
345
+ var unused: vec3<f32>;
346
+ added = added + directLight(surface, point + surface.normal * RAY_EPSILON, -direction, pick.x, lightSample, rng,
347
+ &unused);
348
+ }
349
+
350
+ radiance = radiance + throughput * added * strength;
351
+ } else if (surface.blending == BLEND_MULTIPLY) {
352
+ throughput = throughput * mix(vec3<f32>(1.0), color, strength);
353
+ } else {
354
+ throughput = throughput * (vec3<f32>(1.0) - color * strength);
355
+ }
356
+
357
+ origin = point + direction * RAY_EPSILON;
358
+ limit = select(1e30, limit - hit.distance, straight);
359
+ continue;
360
+ }
361
+
362
+ // A surface that only partly covers its triangle lets the ray straight through the rest of the time. The direction
363
+ // does not change, so the density that found it still stands and the next hit is weighted against the same one.
364
+ if (surface.opacity < 1.0 && coverage.x >= surface.opacity) {
365
+ origin = point + direction * RAY_EPSILON;
366
+ limit = select(1e30, limit - hit.distance, straight);
367
+ continue;
368
+ }
369
+
370
+ // Three's ShadowMaterial shows its colour only where a light is blocked, and nothing elsewhere.
371
+ if (surface.kind == KIND_SHADOW) {
372
+ let shadow = shadowMaskAt(point + surface.facing * RAY_EPSILON, rng);
373
+
374
+ if (coverage.y >= shadow * clamp(materials[m].color.w, 0.0, 1.0)) {
375
+ origin = point + direction * RAY_EPSILON;
376
+ limit = select(1e30, limit - hit.distance, straight);
377
+ continue;
378
+ }
379
+
380
+ if (primary) {
381
+ alpha = 1.0;
382
+ }
383
+
384
+ radiance = radiance + throughput * surface.albedo;
385
+ break;
386
+ }
387
+
388
+ if (straight && depth >= 1e30) {
389
+ depth = length(point - rayOrigin);
390
+ }
391
+
392
+ if (straight && !guided) {
393
+ guided = true;
394
+ diffuseAlbedo = diffuseGuide(surface);
395
+ specularAlbedoGuide = select(specularAlbedo(surface, -direction), vec3<f32>(1.0), (surface.flags & MATERIAL_LIT) == 0u);
396
+ guideNormal = surface.normal;
397
+ guideDistance = length(point - rayOrigin);
398
+ guideRoughness = surface.roughness;
399
+
400
+ if (surface.kind == KIND_PHONG) {
401
+ // Phong's exponent as the roughness of the GGX lobe that is about as wide.
402
+ guideRoughness = pow(2.0 / (surface.shininess + 2.0), 0.25);
403
+ }
404
+ }
405
+
406
+ if (primary) {
407
+ alpha = 1.0;
408
+
409
+ if (!fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {
410
+ afterMaterial = m;
411
+ afterTriangle = hit.triangle;
412
+ afterBary = bary;
413
+ afterDirection = direction;
414
+ afterAlbedo = vec4<f32>(surface.albedo, surface.alpha);
415
+ afterStart = radiance;
416
+ afterThroughput = throughput;
417
+ }
418
+
419
+ if (!fogged) {
420
+ let everySurface = frame.fogParams.w > 0.0;
421
+ let kind = frame.fogColor.w;
422
+
423
+ if (everySurface || (kind > 0.0 && (surface.flags & MATERIAL_FOG) != 0u)) {
424
+ let depth = -(frame.worldToCamera * vec4<f32>(point, 1.0)).z;
425
+
426
+ fogged = true;
427
+ fogAmount = fogFactor(depth, select(kind, 1.0, everySurface));
428
+ fogStart = radiance;
429
+ fogThroughput = throughput;
430
+ }
431
+ }
432
+
433
+ radiance = radiance + throughput * surface.lightMap;
434
+ }
435
+
436
+ if (any(surface.emissive > vec3<f32>(0.0))) {
437
+ let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect);
438
+
439
+ // Seen by the camera or one bounce away this is direct light and stays exact; deeper it may be clamped.
440
+ radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);
441
+ }
442
+
443
+ // Three does not light a basic, points, line or sprite material, so nothing carries on from one.
444
+ if ((surface.flags & MATERIAL_LIT) == 0u || bounce >= frame.bounces) {
445
+ break;
446
+ }
447
+
448
+ // Lambert and Phong blend their own environment map over the lit colour, which here is a share of the path.
449
+ if (surface.legacyAmount > 0.0) {
450
+ let amount = surface.legacyAmount;
451
+
452
+ if (surface.combine == 1u) {
453
+ radiance = radiance + throughput * surface.legacyEnv * amount;
454
+ throughput = throughput * (1.0 - amount);
455
+ } else if (surface.combine == 2u) {
456
+ radiance = radiance + throughput * surface.legacyEnv * amount;
457
+ } else {
458
+ throughput = throughput * mix(vec3<f32>(1.0), surface.legacyEnv, amount);
459
+ }
460
+ }
461
+
462
+ let view = -direction;
463
+ var next = direction;
464
+ var nextOrigin = point + direction * RAY_EPSILON;
465
+
466
+ // Transmission replaces part of the diffuse layer, so that share of the time the surface is a dielectric instead.
467
+ // Choosing it in proportion to its weight leaves both halves unweighted, and only the opaque half aims at a light.
468
+ if (surface.transmission > 0.0 && lobe.y < surface.transmission) {
469
+ var ior = surface.ior;
470
+
471
+ // Three's dispersion: each colour refracts with its own index, spread around the material's. The path picks one
472
+ // colour the first time it enters such a volume and carries only that one from then on.
473
+ if (surface.dispersion > 0.0 && surface.thickness > 0.0) {
474
+ if (all(channel == vec3<f32>(1.0))) {
475
+ let which = min(u32(coverage.y * 3.0), 2u);
476
+
477
+ channel = vec3<f32>(select(0.0, 1.0, which == 0u), select(0.0, 1.0, which == 1u), select(0.0, 1.0, which == 2u));
478
+ throughput = throughput * channel * 3.0;
479
+ }
480
+
481
+ let spread = (ior - 1.0) * 0.025 * surface.dispersion;
482
+
483
+ ior = dot(channel, vec3<f32>(ior - spread, ior, ior + spread));
484
+ }
485
+
486
+ let crossing = sampleDielectric(surface, view, ior, lobe.y / surface.transmission, bounceSample);
487
+
488
+ if (!crossing.valid) {
489
+ break;
490
+ }
491
+
492
+ // Bending or reflecting off glass is the specular signal.
493
+ if (!split && !crossing.straight) {
494
+ split = true;
495
+ splitBase = radiance;
496
+ splitDiffuse = false;
497
+ waitingForSpecular = true;
498
+ }
499
+
500
+ throughput = throughput * crossing.weight;
501
+
502
+ // Straight through a thin wall the direction stands, and with it the density that will weigh the next hit.
503
+ if (!crossing.straight) {
504
+ straight = false;
505
+ next = crossing.direction;
506
+ nextOrigin = point + surface.normal * select(RAY_EPSILON, -RAY_EPSILON, crossing.crossed);
507
+ // A direction no light sampling could have produced: whatever the path finds next keeps its whole value.
508
+ bsdfPdf = 0.0;
509
+ lightsOwnDirect = false;
510
+ escapeMap = surface.envMap;
511
+ escapeIntensity = surface.envMapIntensity;
512
+
513
+ if (crossing.crossed) {
514
+ absorption = select(vec3<f32>(0.0), surface.absorption, surface.front);
515
+ } else {
516
+ primary = false;
517
+ }
518
+ }
519
+
520
+ if (!primary) {
521
+ bounce = bounce + 1u;
522
+ }
523
+ } else {
524
+ let position = point + surface.normal * RAY_EPSILON;
525
+ var directDiffuse: vec3<f32>;
526
+ let direct = throughput * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse);
527
+ let added = select(direct, clampIndirect(direct), bounce > 0u);
528
+ let before = radiance;
529
+
530
+ radiance = radiance + added;
531
+
532
+ let scattered = sampleBsdf(surface, view, lobe.x, bounceSample);
533
+
534
+ // The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
535
+ if (!split) {
536
+ split = true;
537
+ splitBase = before;
538
+ splitDirect = added;
539
+ splitDirectDiffuse = throughput * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),
540
+ bounce > 0u);
541
+ splitDiffuse = !scattered.valid || scattered.diffuse;
542
+ waitingForSpecular = scattered.valid && !scattered.diffuse;
543
+ }
544
+
545
+ if (!scattered.valid) {
546
+ break;
547
+ }
548
+
549
+ throughput = throughput * scattered.weight;
550
+ bsdfPdf = scattered.pdf;
551
+ lightsOwnDirect = surface.kind == KIND_TOON;
552
+ escapeMap = surface.envMap;
553
+ escapeIntensity = surface.envMapIntensity;
554
+ next = scattered.direction;
555
+ nextOrigin = position;
556
+ primary = false;
557
+ straight = false;
558
+ bounce = bounce + 1u;
559
+ }
560
+
561
+ // A path that carries almost nothing is ended by chance rather than by a cut, which keeps the estimate unbiased.
562
+ let survival = max(throughput.r, max(throughput.g, throughput.b));
563
+
564
+ if (survival <= 0.0) {
565
+ break;
566
+ }
567
+
568
+ if (survival < 0.2) {
569
+ if (pick.y > survival) {
570
+ break;
571
+ }
572
+
573
+ throughput = throughput / survival;
574
+ }
575
+
576
+ origin = nextOrigin;
577
+ limit = select(1e30, limit - hit.distance, straight);
578
+ direction = next;
579
+ }
580
+
581
+ // The diffuse signal: everything before the first scatter, its diffuse direct light, and the rest if the diffuse lobe
582
+ // carried the path on. A path that never scattered is all diffuse, which is what the camera sees directly.
583
+ var diffuse = radiance;
584
+
585
+ if (split) {
586
+ let after = radiance - splitBase - splitDirect;
587
+ diffuse = splitBase + splitDirectDiffuse + select(vec3<f32>(0.0), after, splitDiffuse);
588
+ }
589
+
590
+ if (afterMaterial != NO_MATERIAL && all(afterThroughput > vec3<f32>(0.0))) {
591
+ var input = shaderInputAt(afterMaterial, afterTriangle, afterBary, afterDirection);
592
+ input.data = materials[afterMaterial].afterData;
593
+ input.diffuseColor = afterAlbedo;
594
+ input.fragColor = vec4<f32>((radiance - afterStart) / afterThroughput, 1.0);
595
+
596
+ let edited = runProgram(materials[afterMaterial].afterProgram, input);
597
+
598
+ radiance = afterStart + afterThroughput * edited.color.rgb;
599
+ // A hook rewrites the colour as a whole: there is no split left to keep.
600
+ diffuse = radiance;
601
+ }
602
+
603
+ // Three lays its fog after the hooks, over what they made of the surface.
604
+ if (fogged) {
605
+ let fog = frame.fogColor.rgb * fogThroughput * fogAmount;
606
+
607
+ radiance = fogStart + (radiance - fogStart) * (1.0 - fogAmount) + fog;
608
+ // What came before the fogged surface is diffuse, so the fog's own colour joins that signal.
609
+ diffuse = fogStart + (diffuse - fogStart) * (1.0 - fogAmount) + fog;
610
+ }
611
+
612
+ var result: PathResult;
613
+ result.radiance = radiance;
614
+ result.alpha = alpha;
615
+ result.depth = depth;
616
+ result.diffuse = diffuse;
617
+ result.diffuseAlbedo = diffuseAlbedo;
618
+ result.specularAlbedo = specularAlbedoGuide;
619
+ result.normal = guideNormal;
620
+ result.guideDistance = guideDistance;
621
+ result.roughness = guideRoughness;
622
+ result.specularDistance = specularDistance;
623
+
624
+ return result;
625
+ }
626
+
627
+ /** A point on the lens: a disk, or Three-free polygon bokeh shaped as the CPU renderer shapes it. */
628
+ fn lensPoint(u: vec2<f32>) -> vec2<f32> {
629
+ var radius = sqrt(u.x) * frame.lens.x;
630
+ let angle = u.y * 2.0 * PI;
631
+ let blades = frame.lens.z;
632
+
633
+ if (blades >= 3.0) {
634
+ let wedge = 2.0 * PI / blades;
635
+ radius = radius * cos(PI / blades) / cos((angle - floor(angle / wedge) * wedge) - PI / blades);
636
+ }
637
+
638
+ return vec2<f32>(cos(angle), sin(angle)) * radius;
639
+ }
640
+
641
+ /**
642
+ * Where a sample's camera ray starts, which way it points and how far it may go, for any camera: the ray is cast through
643
+ * the inverse of the camera's own projection, from the near plane to the far one, which clip it as Three clips a
644
+ * fragment. A lens moves the origin across its aperture and aims at the point in focus.
645
+ */
646
+ fn cameraRay(pixel: vec2<u32>, jitter: vec2<f32>, lens: vec2<f32>) -> array<vec4<f32>, 2> {
647
+ let uv = (vec2<f32>(pixel) + jitter) / vec2<f32>(frame.resolution);
648
+ let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
649
+ let ray = ndcRay(ndc);
650
+
651
+ if (frame.lens.x <= 0.0 || frame.cameraKind == 1u) {
652
+ return ray;
653
+ }
654
+
655
+ let position = frame.cameraToWorld[3].xyz;
656
+ let forward = -normalize(frame.cameraToWorld[2].xyz);
657
+ let focus = position + ray[1].xyz * (frame.lens.y / max(dot(ray[1].xyz, forward), 1e-6));
658
+ let offset = lensPoint(lens);
659
+ let origin = position + normalize(frame.cameraToWorld[0].xyz) * offset.x + normalize(frame.cameraToWorld[1].xyz) * offset.y;
660
+
661
+ return array<vec4<f32>, 2>(vec4<f32>(origin, 0.0), vec4<f32>(normalize(focus - origin), 1e30));
662
+ }
663
+
664
+ /** The window depth GL would store for the first surface the camera ray stopped at: one where it stopped at none. */
665
+ fn windowDepth(point: vec3<f32>, distance: f32) -> f32 {
666
+ if (distance >= 1e30) {
667
+ return 1.0;
668
+ }
669
+
670
+ let clip = frame.projection * (frame.worldToCamera * vec4<f32>(point, 1.0));
671
+
672
+ return clamp(clip.z / clip.w * 0.5 + 0.5, 0.0, 1.0);
673
+ }
674
+
675
+ /** smoothstep as the CPU renderer writes it, which also holds when the two edges meet. */
676
+ fn edge(low: f32, high: f32, value: f32) -> f32 {
677
+ let t = select(select(0.0, 1.0, value >= high), clamp((value - low) / (high - low), 0.0, 1.0), high > low);
678
+
679
+ return t * t * (3.0 - 2.0 * t);
680
+ }
681
+
682
+ /** How much of what lies at this point the fog lets through to the camera. */
683
+ fn fogVisibility(point: vec3<f32>) -> f32 {
684
+ let everySurface = frame.fogParams.w > 0.0;
685
+ let kind = frame.fogColor.w;
686
+
687
+ if (!everySurface && kind <= 0.0) {
688
+ return 1.0;
689
+ }
690
+
691
+ let depth = -(frame.worldToCamera * vec4<f32>(point, 1.0)).z;
692
+
693
+ return 1.0 - fogFactor(depth, select(kind, 1.0, everySurface));
694
+ }
695
+
696
+ /** The most volumetric lights one view ray marches through. */
697
+ const MAX_VOLUMETRIC: u32 = 8u;
698
+
699
+ /**
700
+ * Steps of one march. The CPU renderer marches 64 once per pixel; here every path marches, a few hundred times a
701
+ * pixel, each at its own random offset, so a few steps a path add up to a finer integral than 64 once, at a fraction of
702
+ * the shadow rays.
703
+ */
704
+ const MARCH_STEPS: u32 = 8u;
705
+
706
+ /**
707
+ * The CPU renderer's volumetric lights (volumetricLight.js): one march across the union of every light's
708
+ * reach along the view ray, Henyey-Greenstein scattering towards the camera, a shadow ray for a spot light that casts
709
+ * one, and the same compression of the sum before it is added over the image.
710
+ */
711
+ fn volumetricGlow(origin: vec3<f32>, direction: vec3<f32>, distance: f32, jitter: f32) -> vec3<f32> {
712
+ let count = min(frame.volumetricCount, MAX_VOLUMETRIC);
713
+ let far = min(distance, 40.0);
714
+ var tStart = far;
715
+ var tEnd = 0.0;
716
+
717
+ for (var k = 0u; k < count; k = k + 1u) {
718
+ let at = frame.volumetricData + k * 20u;
719
+ let position = extraVec4(at).xyz;
720
+ let spot = extraFloat(at + 3u) > 0.5;
721
+ let axis = extraVec4(at + 4u).xyz;
722
+ let radius = extraFloat(at + 7u);
723
+ let outer = extraFloat(at + 15u);
724
+ let delta = origin - position;
725
+ let b = dot(delta, direction);
726
+ let disc = b * b - dot(delta, delta) + radius * radius;
727
+
728
+ if (disc <= 0.0) {
729
+ continue;
730
+ }
731
+
732
+ let start = max(0.0, -b - sqrt(disc));
733
+ let end = min(far, -b + sqrt(disc));
734
+
735
+ if (end <= start) {
736
+ continue;
737
+ }
738
+
739
+ var cuts = array<f32, 4>(start, end, end, end);
740
+ var cutCount = 2u;
741
+ let cone = cos(outer);
742
+
743
+ if (spot) {
744
+ let ar = dot(axis, direction);
745
+ let ac = dot(axis, delta);
746
+ let cos2 = cone * cone;
747
+ let qa = ar * ar - cos2;
748
+ let qb = ar * ac - dot(direction, delta) * cos2;
749
+ let qc = ac * ac - dot(delta, delta) * cos2;
750
+ let discriminant = qb * qb - qa * qc;
751
+ var roots = array<f32, 2>(-1.0, -1.0);
752
+
753
+ if (abs(qa) < 1e-8) {
754
+ if (abs(qb) > 1e-8) {
755
+ roots[0] = -qc / (2.0 * qb);
756
+ }
757
+ } else if (discriminant >= 0.0) {
758
+ roots[0] = (-qb - sqrt(discriminant)) / qa;
759
+ roots[1] = (-qb + sqrt(discriminant)) / qa;
760
+ }
761
+
762
+ for (var r = 0u; r < 2u; r = r + 1u) {
763
+ if (roots[r] > start && roots[r] < end) {
764
+ cuts[cutCount] = roots[r];
765
+ cutCount = cutCount + 1u;
766
+ }
767
+ }
768
+
769
+ // Sort the few cuts in place.
770
+ for (var i = 1u; i < cutCount; i = i + 1u) {
771
+ var j = i;
772
+
773
+ loop {
774
+ if (j == 0u || cuts[j - 1u] <= cuts[j]) {
775
+ break;
776
+ }
777
+
778
+ let swap = cuts[j];
779
+ cuts[j] = cuts[j - 1u];
780
+ cuts[j - 1u] = swap;
781
+ j = j - 1u;
782
+ }
783
+ }
784
+ }
785
+
786
+ for (var segment = 1u; segment < cutCount; segment = segment + 1u) {
787
+ let lo = cuts[segment - 1u];
788
+ let hi = cuts[segment];
789
+
790
+ if (spot && dot(normalize(delta + direction * ((lo + hi) * 0.5)), axis) < cone) {
791
+ continue;
792
+ }
793
+
794
+ tStart = min(tStart, lo);
795
+ tEnd = max(tEnd, hi);
796
+ }
797
+ }
798
+
799
+ if (tEnd <= tStart) {
800
+ return vec3<f32>(0.0);
801
+ }
802
+
803
+ let step = (tEnd - tStart) / f32(MARCH_STEPS);
804
+ var transmittance: array<f32, 8>;
805
+
806
+ for (var k = 0u; k < count; k = k + 1u) {
807
+ transmittance[k] = exp(-extraFloat(frame.volumetricData + k * 20u + 11u) * tStart);
808
+ }
809
+
810
+ var result = vec3<f32>(0.0);
811
+
812
+ for (var i = 0u; i < MARCH_STEPS; i = i + 1u) {
813
+ let t = tStart + (f32(i) + jitter) * step;
814
+ let p = origin + direction * t;
815
+ let fogged = fogVisibility(p);
816
+
817
+ if (fogged <= 0.0001) {
818
+ break;
819
+ }
820
+
821
+ var lit = false;
822
+
823
+ for (var k = 0u; k < count; k = k + 1u) {
824
+ if (transmittance[k] <= 0.003) {
825
+ continue;
826
+ }
827
+
828
+ lit = true;
829
+
830
+ let at = frame.volumetricData + k * 20u;
831
+ let position = extraVec4(at).xyz;
832
+ let spot = extraFloat(at + 3u) > 0.5;
833
+ let axis = extraVec4(at + 4u).xyz;
834
+ let color = extraVec4(at + 8u).rgb;
835
+ let density = extraFloat(at + 11u);
836
+ let angle = extraFloat(at + 12u);
837
+ let penumbra = extraFloat(at + 13u);
838
+ let range = extraFloat(at + 14u);
839
+ let outer = extraFloat(at + 15u);
840
+ let spread = extraFloat(at + 16u);
841
+ let anisotropy = extraFloat(at + 17u);
842
+ let casts = extraFloat(at + 18u) > 0.5;
843
+ let toLight = position - p;
844
+ let dist = length(toLight);
845
+ let toward = toLight / max(dist, 1e-20);
846
+ var attenuation = exp(-dist / (select(10.0, range, range > 0.0) * 0.3));
847
+
848
+ if (range > 0.0) {
849
+ attenuation = attenuation * (1.0 - edge(range * 0.8, range, dist));
850
+ }
851
+
852
+ if (spot) {
853
+ let along = -dot(toward, axis);
854
+ let inner = cos(angle * (1.0 - penumbra));
855
+ let core = edge(cos(angle), inner, along);
856
+ let scattered = spread * pow(edge(cos(outer), inner, along), 2.0);
857
+
858
+ attenuation = attenuation * select(core, max(core, scattered), spread > 0.0);
859
+ }
860
+
861
+ if (attenuation <= 0.0001) {
862
+ continue;
863
+ }
864
+
865
+ // Shadowing follows Three's volumetric lights, which only have shadow maps for spots.
866
+ if (spot && casts && frame.shadowsEnabled != 0u && traceAny(p, toward, dist * (1.0 - 1e-3)) != NO_HIT) {
867
+ continue;
868
+ }
869
+
870
+ let cosine = dot(toward, direction);
871
+ let g = anisotropy;
872
+ let phase = select((1.0 - g * g) / pow(1.0 + g * g - 2.0 * g * cosine, 1.5), 1.0, g == 0.0);
873
+
874
+ // The air between the camera and this point, exactly: a long step evaluated at its start would overstate it.
875
+ result = result + color * (attenuation * density * step * exp(-density * t) * fogged * phase * 1.5);
876
+ }
877
+
878
+ if (!lit) {
879
+ break;
880
+ }
881
+
882
+ for (var k = 0u; k < count; k = k + 1u) {
883
+ transmittance[k] = transmittance[k] * exp(-extraFloat(frame.volumetricData + k * 20u + 11u) * step);
884
+ }
885
+ }
886
+
887
+ return result / (vec3<f32>(1.0) + result);
888
+ }
889
+
890
+ /**
891
+ * How far a pixel's estimate may still be from its mean: the standard error of its luminance, over the square root of
892
+ * the luminance, which is roughly how visible that error is once the image is encoded for display.
893
+ */
894
+ fn pixelError(pixel: vec2<u32>) -> f32 {
895
+ let index = pixel.y * frame.resolution.x + pixel.x;
896
+ let sum = accumulation[index];
897
+
898
+ if (sum.w < 2.0) {
899
+ return 1e30;
900
+ }
901
+
902
+ let squares = accumulation[frame.pixelCount + index];
903
+ let diffuse = accumulation[4u * frame.pixelCount + index].rgb;
904
+
905
+ // Each of the denoiser's signals has its own variance; the pixel has converged when both have.
906
+ return max(
907
+ channelError(luminanceOf(diffuse) / sum.w, squares.z / sum.w, sum.w),
908
+ channelError(luminanceOf(sum.rgb - diffuse) / sum.w, squares.w / sum.w, sum.w));
909
+ }
910
+
911
+ fn channelError(mean: f32, square: f32, samples: f32) -> f32 {
912
+ let variance = max(square - mean * mean, 0.0);
913
+
914
+ return sqrt(variance / samples) / sqrt(max(mean, 0.0) + 1e-3);
915
+ }
916
+
917
+ /**
918
+ * Whether a pixel has converged, judged over its neighbourhood: a rare bright path lands in one pixel, and a pixel
919
+ * judged alone would stop where its neighbours had not yet seen one and leave that area too dark.
920
+ */
921
+ fn converged(pixel: vec2<u32>) -> bool {
922
+ for (var dy = -1; dy <= 1; dy = dy + 1) {
923
+ for (var dx = -1; dx <= 1; dx = dx + 1) {
924
+ let x = clamp(i32(pixel.x) + dx, 0, i32(frame.resolution.x) - 1);
925
+ let y = clamp(i32(pixel.y) + dy, 0, i32(frame.resolution.y) - 1);
926
+
927
+ if (pixelError(vec2<u32>(u32(x), u32(y))) > frame.adaptiveThreshold) {
928
+ return false;
929
+ }
930
+ }
931
+ }
932
+
933
+ return true;
934
+ }
935
+
936
+ @compute @workgroup_size(8, 8, 1)
937
+ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
938
+ if (id.x >= frame.bandSize.x || id.y >= frame.bandSize.y) {
939
+ return;
940
+ }
941
+
942
+ let pixel = frame.bandOrigin + id.xy;
943
+
944
+ if (pixel.x >= frame.resolution.x || pixel.y >= frame.resolution.y) {
945
+ return;
946
+ }
947
+
948
+ let index = pixel.y * frame.resolution.x + pixel.x;
949
+
950
+ // Adaptive sampling: a pixel whose neighbourhood has converged takes no more samples, which leaves the pass to
951
+ // the pixels that still need them.
952
+ if (frame.adaptiveMin > 0u && accumulation[index].w >= f32(frame.adaptiveMin) && converged(pixel)) {
953
+ return;
954
+ }
955
+
956
+ var radiance = vec3<f32>(0.0);
957
+ var alpha = 0.0;
958
+ var depthSum = 0.0;
959
+ var diffuseSquares = 0.0;
960
+ var specularSquares = 0.0;
961
+ var diffuseSum = vec3<f32>(0.0);
962
+ var diffuseAlbedoSum = vec3<f32>(0.0);
963
+ var specularAlbedoSum = vec3<f32>(0.0);
964
+ var normalSum = vec3<f32>(0.0);
965
+ var distanceSum = 0.0;
966
+ var roughnessSum = 0.0;
967
+ var specularDistanceSum = 0.0;
968
+
969
+ for (var s = 0u; s < frame.sampleCount; s = s + 1u) {
970
+ var rng = makeSampler(pixel, frame.sampleIndex + s);
971
+ let jitter = sample2D(&rng);
972
+ let lens = sample2D(&rng);
973
+ let ray = cameraRay(pixel, jitter, lens);
974
+ let result = traceRadiance(ray[0].xyz, ray[1].xyz, ray[1].w, &rng);
975
+ var sampleAlpha = result.alpha;
976
+
977
+ var sampled = result.radiance;
978
+ var sampledDiffuse = result.diffuse;
979
+
980
+ if (frame.volumetricCount > 0u) {
981
+ let glow = volumetricGlow(ray[0].xyz, ray[1].xyz, result.depth, sample2D(&rng).x);
982
+
983
+ // As the CPU renderer composites it: added over the image, and raising the coverage just enough to hold it.
984
+ sampled = sampled + glow;
985
+ sampledDiffuse = sampledDiffuse + glow;
986
+ sampleAlpha = max(sampleAlpha, max3(glow));
987
+ }
988
+
989
+ let sampledSpecular = sampled - sampledDiffuse;
990
+
991
+ radiance = radiance + sampled;
992
+ diffuseSum = diffuseSum + sampledDiffuse;
993
+ diffuseSquares = diffuseSquares + luminanceOf(sampledDiffuse) * luminanceOf(sampledDiffuse);
994
+ specularSquares = specularSquares + luminanceOf(sampledSpecular) * luminanceOf(sampledSpecular);
995
+ diffuseAlbedoSum = diffuseAlbedoSum + result.diffuseAlbedo;
996
+ specularAlbedoSum = specularAlbedoSum + result.specularAlbedo;
997
+ normalSum = normalSum + result.normal;
998
+ distanceSum = distanceSum + result.guideDistance;
999
+ roughnessSum = roughnessSum + result.roughness;
1000
+ specularDistanceSum = specularDistanceSum + result.specularDistance;
1001
+
1002
+ alpha = alpha + sampleAlpha;
1003
+ depthSum = depthSum + windowDepth(ray[0].xyz + ray[1].xyz * min(result.depth, 1e20), result.depth);
1004
+ }
1005
+
1006
+ let coverage = accumulation[frame.pixelCount + index];
1007
+ accumulation[index] = accumulation[index] + vec4<f32>(radiance, f32(frame.sampleCount));
1008
+ accumulation[frame.pixelCount + index] = vec4<f32>(
1009
+ coverage.x + alpha, coverage.y + depthSum, coverage.z + diffuseSquares, coverage.w + specularSquares);
1010
+ // The denoiser's signals and guides, summed like the radiance so they average over the pixel's samples.
1011
+ let pc = frame.pixelCount;
1012
+ accumulation[2u * pc + index] = accumulation[2u * pc + index] + vec4<f32>(diffuseAlbedoSum, roughnessSum);
1013
+ accumulation[3u * pc + index] = accumulation[3u * pc + index] + vec4<f32>(normalSum, distanceSum);
1014
+ accumulation[4u * pc + index] = accumulation[4u * pc + index] + vec4<f32>(diffuseSum, specularDistanceSum);
1015
+ accumulation[5u * pc + index] = accumulation[5u * pc + index] + vec4<f32>(specularAlbedoSum, 0.0);
1016
+ // This row still has work: every pixel that traced writes the same value, so the race is harmless.
1017
+ accumulation[6u * pc + pixel.y] = vec4<f32>(1.0);
1018
+ }
1019
+ `;
1020
+ //# sourceMappingURL=pathTrace.wgsl.js.map