@tinypoly/gpu-renderer 0.0.4 → 0.1.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 (140) hide show
  1. package/ARCHITECTURE.md +81 -10
  2. package/README.md +53 -5
  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/bvh.d.ts +11 -6
  8. package/dist/bvh.d.ts.map +1 -1
  9. package/dist/bvh.js +64 -11
  10. package/dist/bvh.js.map +1 -1
  11. package/dist/denoise.d.ts +17 -2
  12. package/dist/denoise.d.ts.map +1 -1
  13. package/dist/denoise.js +95 -27
  14. package/dist/denoise.js.map +1 -1
  15. package/dist/dispatchScheduler.d.ts +11 -4
  16. package/dist/dispatchScheduler.d.ts.map +1 -1
  17. package/dist/dispatchScheduler.js +14 -7
  18. package/dist/dispatchScheduler.js.map +1 -1
  19. package/dist/frameLayout.d.ts.map +1 -1
  20. package/dist/frameLayout.js +18 -0
  21. package/dist/frameLayout.js.map +1 -1
  22. package/dist/geometry.d.ts.map +1 -1
  23. package/dist/geometry.js +6 -3
  24. package/dist/geometry.js.map +1 -1
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/lensPass.d.ts +60 -0
  30. package/dist/lensPass.d.ts.map +1 -0
  31. package/dist/lensPass.js +235 -0
  32. package/dist/lensPass.js.map +1 -0
  33. package/dist/lightTree.d.ts +43 -0
  34. package/dist/lightTree.d.ts.map +1 -0
  35. package/dist/lightTree.js +107 -0
  36. package/dist/lightTree.js.map +1 -0
  37. package/dist/lights.d.ts +19 -1
  38. package/dist/lights.d.ts.map +1 -1
  39. package/dist/lights.js +63 -1
  40. package/dist/lights.js.map +1 -1
  41. package/dist/materialLayout.d.ts +24 -0
  42. package/dist/materialLayout.d.ts.map +1 -1
  43. package/dist/materialLayout.js +36 -1
  44. package/dist/materialLayout.js.map +1 -1
  45. package/dist/pathTracePass.d.ts +6 -3
  46. package/dist/pathTracePass.d.ts.map +1 -1
  47. package/dist/pathTracePass.js +16 -10
  48. package/dist/pathTracePass.js.map +1 -1
  49. package/dist/renderer.d.ts +19 -7
  50. package/dist/renderer.d.ts.map +1 -1
  51. package/dist/renderer.js +198 -36
  52. package/dist/renderer.js.map +1 -1
  53. package/dist/sceneUpload.d.ts +19 -1
  54. package/dist/sceneUpload.d.ts.map +1 -1
  55. package/dist/sceneUpload.js +25 -3
  56. package/dist/sceneUpload.js.map +1 -1
  57. package/dist/settings.d.ts +136 -0
  58. package/dist/settings.d.ts.map +1 -0
  59. package/dist/settings.js +100 -0
  60. package/dist/settings.js.map +1 -0
  61. package/dist/subsurface.d.ts +76 -0
  62. package/dist/subsurface.d.ts.map +1 -0
  63. package/dist/subsurface.js +70 -0
  64. package/dist/subsurface.js.map +1 -0
  65. package/dist/texturePack.d.ts +16 -4
  66. package/dist/texturePack.d.ts.map +1 -1
  67. package/dist/texturePack.js +61 -8
  68. package/dist/texturePack.js.map +1 -1
  69. package/dist/volumeContainment.d.ts +17 -0
  70. package/dist/volumeContainment.d.ts.map +1 -0
  71. package/dist/volumeContainment.js +61 -0
  72. package/dist/volumeContainment.js.map +1 -0
  73. package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
  74. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
  75. package/dist/wgsl/bsdf.wgsl.js +35 -3
  76. package/dist/wgsl/bsdf.wgsl.js.map +1 -1
  77. package/dist/wgsl/bvh.wgsl.d.ts +8 -2
  78. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  79. package/dist/wgsl/bvh.wgsl.js +167 -96
  80. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  81. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  82. package/dist/wgsl/common.wgsl.js +24 -7
  83. package/dist/wgsl/common.wgsl.js.map +1 -1
  84. package/dist/wgsl/lens.wgsl.d.ts +17 -0
  85. package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
  86. package/dist/wgsl/lens.wgsl.js +315 -0
  87. package/dist/wgsl/lens.wgsl.js.map +1 -0
  88. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  89. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  90. package/dist/wgsl/lights.wgsl.js +206 -33
  91. package/dist/wgsl/lights.wgsl.js.map +1 -1
  92. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  93. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  94. package/dist/wgsl/pathTrace.wgsl.js +410 -25
  95. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  96. package/dist/wgsl/random.wgsl.d.ts +1 -1
  97. package/dist/wgsl/random.wgsl.d.ts.map +1 -1
  98. package/dist/wgsl/random.wgsl.js +9 -17
  99. package/dist/wgsl/random.wgsl.js.map +1 -1
  100. package/dist/wgsl/sampling.wgsl.d.ts +1 -1
  101. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
  102. package/dist/wgsl/sampling.wgsl.js +25 -0
  103. package/dist/wgsl/sampling.wgsl.js.map +1 -1
  104. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  105. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  106. package/dist/wgsl/surface.wgsl.js +47 -4
  107. package/dist/wgsl/surface.wgsl.js.map +1 -1
  108. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  109. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  110. package/dist/wgsl/texture.wgsl.js +10 -11
  111. package/dist/wgsl/texture.wgsl.js.map +1 -1
  112. package/package.json +1 -1
  113. package/src/atmosphere.ts +95 -0
  114. package/src/bvh.ts +81 -12
  115. package/src/denoise.ts +110 -27
  116. package/src/dispatchScheduler.ts +16 -7
  117. package/src/frameLayout.ts +18 -0
  118. package/src/geometry.ts +6 -2
  119. package/src/index.ts +13 -0
  120. package/src/lensPass.ts +299 -0
  121. package/src/lightTree.ts +172 -0
  122. package/src/lights.ts +90 -2
  123. package/src/materialLayout.ts +52 -1
  124. package/src/pathTracePass.ts +19 -10
  125. package/src/renderer.ts +258 -43
  126. package/src/sceneUpload.ts +41 -4
  127. package/src/settings.ts +234 -0
  128. package/src/subsurface.ts +153 -0
  129. package/src/texturePack.ts +80 -13
  130. package/src/volumeContainment.ts +81 -0
  131. package/src/wgsl/bsdf.wgsl.ts +35 -3
  132. package/src/wgsl/bvh.wgsl.ts +167 -96
  133. package/src/wgsl/common.wgsl.ts +24 -7
  134. package/src/wgsl/lens.wgsl.ts +314 -0
  135. package/src/wgsl/lights.wgsl.ts +206 -33
  136. package/src/wgsl/pathTrace.wgsl.ts +410 -25
  137. package/src/wgsl/random.wgsl.ts +9 -17
  138. package/src/wgsl/sampling.wgsl.ts +25 -0
  139. package/src/wgsl/surface.wgsl.ts +47 -4
  140. package/src/wgsl/texture.wgsl.ts +10 -11
