@tinypoly/gpu-renderer 0.0.5 → 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 (110) hide show
  1. package/ARCHITECTURE.md +59 -3
  2. package/README.md +52 -4
  3. package/dist/atmosphere.d.ts +21 -0
  4. package/dist/atmosphere.d.ts.map +1 -0
  5. package/dist/atmosphere.js +72 -0
  6. package/dist/atmosphere.js.map +1 -0
  7. package/dist/denoise.d.ts +11 -1
  8. package/dist/denoise.d.ts.map +1 -1
  9. package/dist/denoise.js +34 -6
  10. package/dist/denoise.js.map +1 -1
  11. package/dist/frameLayout.d.ts.map +1 -1
  12. package/dist/frameLayout.js +15 -0
  13. package/dist/frameLayout.js.map +1 -1
  14. package/dist/geometry.d.ts.map +1 -1
  15. package/dist/geometry.js +6 -3
  16. package/dist/geometry.js.map +1 -1
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/lensPass.d.ts +60 -0
  22. package/dist/lensPass.d.ts.map +1 -0
  23. package/dist/lensPass.js +235 -0
  24. package/dist/lensPass.js.map +1 -0
  25. package/dist/lightTree.d.ts +43 -0
  26. package/dist/lightTree.d.ts.map +1 -0
  27. package/dist/lightTree.js +107 -0
  28. package/dist/lightTree.js.map +1 -0
  29. package/dist/lights.d.ts +10 -0
  30. package/dist/lights.d.ts.map +1 -1
  31. package/dist/lights.js +54 -1
  32. package/dist/lights.js.map +1 -1
  33. package/dist/materialLayout.d.ts +24 -0
  34. package/dist/materialLayout.d.ts.map +1 -1
  35. package/dist/materialLayout.js +36 -1
  36. package/dist/materialLayout.js.map +1 -1
  37. package/dist/pathTracePass.d.ts +6 -3
  38. package/dist/pathTracePass.d.ts.map +1 -1
  39. package/dist/pathTracePass.js +16 -10
  40. package/dist/pathTracePass.js.map +1 -1
  41. package/dist/renderer.d.ts +19 -7
  42. package/dist/renderer.d.ts.map +1 -1
  43. package/dist/renderer.js +118 -14
  44. package/dist/renderer.js.map +1 -1
  45. package/dist/sceneUpload.d.ts +5 -0
  46. package/dist/sceneUpload.d.ts.map +1 -1
  47. package/dist/sceneUpload.js +4 -0
  48. package/dist/sceneUpload.js.map +1 -1
  49. package/dist/settings.d.ts +136 -0
  50. package/dist/settings.d.ts.map +1 -0
  51. package/dist/settings.js +100 -0
  52. package/dist/settings.js.map +1 -0
  53. package/dist/subsurface.d.ts +76 -0
  54. package/dist/subsurface.d.ts.map +1 -0
  55. package/dist/subsurface.js +70 -0
  56. package/dist/subsurface.js.map +1 -0
  57. package/dist/volumeContainment.d.ts +17 -0
  58. package/dist/volumeContainment.d.ts.map +1 -0
  59. package/dist/volumeContainment.js +61 -0
  60. package/dist/volumeContainment.js.map +1 -0
  61. package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
  62. package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
  63. package/dist/wgsl/bsdf.wgsl.js +35 -3
  64. package/dist/wgsl/bsdf.wgsl.js.map +1 -1
  65. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  66. package/dist/wgsl/common.wgsl.js +4 -0
  67. package/dist/wgsl/common.wgsl.js.map +1 -1
  68. package/dist/wgsl/lens.wgsl.d.ts +17 -0
  69. package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
  70. package/dist/wgsl/lens.wgsl.js +315 -0
  71. package/dist/wgsl/lens.wgsl.js.map +1 -0
  72. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  73. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  74. package/dist/wgsl/lights.wgsl.js +203 -31
  75. package/dist/wgsl/lights.wgsl.js.map +1 -1
  76. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  77. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  78. package/dist/wgsl/pathTrace.wgsl.js +396 -25
  79. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  80. package/dist/wgsl/sampling.wgsl.d.ts +1 -1
  81. package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
  82. package/dist/wgsl/sampling.wgsl.js +25 -0
  83. package/dist/wgsl/sampling.wgsl.js.map +1 -1
  84. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  85. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  86. package/dist/wgsl/surface.wgsl.js +21 -1
  87. package/dist/wgsl/surface.wgsl.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/atmosphere.ts +95 -0
  90. package/src/denoise.ts +44 -6
  91. package/src/frameLayout.ts +15 -0
  92. package/src/geometry.ts +6 -2
  93. package/src/index.ts +13 -0
  94. package/src/lensPass.ts +299 -0
  95. package/src/lightTree.ts +172 -0
  96. package/src/lights.ts +71 -1
  97. package/src/materialLayout.ts +52 -1
  98. package/src/pathTracePass.ts +19 -10
  99. package/src/renderer.ts +166 -22
  100. package/src/sceneUpload.ts +9 -0
  101. package/src/settings.ts +234 -0
  102. package/src/subsurface.ts +153 -0
  103. package/src/volumeContainment.ts +81 -0
  104. package/src/wgsl/bsdf.wgsl.ts +35 -3
  105. package/src/wgsl/common.wgsl.ts +4 -0
  106. package/src/wgsl/lens.wgsl.ts +314 -0
  107. package/src/wgsl/lights.wgsl.ts +203 -31
  108. package/src/wgsl/pathTrace.wgsl.ts +396 -25
  109. package/src/wgsl/sampling.wgsl.ts +25 -0
  110. package/src/wgsl/surface.wgsl.ts +21 -1
