@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
@@ -175,6 +175,12 @@ fn coatTransmittance(surface: Surface, view: vec3<f32>) -> f32 {
175
175
  return 1.0 - surface.clearcoat * schlick3(vec3<f32>(0.04), 1.0, nv).x;
176
176
  }
177
177
 
178
+ /**
179
+ * A point inside a volume, where light scatters by the medium's phase function instead of off a surface. Only the
180
+ * integrator makes one: its normal faces back along the ray, its albedo is the medium's, its anisotropy the phase's.
181
+ */
182
+ const KIND_VOLUME: u32 = 100u;
183
+
178
184
  /** How often each lobe is chosen, as (diffuse, specular, clearcoat): by how much of the reflection each accounts for. */
179
185
  fn lobeWeights(surface: Surface, view: vec3<f32>) -> vec3<f32> {
180
186
  let nv = max(dot(surface.normal, view), 1e-4);
@@ -192,7 +198,8 @@ fn lobeWeights(surface: Surface, view: vec3<f32>) -> vec3<f32> {
192
198
 
193
199
  let base = coatTransmittance(surface, view);
194
200
  // The floors keep every lobe reachable: the diffuse one also carries the sheen and the multiple scattering term.
195
- let diffuse = (luminanceOf(surface.albedo * (1.0 - surface.metalness)) + max3(surface.sheenColor)) * base + 0.02;
201
+ let diffuse = (luminanceOf(surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)))
202
+ + max3(surface.sheenColor)) * base + 0.02;
196
203
  let specular = luminanceOf(schlick3(surface.specularColor, surface.specularF90, nv)
197
204
  * (1.0 - surface.metalness) + surface.albedo * surface.metalness) * base + 0.05;
198
205
  let coat = select(0.0, (1.0 - base) + 0.02, surface.clearcoat > 0.0);
@@ -235,7 +242,8 @@ fn physicalValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f3
235
242
  let average = f0 + (vec3<f32>(1.0) - f0) * 0.047619;
236
243
  let multiple = essV * essL * average / (vec3<f32>(1.0) - emsV * emsL * average + 1e-7) * (emsV * emsL);
237
244
 
238
- var diffuse = surface.albedo * (1.0 - surface.metalness) / PI;
245
+ // What scatters inside the material leaves it elsewhere, by the random walk, not from this point.
246
+ var diffuse = surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)) / PI;
239
247
  var specular = single + multiple;
240
248
 
241
249
  // Three's sheen: its lobe sits on top and takes back the energy it reflects from the layers below.
@@ -344,6 +352,16 @@ fn evaluateBsdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> BsdfEval
344
352
  let nl = dot(normal, light);
345
353
  let nv = dot(normal, view);
346
354
 
355
+ // In a medium every direction is open: the phase function stands for the lobes, and samples itself exactly.
356
+ if (surface.kind == KIND_VOLUME) {
357
+ let phase = henyeyGreenstein(dot(-view, light), surface.anisotropy);
358
+
359
+ result.value = surface.albedo * phase;
360
+ result.pdf = phase;
361
+
362
+ return result;
363
+ }
364
+
347
365
  if (nv <= 0.0) {
348
366
  return result;
349
367
  }
@@ -507,6 +525,15 @@ fn sampleBsdf(surface: Surface, view: vec3<f32>, lobe: f32, u: vec2<f32>) -> Bsd
507
525
  result.pdf = 0.0;
508
526
  result.diffuse = true;
509
527
 
528
+ if (surface.kind == KIND_VOLUME) {
529
+ result.direction = sampleHenyeyGreenstein(-view, surface.anisotropy, u);
530
+ result.weight = surface.albedo;
531
+ result.pdf = henyeyGreenstein(dot(-view, result.direction), surface.anisotropy);
532
+ result.valid = true;
533
+
534
+ return result;
535
+ }
536
+
510
537
  if (dot(normal, view) <= 0.0) {
511
538
  return result;
512
539
  }
@@ -574,6 +601,11 @@ fn diffuseValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32
574
601
  return surface.albedo / PI * toonRamp(surface.gradientMap, nl);
575
602
  }
576
603
 
604
+ // Light scattered in a medium is all diffuse, as the denoiser sees it.
605
+ if (surface.kind == KIND_VOLUME) {
606
+ return surface.albedo * henyeyGreenstein(dot(-view, light), surface.anisotropy);
607
+ }
608
+
577
609
  if (nl <= 0.0) {
578
610
  return vec3<f32>(0.0);
579
611
  }
@@ -582,7 +614,7 @@ fn diffuseValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32
582
614
  return surface.albedo / PI * nl;
583
615
  }
584
616
 
585
- var diffuse = surface.albedo * (1.0 - surface.metalness) / PI;
617
+ var diffuse = surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)) / PI;
586
618
 