@@ -98,12 +98,14 @@ fn clampIndirect(contribution: vec3<f32>) -> vec3<f32> {
98
98
  * How much of the emission a bounce ray found the renderer keeps. Direct sampling had its own chance at this triangle,
99
99
  * and the heuristic splits the two so neither counts it twice.
100
100
  */
101
- fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool) -> f32 {
101
+ fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>,
102
+ normal: vec3<f32>) -> f32 {
102
103
  if (frame.areaLightCount == 0u) {
103
104
  return 1.0;
104
105
  }
105
106
 
106
- let areaPdf = lightPdfOfTriangle(hit.triangle);
107
+ // From the vertex the bounce left, which is where direct sampling would have picked it from.
108
+ let areaPdf = lightPdfOfTriangle(hit.triangle, scatteredAt, normal);
107
109
 
108
110
  if (areaPdf <= 0.0) {
109
111
  return 1.0;
@@ -122,6 +124,18 @@ fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect:
122
124
  return misWeight(bsdfPdf, areaPdf * hit.distance * hit.distance / cosLight);
123
125
  }
124
126
 
127
+ /**
128
+ * The normal the light tree weighs lights by, or none where light from behind the surface counts too: a point in a
129
+ * volume, a toon ramp read all round, a clearcoat that may face what its base turns from, and transmission.
130
+ */
131
+ fn treeNormal(surface: Surface) -> vec3<f32> {
132
+ if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0) {
133
+ return vec3<f32>(0.0);
134
+ }
135
+
136
+ return surface.normal;
137
+ }
138
+
125
139
  /** One shadow ray from the surface towards a light, weighted against the chance the bounce would have found it too. */