@@ -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.
@@ -344,7 +629,12 @@ fn traceRadiance(
344
629
  }
345
630
  }
346
631
 
347
- let surface = resolveSurfaceFrom(hit.triangle, bary, direction, base);
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
+ }
348
638
 
349
639
  // Blending other than normal: the surface adds its light to, or filters, whatever is behind it, and the path goes on.
350
640
  if (surface.blending == BLEND_ADDITIVE || surface.blending == BLEND_MULTIPLY || surface.blending == BLEND_SUBTRACTIVE) {
@@ -358,7 +648,7 @@ fn traceRadiance(
358
648
  if ((surface.flags & MATERIAL_LIT) != 0u) {
359
649
  var unused: vec3<f32>;
360
650
  added = added + directLight(surface, point + surface.normal * RAY_EPSILON, -direction, pick.x, lightSample, rng,
361
- &unused);
651
+ &unused, select(0.0, volumeDensity, inVolume));
362
652
  }
363
653
 
364
654
  radiance = radiance + throughput * added * strength;
@@ -410,6 +700,7 @@ fn traceRadiance(
410
700
  guideNormal = surface.normal;
411
701
  guideDistance = length(point - rayOrigin);
412
702
  guideRoughness = surface.roughness;
703
+ guideSubsurface = select(0.0, surface.subsurface, SUBSURFACE_WALK);
413
704
 
414
705
  if (surface.kind == KIND_PHONG) {
415
706
  // Phong's exponent as the roughness of the GGX lobe that is about as wide.
@@ -420,7 +711,7 @@ fn traceRadiance(
420
711
  if (primary) {
421
712
  alpha = 1.0;
422
713
 
423
- if (!fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {
714
+ if (!scattering && !fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {
424
715
  afterMaterial = m;
425
716
  afterTriangle = hit.triangle;
426
717
  afterBary = bary;
@@ -444,11 +735,20 @@ fn traceRadiance(
444
735
  }
445
736
  }
446
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
+
447
747
  radiance = radiance + throughput * surface.lightMap;
448
748
  }
449
749
 
450
750
  if (any(surface.emissive > vec3<f32>(0.0))) {
451
- let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect);
751
+ let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect, lastPosition, lastNormal);
452
752
 
453
753
  // Seen by the camera or one bounce away this is direct light and stays exact; deeper it may be clamped.
454
754
  radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);
@@ -535,15 +835,56 @@ fn traceRadiance(
535
835
  bounce = bounce + 1u;
536
836
  }
537
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
+
538
878
  let position = point + surface.normal * RAY_EPSILON;
539
879
  var directDiffuse: vec3<f32>;
540
- 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));
541
882
  let added = select(direct, clampIndirect(direct), bounce > 0u);
542
883
  let before = radiance;
543
884
 
544
885
  radiance = radiance + added;
545
886
 
546
- let scattered = sampleBsdf(surface, view, lobe.x, bounceSample);
887
+ let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);
547
888
 
548
889
  // The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.
549
890
  if (!split) {
@@ -562,6 +903,8 @@ fn traceRadiance(
562
903
 
563
904
  throughput = throughput * scattered.weight;
564
905
  bsdfPdf = scattered.pdf;
906
+ lastPosition = position;
907
+ lastNormal = treeNormal(surface);
565
908
  lightsOwnDirect = surface.kind == KIND_TOON;
566
909
  escapeMap = surface.envMap;
567
910
  escapeIntensity = surface.envMapIntensity;
@@ -579,7 +922,7 @@ fn traceRadiance(
579
922
  break;
580
923
  }
581
924
 
582
- if (survival < 0.2) {
925
+ if (survival < frame.rouletteThreshold && bounce >= frame.rouletteStart) {
583
926
  if (pick.y > survival) {
584
927
  break;
585
928
  }
@@ -623,6 +966,21 @@ fn traceRadiance(
623
966
  diffuse = fogStart + (diffuse - fogStart) * (1.0 - fogAmount) + fog;
624
967
  }
625
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
+
626
984
  var result: PathResult;
627
985
  result.radiance = radiance;
628
986
  result.alpha = alpha;
@@ -634,6 +992,7 @@ fn traceRadiance(
634
992
  result.guideDistance = guideDistance;
635
993
  result.roughness = guideRoughness;
636
994
  result.specularDistance = specularDistance;
995
+ result.subsurface = guideSubsurface;
637
996
 
638
997
  return result;
639
998
  }
@@ -659,7 +1018,17 @@ fn lensPoint(u: vec2<f32>) -> vec2<f32> {
659
1018
  */
660
1019
  fn cameraRay(pixel: vec2<u32>, jitter: vec2<f32>, lens: vec2<f32>) -> array<vec4<f32>, 2> {
661
1020
  let uv = (vec2<f32>(pixel) + jitter) / vec2<f32>(frame.resolution);
662
- 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
+
663
1032
  let ray = ndcRay(ndc);
664
1033
 
665
1034
  if (frame.lens.x <= 0.0 || frame.cameraKind == 1u) {
@@ -979,6 +1348,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
979
1348
  var distanceSum = 0.0;
980
1349
  var roughnessSum = 0.0;
981
1350
  var specularDistanceSum = 0.0;
1351
+ var subsurfaceSum = 0.0;
982
1352
 
983
1353
  for (var s = 0u; s < frame.sampleCount; s = s + 1u) {
984
1354
  var rng = makeSampler(pixel, frame.sampleIndex + s);
@@ -1012,6 +1382,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
1012
1382
  distanceSum = distanceSum + result.guideDistance;
1013
1383
  roughnessSum = roughnessSum + result.roughness;
1014
1384
  specularDistanceSum = specularDistanceSum + result.specularDistance;
1385
+ subsurfaceSum = subsurfaceSum + result.subsurface;
1015
1386
 
1016
1387
  alpha = alpha + sampleAlpha;
1017
1388
  depthSum = depthSum + windowDepth(ray[0].xyz + ray[1].xyz * min(result.depth, 1e20), result.depth);
@@ -1026,7 +1397,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
1026
1397
  accumulation[2u * pc + index] = accumulation[2u * pc + index] + vec4<f32>(diffuseAlbedoSum, roughnessSum);
1027
1398
  accumulation[3u * pc + index] = accumulation[3u * pc + index] + vec4<f32>(normalSum, distanceSum);
1028
1399
  accumulation[4u * pc + index] = accumulation[4u * pc + index] + vec4<f32>(diffuseSum, specularDistanceSum);
1029
- 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);
1030
1401
  // This row still has work: every pixel that traced writes the same value, so the race is harmless.
1031
1402
  accumulation[6u * pc + pixel.y] = vec4<f32>(1.0);
1032
1403
  }
@@ -21,6 +21,31 @@ fn cosineDirection(normal: vec3<f32>, u: vec2<f32>) -> vec3<f32> {
21
21
  return normalize(orthonormalBasis(normal) * local);
22
22
  }
23
23
 
24
+ /** A direction scattered from forward by the Henyey-Greenstein phase function, which its own sampling cancels. */
25
+ fn sampleHenyeyGreenstein(forward: vec3<f32>, g: f32, u: vec2<f32>) -> vec3<f32> {
26
+ var cosine = 1.0 - 2.0 * u.x;
27
+
28
+ if (abs(g) > 1e-3) {
29
+ let s = (1.0 - g * g) / (1.0 - g + 2.0 * g * u.x);
30
+ cosine = clamp((1.0 + g * g - s * s) / (2.0 * g), -1.0, 1.0);
31
+ }
32
+
33
+ let sine = sqrt(max(0.0, 1.0 - cosine * cosine));
34
+ let angle = 2.0 * PI * u.y;
35
+
36
+ return normalize(orthonormalBasis(forward) * vec3<f32>(sine * cos(angle), sine * sin(angle), cosine));
37
+ }
38
+
39
+ /**
40
+ * The Henyey-Greenstein phase function, per steradian, for a turn whose cosine is given from the direction of travel:
41
+ * the density sampleHenyeyGreenstein draws with.
42
+ */
43
+ fn henyeyGreenstein(cosine: f32, g: f32) -> f32 {
44
+ let denominator = 1.0 + g * g - 2.0 * g * cosine;
45
+
46
+ return (1.0 - g * g) / (4.0 * PI * denominator * sqrt(max(denominator, 1e-12)));
47
+ }
48
+
24
49
  /** The unit normal of the triangle's plane; which side it faces does not matter, every emitter is two sided. */
25
50
  fn triangleNormal(triangle: u32) -> vec3<f32> {
26
51
  let a = triangleVertex(triangle, 0u);