587
619
  if (any(surface.sheenColor > vec3<f32>(0.0))) {
588
620
  diffuse = diffuse * (1.0 - max3(surface.sheenColor)
@@ -1 +1 @@
1
- {"version":3,"file":"bsdf.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bsdf.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8lBnC,CAAC"}
1
+ {"version":3,"file":"bsdf.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bsdf.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8nBnC,CAAC"}
@@ -1,3 +1,9 @@
1
- /** Triangle intersection and the iterative traversal, with the fixed stack the builder caps its depth to. */
2
- export declare const BVH_WGSL = "\nfn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {\n let base = triangle * 9u + corner * 3u;\n\n return vec3<f32>(positions[base], positions[base + 1u], positions[base + 2u]);\n}\n\n/** Moller-Trumbore. Returns the distance in x and the barycentric coordinates in yz, or a negative distance. */\nfn intersectTriangle(triangle: u32, origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {\n let miss = vec3<f32>(-1.0, 0.0, 0.0);\n let a = triangleVertex(triangle, 0u);\n let edge1 = triangleVertex(triangle, 1u) - a;\n let edge2 = triangleVertex(triangle, 2u) - a;\n let p = cross(direction, edge2);\n let determinant = dot(edge1, p);\n\n if (abs(determinant) < 1e-12) {\n return miss;\n }\n\n let inverse = 1.0 / determinant;\n let offset = origin - a;\n let u = dot(offset, p) * inverse;\n\n if (u < 0.0 || u > 1.0) {\n return miss;\n }\n\n let q = cross(offset, edge1);\n let v = dot(direction, q) * inverse;\n\n if (v < 0.0 || u + v > 1.0) {\n return miss;\n }\n\n return vec3<f32>(dot(edge2, q) * inverse, u, v);\n}\n\n/** Where a ray enters a node's box, or 1e30 when it misses it or only meets it past maxDistance. */\nfn enterBox(node: BvhNode, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f32) -> f32 {\n let first = (node.low - origin) * inverse;\n let second = (node.high - origin) * inverse;\n let near = min(first, second);\n let far = max(first, second);\n let enter = max(max(near.x, near.y), max(near.z, 0.0));\n let exit = min(min(far.x, far.y), min(far.z, maxDistance));\n\n return select(1e30, enter, enter <= exit);\n}\n\n/** The triangles of a leaf, against the closest hit so far. */\nfn intersectLeaf(node: BvhNode, origin: vec3<f32>, direction: vec3<f32>, hit: ptr<function, Hit>) {\n let start = u32(-1 - node.link);\n\n for (var i = 0u; i < u32(node.count); i = i + 1u) {\n let triangle = orderedTriangle(start + i);\n let result = intersectTriangle(triangle, origin, direction);\n\n if (result.x > RAY_EPSILON && result.x < (*hit).distance) {\n (*hit).distance = result.x;\n (*hit).triangle = triangle;\n (*hit).u = result.y;\n (*hit).v = result.z;\n }\n }\n}\n\n/**\n * The closest hit along a ray. Both children of a node are tested together and the nearer one is visited first, so\n * the closest hit shrinks the search early and whole subtrees behind it are skipped; each node remembers where the ray\n * entered it, and one whose entry is already past the closest hit is dropped without being opened.\n */\nfn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit {\n var hit: Hit;\n hit.distance = maxDistance;\n hit.triangle = NO_HIT;\n hit.u = 0.0;\n hit.v = 0.0;\n\n if (frame.triangleCount == 0u) {\n return hit;\n }\n\n // A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf.\n let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));\n let inverse = signs / max(abs(direction), vec3<f32>(1e-20));\n let rootEnter = enterBox(nodes[0], origin, inverse, hit.distance);\n\n if (rootEnter >= 1e30) {\n return hit;\n }\n\n var stack: array<u32, 32>;\n var enters: array<f32, 32>;\n var depth = 1u;\n stack[0] = 0u;\n enters[0] = rootEnter;\n\n loop {\n if (depth == 0u) {\n break;\n }\n\n depth = depth - 1u;\n\n // Something closer was found since this node was pushed.\n if (enters[depth] > hit.distance) {\n continue;\n }\n\n let node = nodes[stack[depth]];\n\n if (node.link < 0) {\n intersectLeaf(node, origin, direction, &hit);\n continue;\n }\n\n let leftIndex = u32(node.link);\n let rightIndex = u32(node.count);\n let leftEnter = enterBox(nodes[leftIndex], origin, inverse, hit.distance);\n let rightEnter = enterBox(nodes[rightIndex], origin, inverse, hit.distance);\n let leftHit = leftEnter < 1e30;\n let rightHit = rightEnter < 1e30;\n\n // The builder caps its depth so both children always fit; the guard keeps a malformed tree inside the stack.\n if (depth + 2u > 32u) {\n continue;\n }\n\n if (leftHit && rightHit) {\n // The farther first, so the nearer comes off the stack next.\n let leftNearer = leftEnter <= rightEnter;\n stack[depth] = select(leftIndex, rightIndex, leftNearer);\n enters[depth] = select(leftEnter, rightEnter, leftNearer);\n stack[depth + 1u] = select(rightIndex, leftIndex, leftNearer);\n enters[depth + 1u] = select(rightEnter, leftEnter, leftNearer);\n depth = depth + 2u;\n } else if (leftHit) {\n stack[depth] = leftIndex;\n enters[depth] = leftEnter;\n depth = depth + 1u;\n } else if (rightHit) {\n stack[depth] = rightIndex;\n enters[depth] = rightEnter;\n depth = depth + 1u;\n }\n }\n\n return hit;\n}\n\n/**\n * Whether anything at all lies along a ray before maxDistance, stopping at the first triangle it finds rather than the\n * closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT.\n */\nfn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {\n if (frame.triangleCount == 0u) {\n return NO_HIT;\n }\n\n let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));\n let inverse = signs / max(abs(direction), vec3<f32>(1e-20));\n\n if (enterBox(nodes[0], origin, inverse, maxDistance) >= 1e30) {\n return NO_HIT;\n }\n\n var stack: array<u32, 32>;\n var depth = 1u;\n stack[0] = 0u;\n\n loop {\n if (depth == 0u) {\n break;\n }\n\n depth = depth - 1u;\n\n let node = nodes[stack[depth]];\n\n if (node.link < 0) {\n let start = u32(-1 - node.link);\n\n for (var i = 0u; i < u32(node.count); i = i + 1u) {\n let triangle = orderedTriangle(start + i);\n let result = intersectTriangle(triangle, origin, direction);\n\n if (result.x > RAY_EPSILON && result.x < maxDistance) {\n return triangle;\n }\n }\n\n continue;\n }\n\n let leftIndex = u32(node.link);\n let rightIndex = u32(node.count);\n\n if (depth + 2u > 32u) {\n continue;\n }\n\n if (enterBox(nodes[leftIndex], origin, inverse, maxDistance) < 1e30) {\n stack[depth] = leftIndex;\n depth = depth + 1u;\n }\n\n if (enterBox(nodes[rightIndex], origin, inverse, maxDistance) < 1e30) {\n stack[depth] = rightIndex;\n depth = depth + 1u;\n }\n }\n\n return NO_HIT;\n}\n";
1
+ /**
2
+ * Triangle intersection and the iterative traversal, with the fixed stack the builder caps its depth to.
3
+ *
4
+ * The positions are uploaded in the hierarchy's leaf order, so a leaf is a run of triangles read straight off the
5
+ * buffer; the scene's own triangle number is looked up once, for the hit a trace returns. Every other reader goes the
6
+ * other way, from a triangle number to where its corners sit.
7
+ */
8
+ export declare const BVH_WGSL = "\n/** A corner of the triangle at this position of the hierarchy's leaf order. */\nfn leafVertex(at: u32, corner: u32) -> vec3<f32> {\n let base = at * 9u + corner * 3u;\n\n return vec3<f32>(positions[base], positions[base + 1u], positions[base + 2u]);\n}\n\n/** Where a triangle of the scene sits in the leaf order the positions are stored in. */\nfn leafOf(triangle: u32) -> u32 {\n return indices[frame.leafOffset + triangle];\n}\n\nfn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {\n return leafVertex(leafOf(triangle), corner);\n}\n\n/** Moller-Trumbore. Returns the distance in x and the barycentric coordinates in yz, or a negative distance. */\nfn intersectTriangle(at: u32, origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {\n let miss = vec3<f32>(-1.0, 0.0, 0.0);\n let a = leafVertex(at, 0u);\n let edge1 = leafVertex(at, 1u) - a;\n let edge2 = leafVertex(at, 2u) - a;\n let p = cross(direction, edge2);\n let determinant = dot(edge1, p);\n\n if (abs(determinant) < 1e-12) {\n return miss;\n }\n\n let inverse = 1.0 / determinant;\n let offset = origin - a;\n let u = dot(offset, p) * inverse;\n\n if (u < 0.0 || u > 1.0) {\n return miss;\n }\n\n let q = cross(offset, edge1);\n let v = dot(direction, q) * inverse;\n\n if (v < 0.0 || u + v > 1.0) {\n return miss;\n }\n\n return vec3<f32>(dot(edge2, q) * inverse, u, v);\n}\n\n/** Where a ray enters a box, or 1e30 when it misses it or only meets it past maxDistance. */\nfn enterBox(low: vec3<f32>, high: vec3<f32>, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f32) -> f32 {\n let first = (low - origin) * inverse;\n let second = (high - origin) * inverse;\n let near = min(first, second);\n let far = max(first, second);\n let enter = max(max(near.x, near.y), max(near.z, 0.0));\n let exit = min(min(far.x, far.y), min(far.z, maxDistance));\n\n return select(1e30, enter, enter <= exit);\n}\n\n/** The triangles of a leaf, against the closest hit so far, which records where in the leaf order it found one. */\nfn intersectLeaf(link: i32, count: i32, origin: vec3<f32>, direction: vec3<f32>, hit: ptr<function, Hit>) {\n let start = u32(-1 - link);\n\n for (var i = 0u; i < u32(count); i = i + 1u) {\n let result = intersectTriangle(start + i, origin, direction);\n\n if (result.x > RAY_EPSILON && result.x < (*hit).distance) {\n (*hit).distance = result.x;\n (*hit).triangle = start + i;\n (*hit).u = result.y;\n (*hit).v = result.z;\n }\n }\n}\n\n/** Whether any triangle of a leaf lies along the ray before maxDistance: its position in the leaf order, or NO_HIT. */\nfn anyInLeaf(link: i32, count: i32, origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {\n let start = u32(-1 - link);\n\n for (var i = 0u; i < u32(count); i = i + 1u) {\n let result = intersectTriangle(start + i, origin, direction);\n\n if (result.x > RAY_EPSILON && result.x < maxDistance) {\n return start + i;\n }\n }\n\n return NO_HIT;\n}\n\n/** No node to go to: the traversal takes the next entry off the stack. */\nconst NO_NODE: u32 = 0xffffffffu;\n\n/**\n * A stack entry that is a leaf rather than a node: the node holding it and which of its two children it is. A leaf\n * has no node of its own, so this is how one waits its turn.\n */\nconst LEAF_ENTRY: u32 = 0x80000000u;\n\nfn leafEntry(parent: u32, right: bool) -> u32 {\n return LEAF_ENTRY | (parent << 1u) | select(0u, 1u, right);\n}\n\n/** A waiting leaf's link and count, read back from the node that holds it. */\nfn waitingLeaf(entry: u32) -> vec2<i32> {\n let parent = (entry & ~LEAF_ENTRY) >> 1u;\n\n if ((entry & 1u) != 0u) {\n return vec2<i32>(nodes[parent].rightLink, nodes[parent].rightCount);\n }\n\n return vec2<i32>(nodes[parent].leftLink, nodes[parent].leftCount);\n}\n\n/**\n * The closest hit along a ray. A node holds both of its children's boxes, so one read tests them both. The nearer is\n * visited first, so the closest hit shrinks the search early and whole subtrees behind it are skipped; the farther one\n * waits on the stack with where the ray entered it, and is dropped unopened if something closer turned up meanwhile.\n * A leaf is tested at the top of the next step, the one place the loop tests triangles, which keeps the kernel small.\n * This visits what the binary tree's traversal visited, in the same order, so it finds the same hit.\n */\nfn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit {\n var hit: Hit;\n hit.distance = maxDistance;\n hit.triangle = NO_HIT;\n hit.u = 0.0;\n hit.v = 0.0;\n\n if (frame.triangleCount == 0u) {\n return hit;\n }\n\n // A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf.\n let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));\n let inverse = signs / max(abs(direction), vec3<f32>(1e-20));\n\n var stack: array<u32, 32>;\n var enters: array<f32, 32>;\n var depth = 0u;\n var current = 0u;\n // A leaf to test before anything else: its link, and its triangle count, zero when there is none.\n var leaf = vec2<i32>(0, 0);\n\n loop {\n if (leaf.y > 0) {\n intersectLeaf(leaf.x, leaf.y, origin, direction, &hit);\n leaf.y = 0;\n }\n\n if (current == NO_NODE) {\n if (depth == 0u) {\n break;\n }\n\n depth = depth - 1u;\n\n // Something closer was found since this was pushed.\n if (enters[depth] > hit.distance) {\n continue;\n }\n\n let entry = stack[depth];\n\n if ((entry & LEAF_ENTRY) != 0u) {\n leaf = waitingLeaf(entry);\n continue;\n }\n\n current = entry;\n }\n\n let node = nodes[current];\n let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, hit.distance);\n let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, hit.distance);\n let leftFirst = leftEnter <= rightEnter;\n let nearLink = select(node.rightLink, node.leftLink, leftFirst);\n let farEnter = select(leftEnter, rightEnter, leftFirst);\n let farLink = select(node.leftLink, node.rightLink, leftFirst);\n let parent = current;\n\n current = NO_NODE;\n\n // The nearer child: a miss means both missed.\n if (min(leftEnter, rightEnter) < 1e30) {\n if (nearLink < 0) {\n leaf = vec2<i32>(nearLink, select(node.rightCount, node.leftCount, leftFirst));\n } else {\n current = u32(nearLink);\n }\n }\n\n // The builder caps its depth so the far children always fit; the guard keeps a malformed tree inside the stack.\n if (farEnter < 1e30 && depth < 32u) {\n stack[depth] = select(u32(farLink), leafEntry(parent, leftFirst), farLink < 0);\n enters[depth] = farEnter;\n depth = depth + 1u;\n }\n }\n\n if (hit.triangle != NO_HIT) {\n hit.triangle = orderedTriangle(hit.triangle);\n }\n\n return hit;\n}\n\n/**\n * Whether anything at all lies along a ray before maxDistance, stopping at the first triangle it finds rather than the\n * closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT. The\n * right child goes first and the left one waits, the order the binary tree's traversal took.\n */\nfn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {\n if (frame.triangleCount == 0u) {\n return NO_HIT;\n }\n\n let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));\n let inverse = signs / max(abs(direction), vec3<f32>(1e-20));\n\n var stack: array<u32, 32>;\n var depth = 0u;\n var current = 0u;\n var leaf = vec2<i32>(0, 0);\n\n loop {\n if (leaf.y > 0) {\n let found = anyInLeaf(leaf.x, leaf.y, origin, direction, maxDistance);\n\n if (found != NO_HIT) {\n return orderedTriangle(found);\n }\n\n leaf.y = 0;\n }\n\n if (current == NO_NODE) {\n if (depth == 0u) {\n break;\n }\n\n depth = depth - 1u;\n\n let entry = stack[depth];\n\n if ((entry & LEAF_ENTRY) != 0u) {\n leaf = waitingLeaf(entry);\n continue;\n }\n\n current = entry;\n }\n\n let node = nodes[current];\n let rightHit = enterBox(node.rightLow, node.rightHigh, origin, inverse, maxDistance) < 1e30;\n let leftHit = enterBox(node.leftLow, node.leftHigh, origin, inverse, maxDistance) < 1e30;\n let parent = current;\n\n current = NO_NODE;\n\n // The right child is taken now and the left one waits; with only one of them hit, that one is taken now.\n let takeRight = rightHit;\n let takenLink = select(node.leftLink, node.rightLink, takeRight);\n\n if (rightHit || leftHit) {\n if (takenLink < 0) {\n leaf = vec2<i32>(takenLink, select(node.leftCount, node.rightCount, takeRight));\n } else {\n current = u32(takenLink);\n }\n }\n\n if (rightHit && leftHit && depth < 32u) {\n stack[depth] = select(u32(node.leftLink), leafEntry(parent, false), node.leftLink < 0);\n depth = depth + 1u;\n }\n }\n\n return NO_HIT;\n}\n";
3
9
  //# sourceMappingURL=bvh.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bvh.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,eAAO,MAAM,QAAQ,y4MAuNpB,CAAC"}
1
+ {"version":3,"file":"bvh.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,+5RAwRpB,CAAC"}
@@ -1,17 +1,33 @@
1
- /** Triangle intersection and the iterative traversal, with the fixed stack the builder caps its depth to. */
1
+ /**
2
+ * Triangle intersection and the iterative traversal, with the fixed stack the builder caps its depth to.
3
+ *
4
+ * The positions are uploaded in the hierarchy's leaf order, so a leaf is a run of triangles read straight off the
5
+ * buffer; the scene's own triangle number is looked up once, for the hit a trace returns. Every other reader goes the
6
+ * other way, from a triangle number to where its corners sit.
7
+ */
2
8
  export const BVH_WGSL = /* wgsl */ `
3
- fn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {
4
- let base = triangle * 9u + corner * 3u;
9
+ /** A corner of the triangle at this position of the hierarchy's leaf order. */
10
+ fn leafVertex(at: u32, corner: u32) -> vec3<f32> {
11
+ let base = at * 9u + corner * 3u;
5
12
 
6
13
  return vec3<f32>(positions[base], positions[base + 1u], positions[base + 2u]);
7
14
  }
8
15
 
16
+ /** Where a triangle of the scene sits in the leaf order the positions are stored in. */
17
+ fn leafOf(triangle: u32) -> u32 {
18
+ return indices[frame.leafOffset + triangle];
19
+ }
20
+
21
+ fn triangleVertex(triangle: u32, corner: u32) -> vec3<f32> {
22
+ return leafVertex(leafOf(triangle), corner);
23
+ }
24
+
9
25
  /** Moller-Trumbore. Returns the distance in x and the barycentric coordinates in yz, or a negative distance. */
10
- fn intersectTriangle(triangle: u32, origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
26
+ fn intersectTriangle(at: u32, origin: vec3<f32>, direction: vec3<f32>) -> vec3<f32> {
11
27
  let miss = vec3<f32>(-1.0, 0.0, 0.0);
12
- let a = triangleVertex(triangle, 0u);
13
- let edge1 = triangleVertex(triangle, 1u) - a;
14
- let edge2 = triangleVertex(triangle, 2u) - a;
28
+ let a = leafVertex(at, 0u);
29
+ let edge1 = leafVertex(at, 1u) - a;
30
+ let edge2 = leafVertex(at, 2u) - a;
15
31
  let p = cross(direction, edge2);
16
32
  let determinant = dot(edge1, p);
17
33
 
@@ -37,10 +53,10 @@ fn intersectTriangle(triangle: u32, origin: vec3<f32>, direction: vec3<f32>) ->
37
53
  return vec3<f32>(dot(edge2, q) * inverse, u, v);
38
54
  }
39
55
 
40
- /** Where a ray enters a node's box, or 1e30 when it misses it or only meets it past maxDistance. */
41
- fn enterBox(node: BvhNode, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f32) -> f32 {
42
- let first = (node.low - origin) * inverse;
43
- let second = (node.high - origin) * inverse;
56
+ /** Where a ray enters a box, or 1e30 when it misses it or only meets it past maxDistance. */
57
+ fn enterBox(low: vec3<f32>, high: vec3<f32>, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f32) -> f32 {
58
+ let first = (low - origin) * inverse;
59
+ let second = (high - origin) * inverse;
44
60
  let near = min(first, second);
45
61
  let far = max(first, second);
46
62
  let enter = max(max(near.x, near.y), max(near.z, 0.0));
@@ -49,27 +65,67 @@ fn enterBox(node: BvhNode, origin: vec3<f32>, inverse: vec3<f32>, maxDistance: f
49
65
  return select(1e30, enter, enter <= exit);
50
66
  }
51
67
 
52
- /** The triangles of a leaf, against the closest hit so far. */
53
- fn intersectLeaf(node: BvhNode, origin: vec3<f32>, direction: vec3<f32>, hit: ptr<function, Hit>) {
54
- let start = u32(-1 - node.link);
68
+ /** The triangles of a leaf, against the closest hit so far, which records where in the leaf order it found one. */
69
+ fn intersectLeaf(link: i32, count: i32, origin: vec3<f32>, direction: vec3<f32>, hit: ptr<function, Hit>) {
70
+ let start = u32(-1 - link);
55
71
 
56
- for (var i = 0u; i < u32(node.count); i = i + 1u) {
57
- let triangle = orderedTriangle(start + i);
58
- let result = intersectTriangle(triangle, origin, direction);
72
+ for (var i = 0u; i < u32(count); i = i + 1u) {
73
+ let result = intersectTriangle(start + i, origin, direction);
59
74
 
60
75
  if (result.x > RAY_EPSILON && result.x < (*hit).distance) {
61
76
  (*hit).distance = result.x;
62
- (*hit).triangle = triangle;
77
+ (*hit).triangle = start + i;
63
78
  (*hit).u = result.y;
64
79
  (*hit).v = result.z;
65
80
  }
66
81
  }
67
82
  }
68
83
 
84
+ /** Whether any triangle of a leaf lies along the ray before maxDistance: its position in the leaf order, or NO_HIT. */
85
+ fn anyInLeaf(link: i32, count: i32, origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
86
+ let start = u32(-1 - link);
87
+
88
+ for (var i = 0u; i < u32(count); i = i + 1u) {
89
+ let result = intersectTriangle(start + i, origin, direction);
90
+
91
+ if (result.x > RAY_EPSILON && result.x < maxDistance) {
92
+ return start + i;
93
+ }
94
+ }
95
+
96
+ return NO_HIT;
97
+ }
98
+
99
+ /** No node to go to: the traversal takes the next entry off the stack. */
100
+ const NO_NODE: u32 = 0xffffffffu;
101
+
102
+ /**
103
+ * A stack entry that is a leaf rather than a node: the node holding it and which of its two children it is. A leaf
104
+ * has no node of its own, so this is how one waits its turn.
105
+ */
106
+ const LEAF_ENTRY: u32 = 0x80000000u;
107
+
108
+ fn leafEntry(parent: u32, right: bool) -> u32 {
109
+ return LEAF_ENTRY | (parent << 1u) | select(0u, 1u, right);
110
+ }
111
+
112
+ /** A waiting leaf's link and count, read back from the node that holds it. */
113
+ fn waitingLeaf(entry: u32) -> vec2<i32> {
114
+ let parent = (entry & ~LEAF_ENTRY) >> 1u;
115
+
116
+ if ((entry & 1u) != 0u) {
117
+ return vec2<i32>(nodes[parent].rightLink, nodes[parent].rightCount);
118
+ }
119
+
120
+ return vec2<i32>(nodes[parent].leftLink, nodes[parent].leftCount);
121
+ }
122
+
69
123
  /**
70
- * The closest hit along a ray. Both children of a node are tested together and the nearer one is visited first, so
71
- * the closest hit shrinks the search early and whole subtrees behind it are skipped; each node remembers where the ray
72
- * entered it, and one whose entry is already past the closest hit is dropped without being opened.
124
+ * The closest hit along a ray. A node holds both of its children's boxes, so one read tests them both. The nearer is
125
+ * visited first, so the closest hit shrinks the search early and whole subtrees behind it are skipped; the farther one
126
+ * waits on the stack with where the ray entered it, and is dropped unopened if something closer turned up meanwhile.
127
+ * A leaf is tested at the top of the next step, the one place the loop tests triangles, which keeps the kernel small.
128
+ * This visits what the binary tree's traversal visited, in the same order, so it finds the same hit.
73
129
  */
74
130
  fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit {
75
131
  var hit: Hit;
@@ -85,74 +141,81 @@ fn traceScene(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> Hit
85
141
  // A huge finite reciprocal instead of an infinite one: an axis aligned slab would otherwise produce 0 * inf.
86
142
  let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
87
143
  let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
88
- let rootEnter = enterBox(nodes[0], origin, inverse, hit.distance);
89
-
90
- if (rootEnter >= 1e30) {
91
- return hit;
92
- }
93
144
 
94
145
  var stack: array<u32, 32>;
95
146
  var enters: array<f32, 32>;
96
- var depth = 1u;
97
- stack[0] = 0u;
98
- enters[0] = rootEnter;
147
+ var depth = 0u;
148
+ var current = 0u;
149
+ // A leaf to test before anything else: its link, and its triangle count, zero when there is none.
150
+ var leaf = vec2<i32>(0, 0);
99
151
 
100
152
  loop {
101
- if (depth == 0u) {
102
- break;
153
+ if (leaf.y > 0) {
154
+ intersectLeaf(leaf.x, leaf.y, origin, direction, &hit);
155
+ leaf.y = 0;
103
156
  }
104
157
 
105
- depth = depth - 1u;
158
+ if (current == NO_NODE) {
159
+ if (depth == 0u) {
160
+ break;
161
+ }
106
162
 
107
- // Something closer was found since this node was pushed.
108
- if (enters[depth] > hit.distance) {
109
- continue;
110
- }
163
+ depth = depth - 1u;
111
164
 
112
- let node = nodes[stack[depth]];
165
+ // Something closer was found since this was pushed.
166
+ if (enters[depth] > hit.distance) {
167
+ continue;
168
+ }
113
169
 
114
- if (node.link < 0) {
115
- intersectLeaf(node, origin, direction, &hit);
116
- continue;
117
- }
170
+ let entry = stack[depth];
118
171
 
119
- let leftIndex = u32(node.link);
120
- let rightIndex = u32(node.count);
121
- let leftEnter = enterBox(nodes[leftIndex], origin, inverse, hit.distance);
122
- let rightEnter = enterBox(nodes[rightIndex], origin, inverse, hit.distance);
123
- let leftHit = leftEnter < 1e30;
124
- let rightHit = rightEnter < 1e30;
172
+ if ((entry & LEAF_ENTRY) != 0u) {
173
+ leaf = waitingLeaf(entry);
174
+ continue;
175
+ }
125
176
 
126
- // The builder caps its depth so both children always fit; the guard keeps a malformed tree inside the stack.
127
- if (depth + 2u > 32u) {
128
- continue;
177
+ current = entry;
129
178
  }
130
179
 
131
- if (leftHit && rightHit) {
132
- // The farther first, so the nearer comes off the stack next.
133
- let leftNearer = leftEnter <= rightEnter;
134
- stack[depth] = select(leftIndex, rightIndex, leftNearer);
135
- enters[depth] = select(leftEnter, rightEnter, leftNearer);
136
- stack[depth + 1u] = select(rightIndex, leftIndex, leftNearer);
137
- enters[depth + 1u] = select(rightEnter, leftEnter, leftNearer);
138
- depth = depth + 2u;
139
- } else if (leftHit) {
140
- stack[depth] = leftIndex;
141
- enters[depth] = leftEnter;
142
- depth = depth + 1u;
143
- } else if (rightHit) {
144
- stack[depth] = rightIndex;
145
- enters[depth] = rightEnter;
180
+ let node = nodes[current];
181
+ let leftEnter = enterBox(node.leftLow, node.leftHigh, origin, inverse, hit.distance);
182
+ let rightEnter = enterBox(node.rightLow, node.rightHigh, origin, inverse, hit.distance);
183
+ let leftFirst = leftEnter <= rightEnter;
184
+ let nearLink = select(node.rightLink, node.leftLink, leftFirst);
185
+ let farEnter = select(leftEnter, rightEnter, leftFirst);
186
+ let farLink = select(node.leftLink, node.rightLink, leftFirst);
187
+ let parent = current;
188
+
189
+ current = NO_NODE;
190
+
191
+ // The nearer child: a miss means both missed.
192
+ if (min(leftEnter, rightEnter) < 1e30) {
193
+ if (nearLink < 0) {
194
+ leaf = vec2<i32>(nearLink, select(node.rightCount, node.leftCount, leftFirst));
195
+ } else {
196
+ current = u32(nearLink);
197
+ }
198
+ }
199
+
200
+ // The builder caps its depth so the far children always fit; the guard keeps a malformed tree inside the stack.
201
+ if (farEnter < 1e30 && depth < 32u) {
202
+ stack[depth] = select(u32(farLink), leafEntry(parent, leftFirst), farLink < 0);
203
+ enters[depth] = farEnter;
146
204
  depth = depth + 1u;
147
205
  }
148
206
  }
149
207
 
208
+ if (hit.triangle != NO_HIT) {
209
+ hit.triangle = orderedTriangle(hit.triangle);
210
+ }
211
+
150
212
  return hit;
151
213
  }
152
214
 
153
215
  /**
154
216
  * Whether anything at all lies along a ray before maxDistance, stopping at the first triangle it finds rather than the
155
- * closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT.
217
+ * closest: what a shadow ray asks when every surface it could meet blocks it. Returns that triangle, or NO_HIT. The
218
+ * right child goes first and the left one waits, the order the binary tree's traversal took.
156
219
  */
157
220
  fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
158
221
  if (frame.triangleCount == 0u) {
@@ -162,52 +225,60 @@ fn traceAny(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> u32 {
162
225
  let signs = select(vec3<f32>(-1.0), vec3<f32>(1.0), direction >= vec3<f32>(0.0));
163
226
  let inverse = signs / max(abs(direction), vec3<f32>(1e-20));
164
227
 
165
- if (enterBox(nodes[0], origin, inverse, maxDistance) >= 1e30) {
166
- return NO_HIT;
167
- }
168
-
169
228
  var stack: array<u32, 32>;
170
- var depth = 1u;
171
- stack[0] = 0u;
229
+ var depth = 0u;
230
+ var current = 0u;
231
+ var leaf = vec2<i32>(0, 0);
172
232
 
173
233
  loop {
174
- if (depth == 0u) {
175
- break;
176
- }
234
+ if (leaf.y > 0) {
235
+ let found = anyInLeaf(leaf.x, leaf.y, origin, direction, maxDistance);
177
236
 
178
- depth = depth - 1u;
237
+ if (found != NO_HIT) {
238
+ return orderedTriangle(found);
239
+ }
179
240
 
180
- let node = nodes[stack[depth]];
241
+ leaf.y = 0;
242
+ }
181
243
 
182
- if (node.link < 0) {
183
- let start = u32(-1 - node.link);
244
+ if (current == NO_NODE) {
245
+ if (depth == 0u) {
246
+ break;
247
+ }
248
+
249
+ depth = depth - 1u;
184
250
 
185
- for (var i = 0u; i < u32(node.count); i = i + 1u) {
186
- let triangle = orderedTriangle(start + i);
187
- let result = intersectTriangle(triangle, origin, direction);
251
+ let entry = stack[depth];
188
252
 
189
- if (result.x > RAY_EPSILON && result.x < maxDistance) {
190
- return triangle;
191
- }
253
+ if ((entry & LEAF_ENTRY) != 0u) {
254
+ leaf = waitingLeaf(entry);
255
+ continue;
192
256
  }
193
257
 
194
- continue;
258
+ current = entry;
195
259
  }
196
260
 
197
- let leftIndex = u32(node.link);
198
- let rightIndex = u32(node.count);
261
+ let node = nodes[current];
262
+ let rightHit = enterBox(node.rightLow, node.rightHigh, origin, inverse, maxDistance) < 1e30;
263
+ let leftHit = enterBox(node.leftLow, node.leftHigh, origin, inverse, maxDistance) < 1e30;
264
+ let parent = current;
199
265
 
200
- if (depth + 2u > 32u) {
201
- continue;
202
- }
266
+ current = NO_NODE;
203
267
 
204
- if (enterBox(nodes[leftIndex], origin, inverse, maxDistance) < 1e30) {
205
- stack[depth] = leftIndex;
206
- depth = depth + 1u;
268
+ // The right child is taken now and the left one waits; with only one of them hit, that one is taken now.
269
+ let takeRight = rightHit;
270
+ let takenLink = select(node.leftLink, node.rightLink, takeRight);
271
+
272
+ if (rightHit || leftHit) {
273
+ if (takenLink < 0) {
274
+ leaf = vec2<i32>(takenLink, select(node.leftCount, node.rightCount, takeRight));
275
+ } else {
276
+ current = u32(takenLink);
277
+ }
207
278
  }
208
279
 
209
- if (enterBox(nodes[rightIndex], origin, inverse, maxDistance) < 1e30) {
210
- stack[depth] = rightIndex;
280
+ if (rightHit && leftHit && depth < 32u) {
281
+ stack[depth] = select(u32(node.leftLink), leafEntry(parent, false), node.leftLink < 0);
211
282
  depth = depth + 1u;
212
283
  }
213
284
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bvh.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuNlC,CAAC"}
1
+ {"version":3,"file":"bvh.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/bvh.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwRlC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAGA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QA4LvB,CAAC"}
1
+ {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QA4MvB,CAAC"}
@@ -1,14 +1,20 @@
1
1
  import { FRAME_LAYOUT } from "../frameLayout.js";
2
2
  import { MATERIAL_KIND, MATERIAL_LAYOUT } from "../materialLayout.js";
3
+ import { SOBOL_TABLE, SRGB_TABLE, TEXTURE_TABLE } from "../texturePack.js";
3
4
  /** Bindings and structs shared by every stage of the path tracing module. */
4
5
  export const COMMON_WGSL = /* wgsl */ `
5
6
  ${FRAME_LAYOUT.wgsl}
6
- // Thirty two bytes: a corner and a link, twice. A leaf stores -1 - start in place of its left child.
7
+ // Sixty four bytes: the boxes of a node's two children, each a corner and a link, then a corner and a count. A link of
8
+ // zero or more is the child's own node; a leaf stores -1 - start in it, into the hierarchy's leaf order. See bvh.ts.
7
9
  struct BvhNode {
8
- low: vec3<f32>,
9
- link: i32,
10
- high: vec3<f32>,
11
- count: i32,
10
+ leftLow: vec3<f32>,
11
+ leftLink: i32,
12
+ leftHigh: vec3<f32>,
13
+ leftCount: i32,
14
+ rightLow: vec3<f32>,
15
+ rightLink: i32,
16
+ rightHigh: vec3<f32>,
17
+ rightCount: i32,
12
18
  };
13
19
 
14
20
  /** As materialLayout.ts packs it. A map index of NO_TEXTURE means the slot is empty. */
@@ -92,6 +98,10 @@ const MATERIAL_OBJECT_NORMAL_MAP: u32 = 256u;
92
98
  const MATERIAL_PATCHED: u32 = 512u;
93
99
  const MATERIAL_ALPHA_HASH: u32 = 2048u;
94
100
  const MATERIAL_SPECULAR: u32 = 4096u;
101
+ // The mesh only bounds a medium: its surface is invisible and the space inside scatters light.
102
+ const MATERIAL_VOLUME: u32 = 8192u;
103
+ // An unlit material of a fixed colour, which direct sampling aims at: it emits what it shows.
104
+ const MATERIAL_UNLIT_LIGHT: u32 = 16384u;
95
105
 
96
106
  const KIND_STANDARD: u32 = ${MATERIAL_KIND.standard}u;
97
107
  const KIND_PHYSICAL: u32 = ${MATERIAL_KIND.physical}u;
@@ -118,14 +128,21 @@ const BLEND_MULTIPLY: u32 = 4u;
118
128
  const NO_TEXTURE: u32 = 0xffffffffu;
119
129
  const NO_PROGRAM: u32 = 0xffffffffu;
120
130
 
131
+ // The lookup tables that lead the texture buffer, and where its table of textures starts after them: see texturePack.ts.
132
+ const SOBOL_TABLE: u32 = ${SOBOL_TABLE}u;
133
+ const SRGB_TABLE: u32 = ${SRGB_TABLE}u;
134
+ const TEXTURE_TABLE: u32 = ${TEXTURE_TABLE}u;
135
+
121
136
  // A WebGPU device only has to offer eight storage buffers per stage, so arrays of the same kind share one binding and
122
137
  // the uniform says where the second one starts. See sceneUpload.ts, which joins them.
123
138
  @group(0) @binding(0) var<uniform> frame: Frame;
139
+ // Nine coordinates per triangle, in the hierarchy's leaf order: see leafOf in bvh.wgsl.ts.
124
140
  @group(0) @binding(1) var<storage, read> positions: array<f32>;
125
141
  // Normals, then the UV sets from frame.uvOffset, vertex colours from frame.colorOffset and shader varyings from
126
142
  // frame.varyingOffset.
127
143
  @group(0) @binding(2) var<storage, read> attributes: array<f32>;
128
- // One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on.
144
+ // One material index per triangle, then the hierarchy's triangle order from frame.orderOffset on (the triangle at each
145
+ // position of the leaf order), then from frame.leafOffset on the other way (the position of each triangle).
129
146
  @group(0) @binding(3) var<storage, read> indices: array<u32>;
130
147
  @group(0) @binding(4) var<storage, read> materials: array<Material>;
131
148
  @group(0) @binding(5) var<storage, read> nodes: array<BvhNode>;
@@ -173,7 +190,7 @@ fn extraVec4(at: u32) -> vec4<f32> {
173
190
 
174
191
  /** One entry of the texture table, unpacked from the words it shares with the texels. */
175
192
  fn textureInfo(index: u32) -> TextureInfo {
176
- let at = index * 16u;
193
+ let at = TEXTURE_TABLE + index * 16u;
177
194
  var info: TextureInfo;
178
195
 
179
196
  info.size = vec2<u32>(textures[at], textures[at + 1u]);
@@ -1 +1 @@
1
- {"version":3,"file":"common.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEtE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;EACpC,YAAY,CAAC,IAAI;;;;;;;;;;EAUjB,eAAe,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAiFO,aAAa,CAAC,QAAQ;6BACtB,aAAa,CAAC,QAAQ;4BACvB,aAAa,CAAC,OAAO;0BACvB,aAAa,CAAC,KAAK;yBACpB,aAAa,CAAC,IAAI;0BACjB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;0BACrB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsF9C,CAAC"}
1
+ {"version":3,"file":"common.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE3E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;EACpC,YAAY,CAAC,IAAI;;;;;;;;;;;;;;;EAejB,eAAe,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAqFO,aAAa,CAAC,QAAQ;6BACtB,aAAa,CAAC,QAAQ;4BACvB,aAAa,CAAC,OAAO;0BACvB,aAAa,CAAC,KAAK;yBACpB,aAAa,CAAC,IAAI;0BACjB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;0BACrB,aAAa,CAAC,KAAK;2BAClB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;2BACpB,aAAa,CAAC,MAAM;;;;;;;;;;;;;;;;2BAgBpB,WAAW;0BACZ,UAAU;6BACP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2EzC,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What the lens and the sensor do to the light before it is exposed: a halo around bright light (bloom), colour
3
+ * fringes towards the corners (lateral chromatic aberration) and the camera's white balance. Everything here is linear
4
+ * light, between the traced (or denoised) radiance and the presentation pass, which exposes and tone maps the result.
5
+ *
6
+ * The halo is built over a pyramid of half sized copies of the image, as Jimenez's "Next generation post processing in
7
+ * Call of Duty" (2014) builds it: each level is a 13 tap filter of the one above, and the levels are folded back from
8
+ * the smallest up with a tent filter, each mixed with the one below by the radius. The result is a wide, smooth spread
9
+ * whose weights sum to one, so a flat image is left as it was and the halo takes light from where it is rather than
10
+ * adding any: the image keeps its exact mean.
11
+ *
12
+ * The rays a bright light throws through the aperture are gathered the same way, from a quarter sized level: each
13
+ * texel takes the light of the texels behind it along every ray's direction, fading with the distance, with weights
14
+ * that again sum to one.
15
+ */
16
+ export declare const LENS_WGSL = "\nstruct Lens {\n resolution: vec2<u32>,\n // Whether the colour comes from the denoiser's output rather than straight from the accumulation.\n denoised: u32,\n // Upsampling only: the level read from sits in the downsampled pyramid (the smallest one) instead of the folded one.\n fromDown: u32,\n sourceSize: vec2<u32>,\n // Where the level read from starts in its pyramid; a size of the full resolution reads the image itself.\n sourceOffset: u32,\n // The first downsample reads the image itself, and averages its taps by luminance against fireflies.\n fromImage: u32,\n targetSize: vec2<u32>,\n targetOffset: u32,\n // Where the largest level of the folded pyramid starts, which the composite reads the halo from.\n haloOffset: u32,\n haloSize: vec2<u32>,\n // How much of the light the halo takes, and how much each level takes from the one below it.\n intensity: f32,\n radius: f32,\n // How far red and blue move apart at the corners, as a share of the distance from the centre.\n aberration: f32,\n // How much of the light the aperture's rays take, how many there are, and how far they reach in streak texels.\n glare: f32,\n rays: u32,\n glareLength: f32,\n // The white balance, a linear sRGB matrix, row by row.\n white0: vec4<f32>,\n white1: vec4<f32>,\n white2: vec4<f32>,\n // The size of the level the rays are gathered at, and the angle of the first ray, in radians.\n streakSize: vec2<u32>,\n glareAngle: f32,\n pad0: f32,\n};\n\nconst PI: f32 = 3.14159265358979;\n\n/** Taps along each of the aperture's rays. */\nconst STREAK_TAPS: i32 = 24;\n\n@group(0) @binding(0) var<uniform> lens: Lens;\n@group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;\n@group(0) @binding(3) var<storage, read_write> down: array<vec4<f32>>;\n@group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;\n@group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;\n@group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;\n\n/** The image's own texel: its mean so far, or nothing where no path has landed yet. */\nfn imageTexel(x: i32, y: i32) -> vec3<f32> {\n let size = vec2<i32>(lens.resolution);\n let index = u32(clamp(y, 0, size.y - 1) * size.x + clamp(x, 0, size.x - 1));\n\n if (lens.denoised != 0u) {\n return denoised[index].rgb;\n }\n\n let stored = accumulation[index];\n\n return select(stored.rgb / stored.w, vec3<f32>(0.0), stored.w <= 0.0);\n}\n\nfn downTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(size);\n\n return down[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\nfn upTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(size);\n\n return up[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\n/** Bilinear weights and the texel below and left of a point given in texels, edges clamped. */\nfn bilinearAt(position: vec2<f32>) -> vec4<f32> {\n let p = position - 0.5;\n let base = floor(p);\n\n return vec4<f32>(base, p - base);\n}\n\nfn imageBilinear(position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(imageTexel(x, y), imageTexel(x + 1, y), b.z),\n mix(imageTexel(x, y + 1), imageTexel(x + 1, y + 1), b.z), b.w);\n}\n\nfn downBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(downTexel(offset, size, x, y), downTexel(offset, size, x + 1, y), b.z),\n mix(downTexel(offset, size, x, y + 1), downTexel(offset, size, x + 1, y + 1), b.z), b.w);\n}\n\nfn upBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(\n mix(upTexel(offset, size, x, y), upTexel(offset, size, x + 1, y), b.z),\n mix(upTexel(offset, size, x, y + 1), upTexel(offset, size, x + 1, y + 1), b.z), b.w);\n}\n\n/** The level being filtered, read bilinearly at a point given in its own texels. */\nfn sourceAt(position: vec2<f32>) -> vec3<f32> {\n if (lens.fromImage != 0u) {\n return imageBilinear(position);\n }\n\n return downBilinear(lens.sourceOffset, lens.sourceSize, position);\n}\n\nfn luminance(color: vec3<f32>) -> f32 {\n return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\n/** Karis' average: a group of taps weighed by one over one plus its luminance, so a lone firefly cannot flare. */\nfn karis(sum: vec3<f32>) -> vec4<f32> {\n let weight = 1.0 / (1.0 + luminance(sum));\n\n return vec4<f32>(sum * weight, weight);\n}\n\n/** One level of the pyramid from the one above it: Jimenez's 13 taps, four overlapping boxes and a centre one. */\n@compute @workgroup_size(8, 8, 1)\nfn downsample(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);\n let centre = (vec2<f32>(id.xy) + 0.5) * scale;\n let a = sourceAt(centre + vec2<f32>(-2.0, -2.0));\n let b = sourceAt(centre + vec2<f32>(0.0, -2.0));\n let c = sourceAt(centre + vec2<f32>(2.0, -2.0));\n let d = sourceAt(centre + vec2<f32>(-2.0, 0.0));\n let e = sourceAt(centre);\n let f = sourceAt(centre + vec2<f32>(2.0, 0.0));\n let g = sourceAt(centre + vec2<f32>(-2.0, 2.0));\n let h = sourceAt(centre + vec2<f32>(0.0, 2.0));\n let i = sourceAt(centre + vec2<f32>(2.0, 2.0));\n let j = sourceAt(centre + vec2<f32>(-1.0, -1.0));\n let k = sourceAt(centre + vec2<f32>(1.0, -1.0));\n let l = sourceAt(centre + vec2<f32>(-1.0, 1.0));\n let m = sourceAt(centre + vec2<f32>(1.0, 1.0));\n var result: vec3<f32>;\n\n if (lens.fromImage != 0u) {\n let groups = karis((j + k + l + m) * 0.25) * 0.5\n + karis((a + b + d + e) * 0.25) * 0.125\n + karis((b + c + e + f) * 0.25) * 0.125\n + karis((d + e + g + h) * 0.25) * 0.125\n + karis((e + f + h + i) * 0.25) * 0.125;\n\n result = groups.rgb / max(groups.w, 1e-6);\n } else {\n result = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;\n }\n\n down[lens.targetOffset + id.y * lens.targetSize.x + id.x] = vec4<f32>(max(result, vec3<f32>(0.0)), 1.0);\n}\n\n/** A 3 x 3 tent over the smaller level, read at a point given in its texels. */\nfn tent(position: vec2<f32>) -> vec3<f32> {\n var sum = vec3<f32>(0.0);\n\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;\n let at = position + vec2<f32>(f32(dx), f32(dy));\n\n if (lens.fromDown != 0u) {\n sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;\n } else {\n sum = sum + upBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;\n }\n }\n }\n\n return sum;\n}\n\n/** One level folded back: its own light, mixed by the radius with the wider spread of every level below it. */\n@compute @workgroup_size(8, 8, 1)\nfn upsample(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);\n let wider = tent((vec2<f32>(id.xy) + 0.5) * scale);\n let index = lens.targetOffset + id.y * lens.targetSize.x + id.x;\n\n up[index] = vec4<f32>(mix(down[index].rgb, wider, lens.radius), 1.0);\n}\n\n/** The aperture's rays at one texel of the streak level: the light behind it along each ray, fading with distance. */\n@compute @workgroup_size(8, 8, 1)\nfn streaks(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {\n return;\n }\n\n let centre = vec2<f32>(id.xy) + 0.5;\n var sum = vec3<f32>(0.0);\n var total = 0.0;\n\n for (var r = 0u; r < lens.rays; r = r + 1u) {\n let angle = lens.glareAngle + f32(r) * 2.0 * PI / f32(lens.rays);\n let along = vec2<f32>(cos(angle), sin(angle));\n\n for (var j = 1; j <= STREAK_TAPS; j = j + 1) {\n let t = f32(j) / f32(STREAK_TAPS);\n let weight = exp(-4.0 * t);\n\n sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength)) * weight;\n total = total + weight;\n }\n }\n\n streaked[id.y * lens.targetSize.x + id.x] = vec4<f32>(sum / max(total, 1e-6), 1.0);\n}\n\nfn streakTexel(x: i32, y: i32) -> vec3<f32> {\n let s = vec2<i32>(lens.streakSize);\n\n return streaked[u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;\n}\n\nfn streakBilinear(position: vec2<f32>) -> vec3<f32> {\n let b = bilinearAt(position);\n let x = i32(b.x);\n let y = i32(b.y);\n\n return mix(mix(streakTexel(x, y), streakTexel(x + 1, y), b.z), mix(streakTexel(x, y + 1), streakTexel(x + 1, y + 1), b.z), b.w);\n}\n\n/** The folded halo at full resolution: a tent over its largest level, which is half the image's size. */\nfn tentHalo(position: vec2<f32>) -> vec3<f32> {\n var sum = vec3<f32>(0.0);\n\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;\n\n sum = sum + upBilinear(lens.haloOffset, lens.haloSize, position + vec2<f32>(f32(dx), f32(dy)) * 0.5) * weight;\n }\n }\n\n return sum;\n}\n\n/** The image as the lens delivers it: fringed towards the corners, with its halo, balanced for the camera's white. */\n@compute @workgroup_size(8, 8, 1)\nfn composite(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= lens.resolution.x || id.y >= lens.resolution.y) {\n return;\n }\n\n let size = vec2<f32>(lens.resolution);\n let position = vec2<f32>(id.xy) + 0.5;\n var color = imageTexel(i32(id.x), i32(id.y));\n\n // Lateral chromatic aberration: each colour is magnified a little differently, red outwards and blue inwards,\n // in proportion to the distance from the centre, which is how a lens's dispersion moves them.\n if (lens.aberration > 0.0) {\n let centre = size * 0.5;\n let away = position - centre;\n let red = imageBilinear(centre + away * (1.0 - lens.aberration));\n let blue = imageBilinear(centre + away * (1.0 + lens.aberration));\n\n color = vec3<f32>(red.r, color.g, blue.b);\n }\n\n if (lens.intensity > 0.0) {\n let halo = tentHalo(position * vec2<f32>(lens.haloSize) / size);\n\n color = mix(color, halo, lens.intensity);\n }\n\n if (lens.glare > 0.0) {\n color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);\n }\n\n let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));\n\n graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);\n}\n";
17
+ //# sourceMappingURL=lens.wgsl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lens.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lens.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,suVA0SrB,CAAC"}