126
140
  fn directLight(
127
141
  surface: Surface,
@@ -130,10 +144,11 @@ fn directLight(
130
144
  pick: f32,
131
145
  u: vec2<f32>,
132
146
  rng: ptr<function, PathSampler>,
133
- diffusePart: ptr<function, vec3<f32>>) -> vec3<f32> {
147
+ diffusePart: ptr<function, vec3<f32>>,
148
+ medium: f32) -> vec3<f32> {
134
149
  *diffusePart = vec3<f32>(0.0);
135
150
 
136
- let chosen = sampleLight(position, pick, u);
151
+ let chosen = sampleLight(position, treeNormal(surface), pick, u);
137
152
 
138
153
  if (!chosen.valid) {
139
154
  return vec3<f32>(0.0);
@@ -153,7 +168,7 @@ fn directLight(
153
168
  let toon = surface.kind == KIND_TOON;
154
169
  // Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.
155
170
  let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);
156
- let through = lightTransmittance(position, chosen, rng, skip);
171
+ let through = lightTransmittance(position, chosen, rng, skip, medium);
157
172
 
158
173
  if (all(through <= vec3<f32>(0.0))) {
159
174
  return vec3<f32>(0.0);
@@ -191,6 +206,142 @@ fn diffuseGuide(surface: Surface) -> vec3<f32> {
191
206
  return surface.albedo * (1.0 - surface.metalness) * (1.0 - clamp(surface.transmission, 0.0, 1.0));
192
207
  }
193
208
 
209
+ /**
210
+ * How often a vertex on a material that scatters inside takes the walk into it instead of its surface lobes: by the
211
+ * share of the light each accounts for, as lobeWeights chooses between lobes. Never all of the time, so the specular
212
+ * layer on top stays reachable.
213
+ */
214
+ fn subsurfaceChance(surface: Surface, view: vec3<f32>) -> f32 {
215
+ if (surface.subsurface <= 0.0 || (surface.flags & MATERIAL_PBR) == 0u) {
216
+ return 0.0;
217
+ }
218
+
219
+ let diffuse = luminanceOf(surface.albedo * (1.0 - surface.metalness)) * coatTransmittance(surface, view);
220
+ let inside = diffuse * surface.subsurface;
221
+ let outside = diffuse * (1.0 - surface.subsurface) + luminanceOf(specularAlbedo(surface, view)) + 0.02;
222
+
223
+ return clamp(inside / max(inside + outside, 1e-6), 0.0, 0.95);
224
+ }
225
+
226
+ /**
227
+ * The single scattering albedo that gives this multiple scattering albedo in a semi-infinite medium: van de Hulst's
228
+ * relation, inverted as Cycles inverts it for its random walk. What the material's colour says it reflects in all
229
+ * is what light keeps at each scattering inside it.
230
+ */
231
+ fn subsurfaceAlbedo(albedo: vec3<f32>) -> vec3<f32> {
232
+ let a = clamp(albedo, vec3<f32>(0.0), vec3<f32>(0.999));
233
+ let s = 4.09712 + 4.20863 * a - sqrt(9.59217 + 41.6808 * a + 17.7126 * a * a);
234
+
235
+ return vec3<f32>(1.0) - s * s;
236
+ }
237
+
238
+ /**
239
+ * How far the walk goes before it next scatters, drawn for one colour picked in proportion to what the path still
240
+ * carries of it. choice keeps those odds, which the next step weighs its distance by over all three colours, so each
241
+ * colour keeps its own mean free path and the estimate stays exact (spectral MIS, as Cycles' random walk does it).
242
+ */
243
+ fn walkDistance(throughput: vec3<f32>, sigma: vec3<f32>, u: vec2<f32>, choice: ptr<function, vec3<f32>>) -> f32 {
244
+ let weights = max(throughput, vec3<f32>(0.0));
245
+ let total = weights.x + weights.y + weights.z;
246
+ let odds = select(vec3<f32>(1.0 / 3.0), weights / total, total > 0.0);
247
+ let chosen = select(select(sigma.z, sigma.y, u.x < odds.x + odds.y), sigma.x, u.x < odds.x);
248
+
249
+ *choice = odds;
250
+
251
+ return -log(max(1.0 - u.y, 1e-12)) / chosen;
252
+ }
253
+
254
+ /**
255
+ * Where a walk leaves the material: a white Lambert surface facing out, as Cycles leaves its random walk. The colour
256
+ * was taken inside, scattering by scattering, so nothing tints the light again on the way out.
257
+ */
258
+ fn subsurfaceExit(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {
259
+ var surface: Surface;
260
+ // Turned to face against the reversed ray: outwards, the side the walk is leaving through.
261
+ let out = interpolatedNormal(m, triangle, bary, -direction);
262
+
263
+ surface.normal = out;
264
+ surface.facing = out;
265
+ surface.albedo = vec3<f32>(1.0);
266
+ surface.flags = MATERIAL_LIT;
267
+ surface.kind = KIND_LAMBERT;
268
+ surface.opacity = 1.0;
269
+ surface.alpha = 1.0;
270
+ surface.blending = BLEND_NORMAL;
271
+ surface.material = m;
272
+ surface.roughness = 1.0;
273
+ surface.ior = 1.5;
274
+ surface.specularF90 = 1.0;
275
+ surface.envMap = NO_TEXTURE;
276
+ surface.envMapIntensity = 1.0;
277
+ surface.gradientMap = NO_TEXTURE;
278
+
279
+ return surface;
280
+ }
281
+
282
+ /** A point inside a volume where the light scatters: the medium's phase function in place of a surface's lobes. */
283
+ fn volumePoint(direction: vec3<f32>, albedo: vec3<f32>, anisotropy: f32) -> Surface {
284
+ var surface: Surface;
285
+
286
+ surface.normal = -direction;
287
+ surface.facing = -direction;
288
+ surface.albedo = albedo;
289
+ surface.flags = MATERIAL_LIT;
290
+ surface.kind = KIND_VOLUME;
291
+ surface.anisotropy = anisotropy;
292
+ surface.opacity = 1.0;
293
+ surface.alpha = 1.0;
294
+ surface.blending = BLEND_NORMAL;
295
+ surface.material = NO_MATERIAL;
296
+ surface.roughness = 1.0;
297
+ surface.ior = 1.0;
298
+ surface.specularF90 = 1.0;
299
+ surface.envMap = NO_TEXTURE;
300
+ surface.envMapIntensity = 1.0;
301
+ surface.gradientMap = NO_TEXTURE;
302
+
303
+ return surface;
304
+ }
305
+
306
+ /**
307
+ * The shading normal with the edges near it rounded, as Cycles' bevel node rounds them, by one probe a sample. A
308
+ * point is drawn on a disk of the bevel radius around the shaded point, across an axis picked along the normal half
309
+ * the time and along each tangent a quarter; a short ray along that axis through it finds the surface of the same
310
+ * material there, and that surface's normal turns this one by as much as it differs from the normal here. Over the
311
+ * samples a pixel takes, the normal blends from one face to the next within the radius of every edge, so a box reads
312
+ * as a machined one. A probe that finds nothing, or a normal turned away from the view, leaves the normal as it was.
313
+ */
314
+ fn bevelNormal(m: u32, point: vec3<f32>, surface: Surface, view: vec3<f32>, radius: f32, u: vec2<f32>,
315
+ v: vec2<f32>) -> vec3<f32> {
316
+ let basis = orthonormalBasis(surface.facing);
317
+ var axis = basis[2];
318
+ var first = basis[0];
319
+ var second = basis[1];
320
+
321
+ if (v.x >= 0.5) {
322
+ axis = select(basis[1], basis[0], v.x < 0.75);
323
+ first = select(basis[2], basis[1], v.x < 0.75);
324
+ second = select(basis[0], basis[2], v.x < 0.75);
325
+ }
326
+
327
+ let reach = radius * sqrt(u.x);
328
+ let angle = 2.0 * PI * u.y;
329
+ let across = point + first * (reach * cos(angle)) + second * (reach * sin(angle));
330
+ // Along the axis one way or the other at random, from the far edge of the radius across it.
331
+ let way = select(-1.0, 1.0, v.y < 0.5);
332
+ let hit = traceScene(across - axis * (radius * way), axis * way, 2.0 * radius);
333
+
334
+ if (hit.triangle == NO_HIT || materialIndexOf(hit.triangle) != m) {
335
+ return surface.normal;
336
+ }
337
+
338
+ // The authored normal there, turned the way this side's own is: the facing normal is the authored one, or its reverse.
339
+ let there = authoredNormal(hit.triangle, vec2<f32>(hit.u, hit.v)) * select(-1.0, 1.0, surface.front);
340
+ let turned = normalize(surface.normal + there - surface.facing);
341
+
342
+ return select(surface.normal, turned, dot(turned, view) > 0.0 && all(turned == turned));
343
+ }
344
+
194
345
  struct PathResult {
195
346
  radiance: vec3<f32>,
196
347
  alpha: f32,
@@ -206,11 +357,16 @@ struct PathResult {
206
357
  guideDistance: f32,
207
358
  roughness: f32,
208
359
  specularDistance: f32,
360
+ // How much of the first surface scatters light inside, which the denoiser relaxes its tests over.
361
+ subsurface: f32,
209
362
  };
210
363
 
211
364
  /** How many surfaces a path may pass through without scattering (cutouts, culled faces, glass) before it gives up. */
212
365
  const MAX_STEPS: u32 = 64u;
213
366
 
367
+ /** Scatterings one walk inside a material may take; the roulette ends most walks well before. */
368
+ const MAX_WALK: u32 = 128u;
369
+
214
370
  fn traceRadiance(
215
371
  rayOrigin: vec3<f32>, rayDirection: vec3<f32>, rayLength: f32, rng: ptr<function, PathSampler>) -> PathResult {
216
372
  var radiance = vec3<f32>(0.0);
@@ -230,6 +386,9 @@ fn traceRadiance(
230
386
  var bounce = 0u;
231
387
  // The vertex the path last scattered at took all of its direct light by sampling lights: a toon surface.
232
388
  var lightsOwnDirect = false;
389
+ // Where the path last scattered and the normal the light tree weighed lights by there, for the heuristic's other half.
390
+ var lastPosition = rayOrigin;
391
+ var lastNormal = vec3<f32>(0.0);
233
392
  // That vertex's own environment map, which a ray escaping from it sees instead of the scene's.
234
393
  var escapeMap = NO_TEXTURE;
235
394
  var escapeIntensity = 1.0;
@@ -240,6 +399,12 @@ fn traceRadiance(
240
399
  var fogAmount = 0.0;
241
400
  var fogStart = vec3<f32>(0.0);
242
401
  var fogThroughput = vec3<f32>(0.0);
402
+ // The air between the camera and the first surface it sees: aerial perspective.
403
+ var hazed = false;
404
+ var hazeDistance = 0.0;
405
+ var hazeStart = vec3<f32>(0.0);
406
+ var hazeThroughput = vec3<f32>(0.0);
407
+ var hazeDirection = vec3<f32>(0.0);
243
408
  // A built-in material's hooks after <opaque_fragment> edit the colour the camera sees of it, which is everything the
244
409
  // path gathers from that surface on.
245
410
  var afterMaterial = NO_MATERIAL;
@@ -257,6 +422,7 @@ fn traceRadiance(
257
422
  var guideNormal = vec3<f32>(0.0);
258
423
  var guideDistance = 1e6;
259
424
  var guideRoughness = 1.0;
425
+ var guideSubsurface = 0.0;
260
426
  // Where the path first scattered, which decides which of the two signals the rest of it feeds.
261
427
  var split = false;
262
428
  var splitBase = vec3<f32>(0.0);
@@ -265,17 +431,109 @@ fn traceRadiance(
265
431
  var splitDiffuse = true;
266
432
  var waitingForSpecular = false;
267
433
  var specularDistance = 1e6;
434
+ // Inside a material that scatters light, between two scatterings: its extinction and single scattering albedo per
435
+ // colour, its anisotropy, the odds each colour had when the current step's distance was drawn, and the steps so far.
436
+ var inMedium = false;
437
+ var sigma = vec3<f32>(0.0);
438
+ var walkAlbedo = vec3<f32>(0.0);
439
+ var walkG = 0.0;
440
+ var walkOdds = vec3<f32>(0.0);
441
+ var walkSteps = 0u;
442
+ // Inside a volume: its density per unit of distance, what it keeps at each scattering and its anisotropy. A camera
443
+ // inside one starts there.
444
+ var inVolume = false;
445
+ var volumeDensity = 0.0;
446
+ var volumeAlbedo = vec3<f32>(0.0);
447
+ var volumeG = 0.0;
448
+
449
+ if (VOLUMES && frame.cameraVolume != NO_MATERIAL && frame.volumesEnabled != 0u) {
450
+ inVolume = true;
451
+ volumeDensity = materials[frame.cameraVolume].volume.w;
452
+ volumeAlbedo = materials[frame.cameraVolume].volume.rgb;
453
+ volumeG = materials[frame.cameraVolume].volumeAnisotropy;
454
+ }
268
455
 
269
- for (var step = 0u; step < MAX_STEPS; step = step + 1u) {
456
+ for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {
270
457
  let hit = traceScene(origin, direction, limit);
271
458
 
459
+ // Every vertex draws the same dimensions in the same order, whatever the path then does with them.
460
+ let coverage = sample2D(rng);
461
+ let pick = sample2D(rng);
462
+ let lightSample = sample2D(rng);
463
+ let lobe = sample2D(rng);
464
+ let bounceSample = sample2D(rng);
465
+ var mediumSample = vec2<f32>(0.0);
466
+ var bevelSample = vec4<f32>(0.0);
467
+
468
+ if (VOLUMES) {
469
+ mediumSample = sample2D(rng);
470
+ }
471
+
472
+ if (BEVEL) {
473
+ bevelSample = vec4<f32>(sample2D(rng), sample2D(rng));
474
+ }
475
+
476
+ // The walk left the material here, and the surface below is its way out rather than what the material shows.
477
+ var exiting = false;
478
+
479
+ if (SUBSURFACE_WALK && inMedium) {
480
+ if (hit.triangle == NO_HIT) {
481
+ // Nothing within the drawn distance: the light scatters there, keeping each colour's share of the medium.
482
+ let travelled = exp(-sigma * limit);
483
+ let density = dot(walkOdds, sigma * travelled);
484
+
485
+ throughput = throughput * sigma * walkAlbedo * travelled / max(density, 1e-20);
486
+ walkSteps = walkSteps + 1u;
487
+
488
+ let survival = max3(throughput);
489
+
490
+ if (walkSteps >= MAX_WALK || survival <= 0.0) {
491
+ break;
492
+ }
493
+
494
+ if (survival < 0.2) {
495
+ if (coverage.x > survival) {
496
+ break;
497
+ }
498
+
499
+ throughput = throughput / survival;
500
+ }
501
+
502
+ origin = origin + direction * limit;
503
+ direction = sampleHenyeyGreenstein(direction, walkG, lobe);
504
+ limit = walkDistance(throughput, sigma, pick, &walkOdds);
505
+ continue;
506
+ }
507
+
508
+ // The walk reached the surface before scattering again: the chance of travelling that far, per colour.
509
+ let travelled = exp(-sigma * hit.distance);
510
+
511
+ throughput = throughput * travelled / max(dot(walkOdds, travelled), 1e-20);
512
+ inMedium = false;
513
+ exiting = true;
514
+ }
515
+
516
+ // Inside a volume, the medium may stop the ray before whatever it hit. The distance is drawn now that the segment's
517
+ // length is known, which a homogeneous medium allows; one grey density makes the weight of either outcome one.
518
+ var scatterAt = -1.0;
519
+
520
+ if (VOLUMES && inVolume && !exiting) {
521
+ let free = -log(max(1.0 - mediumSample.x, 1e-12)) / max(volumeDensity, 1e-12);
522
+
523
+ if (free < select(hit.distance, limit, hit.triangle == NO_HIT)) {
524
+ scatterAt = free;
525
+ }
526
+ }
527
+
528
+ let scattering = scatterAt >= 0.0;
529
+
272
530
  // The specular continuation's first hit: how far the reflection the denoiser filters actually is.
273
531
  if (waitingForSpecular) {
274
532
  specularDistance = select(min(hit.distance, 1e6), 1e6, hit.triangle == NO_HIT);
275
533
  waitingForSpecular = false;
276
534
  }
277
535
 
278
- if (hit.triangle == NO_HIT) {
536
+ if (hit.triangle == NO_HIT && !scattering) {
279
537
  if (primary) {
280
538
  // A transparent background leaves the pixel empty, which its alpha says.
281
539
  if (frame.background.w != 2.0) {
@@ -299,18 +557,45 @@ fn traceRadiance(
299
557
  break;
300
558
  }
301
559
 
302
- throughput = throughput * exp(-absorption * hit.distance);
560
+ let travel = select(hit.distance, scatterAt, scattering);
561
+
562
+ throughput = throughput * exp(-absorption * travel);
303
563
 
304
564
  let bary = vec2<f32>(hit.u, hit.v);
305
- let point = origin + direction * hit.distance;
306
- let m = materialIndexOf(hit.triangle);
565
+ let point = origin + direction * travel;
566
+ var m = 0u;
307
567
 
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);
568
+ if (!scattering) {
569
+ m = materialIndexOf(hit.triangle);
570
+ }
571
+
572
+ var surface: Surface;
573
+
574
+ if (SUBSURFACE_WALK && exiting) {
575
+ surface = subsurfaceExit(m, hit.triangle, bary, direction);
576
+ // Seen from outside, along its normal: the side the light leaves through.
577
+ direction = -surface.normal;
578
+ } else if (VOLUMES && scattering) {
579
+ surface = volumePoint(direction, volumeAlbedo, volumeG);
580
+ } else {
581
+
582
+ // A volume's boundary is not a surface: the ray goes in or out, and the medium is what it meets.
583
+ if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {
584
+ if (dot(authoredNormal(hit.triangle, bary), direction) < 0.0) {
585
+ if (frame.volumesEnabled != 0u) {
586
+ inVolume = true;
587
+ volumeDensity = materials[m].volume.w;
588
+ volumeAlbedo = materials[m].volume.rgb;
589
+ volumeG = materials[m].volumeAnisotropy;
590
+ }
591
+ } else {
592
+ inVolume = false;
593
+ }
594
+
595
+ origin = point + direction * RAY_EPSILON;
596
+ limit = select(1e30, limit - hit.distance, straight);
597
+ continue;
598
+ }
314
599
 
315
600
  // Three culls the side a material does not draw. Only the camera's rays are culled: a mesh still casts shadows and
316
601
  // holds light in from both sides.
@@ -330,7 +615,26 @@ fn traceRadiance(
330
615
  continue;
331
616
  }
332
617
 
333
- let surface = resolveSurface(hit.triangle, bary, direction);
618
+ let base = sampleMap(materials[m].map, hit.triangle, bary);
619
+
620
+ // A cutout lets the ray through before anything else about the surface is read: the normal map, the other maps and
621
+ // the lobes would only be thrown away. The draw and the comparison are the ones made below, so nothing changes.
622
+ if (coverageFirst(m)) {
623
+ let opacity = coverageOf(m, materialAlphaFrom(m, hit.triangle, bary, base));
624
+
625
+ if (opacity < 1.0 && coverage.x >= opacity) {
626
+ origin = point + direction * RAY_EPSILON;
627
+ limit = select(1e30, limit - hit.distance, straight);
628
+ continue;
629
+ }
630
+ }
631
+
632
+ surface = resolveSurfaceFrom(hit.triangle, bary, direction, base);
633
+
634
+ if (BEVEL && frame.bevelEnabled != 0u && materials[m].bevel > 0.0) {
635
+ surface.normal = bevelNormal(m, point, surface, -direction, materials[m].bevel, bevelSample.xy, bevelSample.zw);
636
+ }
637
+ }
334
638
 
335
639
  // Blending other than normal: the surface adds its light to, or filters, whatever is behind it, and the path goes on.
336
640
  if (surface.blending == BLEND_ADDITIVE || surface.blending == BLEND_MULTIPLY || surface.blending == BLEND_SUBTRACTIVE) {
@@ -344,7 +648,7 @@ fn traceRadiance(
344
648
  if ((surface.flags & MATERIAL_LIT) != 0u) {
345
649
  var unused: vec3<f32>;
346
650
  added = added + directLight(surface, point + surface.normal * RAY_EPSILON, -direction, pick.x, lightSample, rng,
347
- &unused);
651
+ &unused, select(0.0, volumeDensity, inVolume));
348
652
  }
349
653
 
350
654
  radiance = radiance + throughput * added * strength;
@@ -396,6 +700,7 @@ fn traceRadiance(
396
700
  guideNormal = surface.normal;
397
701
  guideDistance = length(point - rayOrigin);
398
702
  guideRoughness = surface.roughness;
703
+ guideSubsurface = select(0.0, surface.subsurface, SUBSURFACE_WALK);
399
704
 
400
705
  if (surface.kind == KIND_PHONG) {
401
706
  // Phong's exponent as the roughness of the GGX lobe that is about as wide.
@@ -406,7 +711,7 @@ fn traceRadiance(
406
711
  if (primary) {
407
712
  alpha = 1.0;
408
713
 
409
- if (!fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {
714
+ if (!scattering && !fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {
410
715
  afterMaterial = m;
411
716
  afterTriangle = hit.triangle;
412
717
  afterBary = bary;
@@ -430,11 +735,20 @@ fn traceRadiance(
430
735
  }
431
736
  }
432
737
 
738
+ if (!hazed && frame.atmosphereColor.w > 0.0) {
739
+ hazed = true;
740
+ // From the eye, where the camera ray only starts on the near plane; an orthographic view has no eye.
741
+ hazeDistance = length(point - select(frame.cameraToWorld[3].xyz, rayOrigin, frame.cameraKind == 1u));
742
+ hazeStart = radiance;
743
+ hazeThroughput = throughput;
744
+ hazeDirection = direction;
745
+ }
746
+
433
747
  radiance = radiance + throughput * surface.lightMap;
434
748
  }
435
749
 
436
750
  if (any(surface.emissive > vec3<f32>(0.0))) {
437
- let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect);
751
+ let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect, lastPosition, lastNormal);
438
752
 
439
753
  // Seen by the camera or one bounce away this is direct light and stays exact; deeper it may be clamped.
440
754
  radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);
@@ -521,15 +835,56 @@ fn traceRadiance(
521
835
  bounce = bounce + 1u;
522
836
  }
523
837
  } else {
838
+ // Light that enters the material: a random walk inside it from here, in place of the share of the diffuse
839
+ // layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.
840
+ var chance = 0.0;
841
+
842
+ if (SUBSURFACE_WALK) {
843
+ chance = subsurfaceChance(surface, view);
844
+ }
845
+
846
+ if (SUBSURFACE_WALK && lobe.x < chance) {
847
+ let medium = materials[surface.material].subsurface;
848
+
849
+ throughput = throughput
850
+ * ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);
851
+ sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));
852
+ walkAlbedo = subsurfaceAlbedo(surface.albedo);
853
+ walkG = materials[surface.material].subsurfaceAnisotropy;
854
+ walkSteps = 0u;
855
+
856
+ // Light that scattered inside is diffuse light, which is the signal it feeds.
857
+ if (!split) {
858
+ split = true;
859
+ splitBase = radiance;
860
+ splitDiffuse = true;
861
+ }
862
+
863
+ inMedium = true;
864
+ primary = false;
865
+ straight = false;
866
+ bsdfPdf = 0.0;
867
+ lightsOwnDirect = false;
868
+ escapeMap = NO_TEXTURE;
869
+ bounce = bounce + 1u;
870
+ direction = cosineDirection(-surface.facing, bounceSample);
871
+ origin = point - surface.facing * RAY_EPSILON;
872
+ limit = walkDistance(throughput, sigma, pick, &walkOdds);
873
+ continue;
874
+ }
875
+
876
+ throughput = throughput / (1.0 - chance);
877
+
524
878
  let position = point + surface.normal * RAY_EPSILON;
525
879
  var directDiffuse: vec3<f32>;
526
- let direct = throughput * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse);
880
+ let direct = throughput * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,
881
+ select(0.0, volumeDensity, inVolume));
527
882
  let added = select(direct, clampIndirect(direct), bounce > 0u);
528
883
  let before = radiance;
529
884
 
530
885
  radiance = radiance + added;
531
886
 
532
- let scattered = sampleBsdf(surface, view, lobe.x, bounceSample);
887
+ let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
533
888
 
534
889
  // The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
535
890
  if (!split) {
@@ -548,6 +903,8 @@ fn traceRadiance(
548
903
 
549
904
  throughput = throughput * scattered.weight;
550
905
  bsdfPdf = scattered.pdf;
906
+ lastPosition = position;
907
+ lastNormal = treeNormal(surface);
551
908
  lightsOwnDirect = surface.kind == KIND_TOON;
552
909
  escapeMap = surface.envMap;
553
910
  escapeIntensity = surface.envMapIntensity;
@@ -565,7 +922,7 @@ fn traceRadiance(
565
922
  break;
566
923
  }
567
924
 
568
- if (survival < 0.2) {
925
+ if (survival < frame.rouletteThreshold && bounce >= frame.rouletteStart) {
569
926
  if (pick.y > survival) {
570
927
  break;
571
928
  }
@@ -609,6 +966,21 @@ fn traceRadiance(
609
966
  diffuse = fogStart + (diffuse - fogStart) * (1.0 - fogAmount) + fog;
610
967
  }
611
968
 
969
+ // The air takes its share of what lies behind it and adds the light it scatters towards the camera; the background,
970
+ // which already is the sky, is left as it is.
971
+ if (hazed) {
972
+ let through = exp(-frame.atmosphereExtinction.rgb * hazeDistance);
973
+ // The sun lights the air by the phase function of what fills it: Rayleigh's for clean air, a forward lobe for haze
974
+ // (Henyey-Greenstein at 0.76), which is what makes the air glow towards the sun.
975
+ let turn = dot(hazeDirection, frame.atmosphereSun.xyz);
976
+ let phase = mix(3.0 / (16.0 * PI) * (1.0 + turn * turn), henyeyGreenstein(turn, 0.76), frame.atmosphereExtinction.w);
977
+ let air = frame.atmosphereColor.rgb + frame.atmosphereSunColor.rgb * phase;
978
+ let glow = air * hazeThroughput * (vec3<f32>(1.0) - through);
979
+
980
+ radiance = hazeStart + (radiance - hazeStart) * through + glow;
981
+ diffuse = hazeStart + (diffuse - hazeStart) * through + glow;
982
+ }
983
+
612
984
  var result: PathResult;
613
985
  result.radiance = radiance;
614
986
  result.alpha = alpha;
@@ -620,6 +992,7 @@ fn traceRadiance(
620
992
  result.guideDistance = guideDistance;
621
993
  result.roughness = guideRoughness;
622
994
  result.specularDistance = specularDistance;
995
+ result.subsurface = guideSubsurface;
623
996
 
624
997
  return result;
625
998
  }
@@ -645,7 +1018,17 @@ fn lensPoint(u: vec2<f32>) -> vec2<f32> {
645
1018
  */
646
1019
  fn cameraRay(pixel: vec2<u32>, jitter: vec2<f32>, lens: vec2<f32>) -> array<vec4<f32>, 2> {
647
1020
  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);
1021
+ var ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
1022
+
1023
+ // A lens's radial distortion: the pixel sees the scene at a radius scaled by the square of its own, which is one at
1024
+ // the corners. Positive reaches further out at the edges (barrel), negative less far (pincushion).
1025
+ if (frame.lensDistortion != 0.0) {
1026
+ let aspect = f32(frame.resolution.x) / f32(frame.resolution.y);
1027
+ let radius2 = (ndc.x * ndc.x * aspect * aspect + ndc.y * ndc.y) / (aspect * aspect + 1.0);
1028
+
1029
+ ndc = ndc * (1.0 + frame.lensDistortion * radius2);
1030
+ }
1031
+
649
1032
  let ray = ndcRay(ndc);
650
1033
 
651
1034
  if (frame.lens.x <= 0.0 || frame.cameraKind == 1u) {
@@ -965,6 +1348,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
965
1348
  var distanceSum = 0.0;
966
1349
  var roughnessSum = 0.0;
967
1350
  var specularDistanceSum = 0.0;
1351
+ var subsurfaceSum = 0.0;
968
1352
 
969
1353
  for (var s = 0u; s < frame.sampleCount; s = s + 1u) {
970
1354
  var rng = makeSampler(pixel, frame.sampleIndex + s);
@@ -998,6 +1382,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
998
1382
  distanceSum = distanceSum + result.guideDistance;
999
1383
  roughnessSum = roughnessSum + result.roughness;
1000
1384
  specularDistanceSum = specularDistanceSum + result.specularDistance;
1385
+ subsurfaceSum = subsurfaceSum + result.subsurface;
1001
1386
 
1002
1387
  alpha = alpha + sampleAlpha;
1003
1388
  depthSum = depthSum + windowDepth(ray[0].xyz + ray[1].xyz * min(result.depth, 1e20), result.depth);
@@ -1012,7 +1397,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
1012
1397
  accumulation[2u * pc + index] = accumulation[2u * pc + index] + vec4<f32>(diffuseAlbedoSum, roughnessSum);
1013
1398
  accumulation[3u * pc + index] = accumulation[3u * pc + index] + vec4<f32>(normalSum, distanceSum);
1014
1399
  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);
1400
+ accumulation[5u * pc + index] = accumulation[5u * pc + index] + vec4<f32>(specularAlbedoSum, subsurfaceSum);
1016
1401
  // This row still has work: every pixel that traced writes the same value, so the race is harmless.
1017
1402
  accumulation[6u * pc + pixel.y] = vec4<f32>(1.0);
1018
1403
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pathTrace.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8+BzC,CAAC"}
1
+ {"version":3,"file":"pathTrace.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+2CzC,CAAC"}
@@ -5,5 +5,5 @@
5
5
  * random numbers. Only the sample index, the pixel and a dimension counter feed the generator, so a pixel draws the same
6
6
  * numbers whichever dispatch happens to cover it and the image does not depend on the tiling.
7
7
  */
8
- export declare const RANDOM_WGSL = "\nfn pcgHash(value: u32) -> u32 {\n let state = value * 747796405u + 2891336453u;\n let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n\n return (word >> 22u) ^ word;\n}\n\nstruct PathSampler {\n seed: u32,\n index: u32,\n dimension: u32,\n};\n\nfn makeSampler(pixel: vec2<u32>, index: u32) -> PathSampler {\n var rng: PathSampler;\n rng.seed = pcgHash(pixel.x * 1973u + pixel.y * 9277u + 1u);\n rng.index = index;\n rng.dimension = 0u;\n\n return rng;\n}\n\n/**\n * The Laine-Karras hash, which permutes the high bits in a way that nests: what Owen scrambling asks for. Every\n * multiplier has to be even, or a bit stops depending on itself and the map is no longer a permutation.\n */\nfn laineKarras(value: u32, seed: u32) -> u32 {\n var x = value + seed;\n x = x ^ (x * 0x6c50b47cu);\n x = x ^ (x * 0xb82f1e52u);\n x = x ^ (x * 0xc7afe784u);\n x = x ^ (x * 0x8d22f6e6u);\n\n return x;\n}\n\nfn nestedScramble(value: u32, seed: u32) -> u32 {\n return reverseBits(laineKarras(reverseBits(value), seed));\n}\n\n/** The first two Sobol dimensions, as fixed point fractions. */\nfn sobol2(index: u32) -> vec2<u32> {\n var second = 0u;\n var direction = 0x80000000u;\n var bits = index;\n\n loop {\n if (bits == 0u) {\n break;\n }\n\n if ((bits & 1u) != 0u) {\n second = second ^ direction;\n }\n\n bits = bits >> 1u;\n direction = direction ^ (direction >> 1u);\n }\n\n return vec2<u32>(reverseBits(index), second);\n}\n\nfn toUnit(value: u32) -> f32 {\n return min(f32(value) * 2.3283064365386963e-10, 0.99999994);\n}\n\n/**\n * The next pair of dimensions, in [0, 1). The sample index is shuffled per pair as well, which pads the two dimensional\n * sequence out to as many dimensions as a path needs without correlating them.\n */\nfn sample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {\n let pair = (*rng).seed + (*rng).dimension * 2654435761u;\n (*rng).dimension = (*rng).dimension + 1u;\n\n let shuffled = nestedScramble((*rng).index, pcgHash(pair));\n let point = sobol2(shuffled);\n\n return vec2<f32>(\n toUnit(nestedScramble(point.x, pcgHash(pair + 0x9e3779b9u))),\n toUnit(nestedScramble(point.y, pcgHash(pair + 0x3c6ef372u))));\n}\n";
8
+ export declare const RANDOM_WGSL = "\nfn pcgHash(value: u32) -> u32 {\n let state = value * 747796405u + 2891336453u;\n let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n\n return (word >> 22u) ^ word;\n}\n\nstruct PathSampler {\n seed: u32,\n index: u32,\n dimension: u32,\n};\n\nfn makeSampler(pixel: vec2<u32>, index: u32) -> PathSampler {\n var rng: PathSampler;\n rng.seed = pcgHash(pixel.x * 1973u + pixel.y * 9277u + 1u);\n rng.index = index;\n rng.dimension = 0u;\n\n return rng;\n}\n\n/**\n * The Laine-Karras hash, which permutes the high bits in a way that nests: what Owen scrambling asks for. Every\n * multiplier has to be even, or a bit stops depending on itself and the map is no longer a permutation.\n */\nfn laineKarras(value: u32, seed: u32) -> u32 {\n var x = value + seed;\n x = x ^ (x * 0x6c50b47cu);\n x = x ^ (x * 0xb82f1e52u);\n x = x ^ (x * 0xc7afe784u);\n x = x ^ (x * 0x8d22f6e6u);\n\n return x;\n}\n\nfn nestedScramble(value: u32, seed: u32) -> u32 {\n return reverseBits(laineKarras(reverseBits(value), seed));\n}\n\n/**\n * The first two Sobol dimensions, as fixed point fractions. The second is the index times Pascal's triangle mod 2,\n * which is linear in the bits of the index, so it is read one byte at a time from the tables texturePack.ts writes\n * instead of folded in one bit at a time: four reads where the scrambled index made it thirty two steps.\n */\nfn sobol2(index: u32) -> vec2<u32> {\n let second = textures[SOBOL_TABLE + (index & 0xffu)]\n ^ textures[SOBOL_TABLE + 256u + ((index >> 8u) & 0xffu)]\n ^ textures[SOBOL_TABLE + 512u + ((index >> 16u) & 0xffu)]\n ^ textures[SOBOL_TABLE + 768u + (index >> 24u)];\n\n return vec2<u32>(reverseBits(index), second);\n}\n\nfn toUnit(value: u32) -> f32 {\n return min(f32(value) * 2.3283064365386963e-10, 0.99999994);\n}\n\n/**\n * The next pair of dimensions, in [0, 1). The sample index is shuffled per pair as well, which pads the two dimensional\n * sequence out to as many dimensions as a path needs without correlating them.\n */\nfn sample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {\n let pair = (*rng).seed + (*rng).dimension * 2654435761u;\n (*rng).dimension = (*rng).dimension + 1u;\n\n let shuffled = nestedScramble((*rng).index, pcgHash(pair));\n let point = sobol2(shuffled);\n\n return vec2<f32>(\n toUnit(nestedScramble(point.x, pcgHash(pair + 0x9e3779b9u))),\n toUnit(nestedScramble(point.y, pcgHash(pair + 0x3c6ef372u))));\n}\n";
9
9
  //# sourceMappingURL=random.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"random.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,4rEAkFvB,CAAC"}
1
+ {"version":3,"file":"random.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,y6EA0EvB,CAAC"}