@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
@@ -0,0 +1,70 @@
1
+ const DEFAULT_RADIUS = [1, 0.2, 0.1];
2
+ const DEFAULT_SCALE = 0.05;
3
+ const finite = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
4
+ /** The medium an input describes, with its defaults, or `null` when it describes none. */
5
+ export function subsurfaceMedium(input) {
6
+ if (input === true)
7
+ input = {};
8
+ if (!input || typeof input !== "object")
9
+ return null;
10
+ const value = input;
11
+ const weight = Math.min(1, Math.max(0, finite(value.weight, 1)));
12
+ if (weight <= 0)
13
+ return null;
14
+ const scale = Math.max(0, finite(value.scale, DEFAULT_SCALE));
15
+ const radius = typeof value.radius === "number"
16
+ ? [value.radius, value.radius, value.radius]
17
+ : Array.isArray(value.radius) && value.radius.length >= 3
18
+ ? value.radius.slice(0, 3)
19
+ : DEFAULT_RADIUS;
20
+ // A path that never travels would divide by zero; a hair of distance keeps it a very dense medium instead.
21
+ const paths = radius.map((channel, k) => Math.max(1e-5, finite(channel, DEFAULT_RADIUS[k]) * scale));
22
+ return {
23
+ weight,
24
+ radius: [paths[0], paths[1], paths[2]],
25
+ anisotropy: Math.min(0.95, Math.max(-0.95, finite(value.anisotropy, 0))),
26
+ };
27
+ }
28
+ /** The medium a snapshot material carries, if any. */
29
+ export function subsurfaceOf(material) {
30
+ return subsurfaceMedium(material?.subsurface);
31
+ }
32
+ const srgbToLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
33
+ function albedoOf(color) {
34
+ if (Array.isArray(color) && color.length >= 3) {
35
+ return [0, 1, 2].map(k => Math.min(1, Math.max(0, finite(color[k], 1))));
36
+ }
37
+ const match = typeof color === "string" ? /^#?([0-9a-f]{6})$/i.exec(color) : null;
38
+ if (!match)
39
+ return [1, 1, 1];
40
+ const value = parseInt(match[1], 16);
41
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255].map(c => srgbToLinear(c / 255));
42
+ }
43
+ /** The medium an input describes, with its defaults, or `null` when it describes none. */
44
+ export function volumeMedium(input) {
45
+ if (input === true)
46
+ input = {};
47
+ if (!input || typeof input !== "object")
48
+ return null;
49
+ const value = input;
50
+ const density = Math.max(0, finite(value.density, 1));
51
+ if (density <= 0)
52
+ return null;
53
+ return {
54
+ density,
55
+ albedo: albedoOf(value.color),
56
+ anisotropy: Math.min(0.95, Math.max(-0.95, finite(value.anisotropy, 0))),
57
+ };
58
+ }
59
+ /** The bevel radius an input asks for, or zero. */
60
+ export function bevelRadius(input) {
61
+ const radius = typeof input === "number" ? input : input?.radius;
62
+ return typeof radius === "number" && Number.isFinite(radius) && radius > 0 ? radius : 0;
63
+ }
64
+ export function bevelOf(material) {
65
+ return bevelRadius(material?.bevel);
66
+ }
67
+ export function volumeOf(material) {
68
+ return volumeMedium(material?.volume);
69
+ }
70
+ //# sourceMappingURL=subsurface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subsurface.js","sourceRoot":"","sources":["../src/subsurface.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { PackedGeometry } from "./geometry.js";
2
+ /** What the kernel reads as no material at all. */
3
+ export declare const NO_MATERIAL = 4294967295;
4
+ /** The triangles that bound a volume, each with its material's index. */
5
+ export interface VolumeTriangles {
6
+ /** Nine floats a triangle. */
7
+ positions: Float32Array;
8
+ materials: Uint32Array;
9
+ }
10
+ /** The boundaries of every volume in a packed scene, or `null` when it has none. */
11
+ export declare function volumeTriangles(geometry: PackedGeometry): VolumeTriangles | null;
12
+ /**
13
+ * The volume a point stands inside, by the parity of the boundary crossings of a ray from it: an odd count of one
14
+ * volume's triangles means inside it. The camera usually stands in none, so the kernel only asks this of the camera.
15
+ */
16
+ export declare function containingVolume(volumes: VolumeTriangles | null, point: readonly number[]): number;
17
+ //# sourceMappingURL=volumeContainment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volumeContainment.d.ts","sourceRoot":"","sources":["../src/volumeContainment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,mDAAmD;AACnD,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,SAAS,EAAE,YAAY,CAAC;IACxB,SAAS,EAAE,WAAW,CAAC;CACxB;AAED,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,eAAe,GAAG,IAAI,CAmBhF;AAKD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAsClG"}
@@ -0,0 +1,61 @@
1
+ import { MATERIAL_VOLUME } from "./materialLayout.js";
2
+ /** What the kernel reads as no material at all. */
3
+ export const NO_MATERIAL = 0xffffffff;
4
+ /** The boundaries of every volume in a packed scene, or `null` when it has none. */
5
+ export function volumeTriangles(geometry) {
6
+ const volume = geometry.materialProperties.map(properties => (properties.flags & MATERIAL_VOLUME) !== 0);
7
+ if (!volume.some(Boolean))
8
+ return null;
9
+ const picked = [];
10
+ for (let triangle = 0; triangle < geometry.triangleCount; triangle++)
11
+ if (volume[geometry.materials[triangle]])
12
+ picked.push(triangle);
13
+ const positions = new Float32Array(picked.length * 9);
14
+ const materials = new Uint32Array(picked.length);
15
+ picked.forEach((triangle, k) => {
16
+ positions.set(geometry.positions.subarray(triangle * 9, triangle * 9 + 9), k * 9);
17
+ materials[k] = geometry.materials[triangle];
18
+ });
19
+ return { positions, materials };
20
+ }
21
+ /** An irrational direction, so a ray from the camera does not run along an edge of an axis aligned box. */
22
+ const PROBE = [0.4123, 0.7071, 0.5745].map((value, _, all) => value / Math.hypot(...all));
23
+ /**
24
+ * The volume a point stands inside, by the parity of the boundary crossings of a ray from it: an odd count of one
25
+ * volume's triangles means inside it. The camera usually stands in none, so the kernel only asks this of the camera.
26
+ */
27
+ export function containingVolume(volumes, point) {
28
+ if (!volumes)
29
+ return NO_MATERIAL;
30
+ const crossings = new Map();
31
+ const p = volumes.positions;
32
+ const [dx, dy, dz] = PROBE;
33
+ for (let k = 0; k < volumes.materials.length; k++) {
34
+ const at = k * 9;
35
+ const e1x = p[at + 3] - p[at], e1y = p[at + 4] - p[at + 1], e1z = p[at + 5] - p[at + 2];
36
+ const e2x = p[at + 6] - p[at], e2y = p[at + 7] - p[at + 1], e2z = p[at + 8] - p[at + 2];
37
+ // Moller and Trumbore.
38
+ const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
39
+ const det = e1x * hx + e1y * hy + e1z * hz;
40
+ if (Math.abs(det) < 1e-12)
41
+ continue;
42
+ const inverse = 1 / det;
43
+ const sx = point[0] - p[at], sy = point[1] - p[at + 1], sz = point[2] - p[at + 2];
44
+ const u = (sx * hx + sy * hy + sz * hz) * inverse;
45
+ if (u < 0 || u > 1)
46
+ continue;
47
+ const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
48
+ const v = (dx * qx + dy * qy + dz * qz) * inverse;
49
+ if (v < 0 || u + v > 1)
50
+ continue;
51
+ if ((e2x * qx + e2y * qy + e2z * qz) * inverse > 0) {
52
+ const material = volumes.materials[k];
53
+ crossings.set(material, (crossings.get(material) ?? 0) + 1);
54
+ }
55
+ }
56
+ for (const [material, count] of crossings)
57
+ if (count % 2 === 1)
58
+ return material;
59
+ return NO_MATERIAL;
60
+ }
61
+ //# sourceMappingURL=volumeContainment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"volumeContainment.js","sourceRoot":"","sources":["../src/volumeContainment.ts"],"names":[],"mappings":"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"}
@@ -9,5 +9,5 @@
9
9
  * for the diffuse lobe, the visible normal distribution (Heitz 2018) for GGX, anisotropic or not, and the Blinn-Phong
10
10
  * lobe for Phong, mixed by how much each lobe reflects.
11
11
  */
12
- export declare const BSDF_WGSL = "\n/** How a surface answers a direction: what it reflects, and how likely sampling was to have chosen it. */\nstruct BsdfEval {\n // The bidirectional distribution times the cosine at the surface, which is what a contribution is multiplied by.\n value: vec3<f32>,\n // Solid angle density of this direction under this renderer's own sampling, for the power heuristic.\n pdf: f32,\n};\n\nstruct BsdfSample {\n direction: vec3<f32>,\n // The distribution over its own density: what the path's throughput is multiplied by.\n weight: vec3<f32>,\n pdf: f32,\n valid: bool,\n // The diffuse lobe chose the direction, rather than a specular one: which of the denoiser's two signals it feeds.\n diffuse: bool,\n};\n\nfn luminanceOf(color: vec3<f32>) -> f32 {\n return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\nfn max3(color: vec3<f32>) -> f32 {\n return max(color.r, max(color.g, color.b));\n}\n\n/** Schlick's approximation, as in Three: the reflectance always reaches one at a grazing angle. */\nfn fresnelSchlick(f0: vec3<f32>, cosine: f32) -> vec3<f32> {\n return schlick3(f0, 1.0, cosine);\n}\n\n/** Reflectance at normal incidence: the dielectric's own, turned towards the base colour as the surface turns metal. */\nfn surfaceF0(surface: Surface) -> vec3<f32> {\n return mix(surface.specularColor, surface.albedo, surface.metalness);\n}\n\n/** GGX, with alpha the square of the roughness, as Three parameterises it. */\nfn ggxDistribution(alpha2: f32, nh: f32) -> f32 {\n let denominator = nh * nh * (alpha2 - 1.0) + 1.0;\n\n return alpha2 / (PI * denominator * denominator);\n}\n\n/** Smith's height correlated visibility, which already carries the 1/(4 nl nv) the lobe would otherwise need. */\nfn ggxVisibility(alpha2: f32, nl: f32, nv: f32) -> f32 {\n return 0.5 / (nl * sqrt(nv * nv * (1.0 - alpha2) + alpha2) + nv * sqrt(nl * nl * (1.0 - alpha2) + alpha2) + 1e-7);\n}\n\n/** The separable masking term, which is the one the visible normal sampling is built on. */\nfn ggxMasking(alpha2: f32, nv: f32) -> f32 {\n return 2.0 * nv / (nv + sqrt(alpha2 + (1.0 - alpha2) * nv * nv));\n}\n\n/** Karis' analytic fit of the GGX directional albedo (Three's DFGApprox): the scale and bias applied to F0 and F90. */\nfn dfgApprox(roughness: f32, nv: f32) -> vec2<f32> {\n let r = roughness * vec4<f32>(-1.0, -0.0275, -0.572, 0.022) + vec4<f32>(1.0, 0.0425, 1.04, -0.04);\n let a004 = min(r.x * r.x, exp2(-9.28 * nv)) * r.x + r.y;\n\n return vec2<f32>(-1.04, 1.04) * a004 + r.zw;\n}\n\n/** Three's IBLSheenBRDF: the Charlie sheen lobe integrated over the hemisphere. */\nfn sheenAlbedo(cosine: f32, roughness: f32) -> f32 {\n let r2 = roughness * roughness;\n let inverse = 1.0 / (roughness + 0.1);\n let a = -1.9362 + 1.0678 * roughness + 0.4573 * r2 - 0.8469 * inverse;\n let b = -0.6014 + 0.5538 * roughness - 0.4670 * r2 - 0.1255 * inverse;\n\n return clamp(exp(a * clamp(cosine, 0.0, 1.0) + b), 0.0, 1.0);\n}\n\n/** Three's D_Charlie times V_Neubelt. */\nfn sheenLobe(roughness: f32, nh: f32, nv: f32, nl: f32) -> f32 {\n let inverse = 1.0 / (roughness * roughness);\n let sin2h = max(1.0 - nh * nh, 0.0078125);\n let distribution = (2.0 + inverse) * pow(sin2h, inverse * 0.5) / (2.0 * PI);\n\n return distribution * clamp(1.0 / (4.0 * (nl + nv - nl * nv)), 0.0, 1.0);\n}\n\n/** The frame the specular lobe is defined in: Three's anisotropy direction when there is one, any frame otherwise. */\nfn specularBasis(surface: Surface) -> mat3x3<f32> {\n let normal = surface.normal;\n\n if (surface.anisotropy > 0.0) {\n let t = surface.anisotropyT - normal * dot(normal, surface.anisotropyT);\n let tangent = normalize(t);\n\n if (all(tangent == tangent) && dot(t, t) > 1e-12) {\n return mat3x3<f32>(tangent, cross(normal, tangent), normal);\n }\n }\n\n return orthonormalBasis(normal);\n}\n\n/** The GGX roughness along the frame's two axes: Three's alphaT along the anisotropy, alpha across it. */\nfn specularAlphas(surface: Surface) -> vec2<f32> {\n let alpha = surface.roughness * surface.roughness;\n\n if (surface.anisotropy > 0.0) {\n return vec2<f32>(max(surface.alphaT, 1e-4), max(alpha, 1e-4));\n }\n\n return vec2<f32>(max(alpha, 1e-4));\n}\n\n/** Anisotropic GGX in the lobe's own frame; with equal alphas it is exactly the isotropic distribution. */\nfn ggxAnisotropic(h: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let x = h.x / alphas.x;\n let y = h.y / alphas.y;\n let d = x * x + y * y + h.z * h.z;\n\n return 1.0 / (PI * alphas.x * alphas.y * d * d);\n}\n\n/** Smith's masking of one direction, in the lobe's frame. */\nfn ggxMaskingAnisotropic(v: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let projected = alphas.x * alphas.x * v.x * v.x + alphas.y * alphas.y * v.y * v.y;\n\n return 2.0 * v.z / (v.z + sqrt(v.z * v.z + projected));\n}\n\n/** Three's V_GGX_SmithCorrelated_Anisotropic, in the lobe's frame. */\nfn ggxVisibilityAnisotropic(l: vec3<f32>, v: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let gv = l.z * length(vec3<f32>(alphas.x * v.x, alphas.y * v.y, v.z));\n let gl = v.z * length(vec3<f32>(alphas.x * l.x, alphas.y * l.y, l.z));\n\n return 0.5 / max(gv + gl, 1e-7);\n}\n\n/**\n * A half vector drawn from the distribution of normals the view direction can actually see (Heitz 2018), which is what\n * keeps the specular lobe from wasting samples on microfacets facing away. The view is in the lobe's frame.\n */\nfn sampleVisibleNormal(view: vec3<f32>, alphas: vec2<f32>, u: vec2<f32>) -> vec3<f32> {\n let stretched = normalize(vec3<f32>(alphas.x * view.x, alphas.y * view.y, view.z));\n let lengthSquared = stretched.x * stretched.x + stretched.y * stretched.y;\n let t1 = select(\n vec3<f32>(1.0, 0.0, 0.0),\n vec3<f32>(-stretched.y, stretched.x, 0.0) * inverseSqrt(max(lengthSquared, 1e-20)),\n lengthSquared > 1e-20);\n let t2 = cross(stretched, t1);\n let radius = sqrt(u.x);\n let angle = 2.0 * PI * u.y;\n let px = radius * cos(angle);\n let raw = radius * sin(angle);\n let lift = 0.5 * (1.0 + stretched.z);\n let py = (1.0 - lift) * sqrt(max(0.0, 1.0 - px * px)) + lift * raw;\n let normal = t1 * px + t2 * py + stretched * sqrt(max(0.0, 1.0 - px * px - py * py));\n\n return normalize(vec3<f32>(alphas.x * normal.x, alphas.y * normal.y, max(normal.z, 1e-6)));\n}\n\n/** What the clearcoat takes back from everything under it, seen from this view: Three's (1 - clearcoat * Fcc). */\nfn coatTransmittance(surface: Surface, view: vec3<f32>) -> f32 {\n if (surface.clearcoat <= 0.0) {\n return 1.0;\n }\n\n let nv = clamp(dot(surface.clearcoatNormal, view), 0.0, 1.0);\n\n return 1.0 - surface.clearcoat * schlick3(vec3<f32>(0.04), 1.0, nv).x;\n}\n\n/** How often each lobe is chosen, as (diffuse, specular, clearcoat): by how much of the reflection each accounts for. */\nfn lobeWeights(surface: Surface, view: vec3<f32>) -> vec3<f32> {\n let nv = max(dot(surface.normal, view), 1e-4);\n\n if (surface.kind == KIND_PHONG) {\n let diffuse = luminanceOf(surface.albedo) + 0.02;\n let specular = luminanceOf(surface.specularColor) * surface.specularStrength * (surface.shininess * 0.125 + 1.0) + 0.02;\n\n return vec3<f32>(diffuse, specular, 0.0) / (diffuse + specular);\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return vec3<f32>(1.0, 0.0, 0.0);\n }\n\n let base = coatTransmittance(surface, view);\n // The floors keep every lobe reachable: the diffuse one also carries the sheen and the multiple scattering term.\n let diffuse = (luminanceOf(surface.albedo * (1.0 - surface.metalness)) + max3(surface.sheenColor)) * base + 0.02;\n let specular = luminanceOf(schlick3(surface.specularColor, surface.specularF90, nv)\n * (1.0 - surface.metalness) + surface.albedo * surface.metalness) * base + 0.05;\n let coat = select(0.0, (1.0 - base) + 0.02, surface.clearcoat > 0.0);\n let total = diffuse + specular + coat;\n\n return vec3<f32>(diffuse, specular, coat) / total;\n}\n\n/** Three's physical direct lighting for one light direction, times the cosine. */\nfn physicalValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32> {\n let normal = surface.normal;\n let nl = dot(normal, light);\n let nv = max(dot(normal, view), 1e-4);\n var value = vec3<f32>(0.0);\n\n if (nl > 0.0) {\n let basis = specularBasis(surface);\n let alphas = specularAlphas(surface);\n let half = normalize(light + view);\n let vh = clamp(dot(view, half), 0.0, 1.0);\n let f0 = surfaceF0(surface);\n var fresnel = schlick3(f0, surface.specularF90, vh);\n\n if (surface.iridescence > 0.0) {\n fresnel = mix(fresnel, surface.iridescenceFresnel, surface.iridescence);\n }\n\n let localLight = vec3<f32>(dot(light, basis[0]), dot(light, basis[1]), nl);\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), nv);\n let localHalf = vec3<f32>(dot(half, basis[0]), dot(half, basis[1]), max(dot(half, normal), 0.0));\n let single = fresnel * (ggxVisibilityAnisotropic(localLight, localView, alphas) * ggxAnisotropic(localHalf, alphas));\n\n // Three's BRDF_GGX_Multiscatter: the energy single scattering loses at high roughness, returned.\n let dfgV = dfgApprox(surface.roughness, nv);\n let dfgL = dfgApprox(surface.roughness, nl);\n let essV = f0 * dfgV.x + surface.specularF90 * dfgV.y;\n let essL = f0 * dfgL.x + surface.specularF90 * dfgL.y;\n let emsV = 1.0 - (dfgV.x + dfgV.y);\n let emsL = 1.0 - (dfgL.x + dfgL.y);\n let average = f0 + (vec3<f32>(1.0) - f0) * 0.047619;\n let multiple = essV * essL * average / (vec3<f32>(1.0) - emsV * emsL * average + 1e-7) * (emsV * emsL);\n\n var diffuse = surface.albedo * (1.0 - surface.metalness) / PI;\n var specular = single + multiple;\n\n // Three's sheen: its lobe sits on top and takes back the energy it reflects from the layers below.\n if (any(surface.sheenColor > vec3<f32>(0.0))) {\n let energy = 1.0 - max3(surface.sheenColor)\n * max(sheenAlbedo(nv, surface.sheenRoughness), sheenAlbedo(nl, surface.sheenRoughness));\n\n diffuse = diffuse * energy;\n specular = specular * energy\n + surface.sheenColor * sheenLobe(surface.sheenRoughness, clamp(dot(normal, half), 0.0, 1.0), nv, nl);\n }\n\n value = (diffuse + specular) * nl;\n }\n\n if (surface.clearcoat > 0.0) {\n let coatNormal = surface.clearcoatNormal;\n value = value * coatTransmittance(surface, view);\n\n let nlCoat = dot(coatNormal, light);\n let nvCoat = max(dot(coatNormal, view), 1e-4);\n\n if (nlCoat > 0.0) {\n let half = normalize(light + view);\n let alpha = surface.clearcoatRoughness * surface.clearcoatRoughness;\n let alpha2 = alpha * alpha;\n let fresnel = schlick3(vec3<f32>(0.04), 1.0, clamp(dot(view, half), 0.0, 1.0)).x;\n let lobe = fresnel * ggxVisibility(alpha2, nlCoat, nvCoat) * ggxDistribution(alpha2, max(dot(coatNormal, half), 0.0));\n\n value = value + vec3<f32>(surface.clearcoat * lobe * nlCoat);\n }\n }\n\n return value;\n}\n\n/** Three's D_BlinnPhong times its implicit visibility of a quarter: the Phong highlight's distribution. */\nfn blinnPhong(shininess: f32, nh: f32) -> f32 {\n return 0.25 * (shininess * 0.5 + 1.0) / PI * pow(max(nh, 0.0), shininess);\n}\n\n/** Density of the Blinn-Phong lobe's sampling, per solid angle of the light direction. */\nfn blinnPhongPdf(shininess: f32, nh: f32, vh: f32) -> f32 {\n return (shininess + 1.0) / (2.0 * PI) * pow(max(nh, 0.0), shininess) / max(4.0 * vh, 1e-6);\n}\n\n/** Density with which the GGX lobe (VNDF) would have produced this light direction, in the lobe's frame. */\nfn ggxPdf(localLight: vec3<f32>, localView: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let half = normalize(localLight + localView);\n\n if (half.z <= 0.0 || localView.z <= 0.0) {\n return 0.0;\n }\n\n return ggxMaskingAnisotropic(localView, alphas) * ggxAnisotropic(half, alphas) / (4.0 * localView.z);\n}\n\n/** The mixture density of every lobe this surface samples with. */\nfn bsdfPdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> f32 {\n let normal = surface.normal;\n let nl = dot(normal, light);\n let weights = lobeWeights(surface, view);\n var pdf = weights.x * max(nl, 0.0) / PI;\n\n if (surface.kind == KIND_PHONG) {\n let half = normalize(light + view);\n\n return pdf + weights.y * blinnPhongPdf(surface.shininess, dot(normal, half), max(dot(view, half), 0.0));\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return pdf;\n }\n\n let basis = specularBasis(surface);\n let alphas = specularAlphas(surface);\n\n pdf = pdf + weights.y * ggxPdf(\n vec3<f32>(dot(light, basis[0]), dot(light, basis[1]), nl),\n vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), dot(view, normal)),\n alphas);\n\n if (weights.z > 0.0) {\n let coatBasis = orthonormalBasis(surface.clearcoatNormal);\n let alpha = max(surface.clearcoatRoughness * surface.clearcoatRoughness, 1e-4);\n\n pdf = pdf + weights.z * ggxPdf(\n vec3<f32>(dot(light, coatBasis[0]), dot(light, coatBasis[1]), dot(light, surface.clearcoatNormal)),\n vec3<f32>(dot(view, coatBasis[0]), dot(view, coatBasis[1]), dot(view, surface.clearcoatNormal)),\n vec2<f32>(alpha));\n }\n\n return pdf;\n}\n\n/**\n * What the surface sends towards the view direction, and the density of that direction. A Lambert material keeps\n * Three's Lambert term alone, which is all its own shading has; a toon material reads its ramp in place of the cosine.\n */\nfn evaluateBsdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> BsdfEval {\n let normal = surface.normal;\n var result: BsdfEval;\n result.value = vec3<f32>(0.0);\n result.pdf = 0.0;\n\n let nl = dot(normal, light);\n let nv = dot(normal, view);\n\n if (nv <= 0.0) {\n return result;\n }\n\n if (surface.kind == KIND_TOON) {\n // Three's toon lighting reads its ramp across the whole sphere of directions, the side facing away included.\n result.value = surface.albedo / PI * toonRamp(surface.gradientMap, nl);\n result.pdf = max(nl, 0.0) / PI;\n\n return result;\n }\n\n // A clearcoat may face a light its base turns away from, so only a surface without one stops here.\n if (nl <= 0.0 && surface.clearcoat <= 0.0) {\n return result;\n }\n\n if ((surface.flags & MATERIAL_PBR) != 0u) {\n result.value = physicalValue(surface, view, light);\n } else if (surface.kind == KIND_PHONG) {\n let half = normalize(light + view);\n let fresnel = schlick3(surface.specularColor, 1.0, clamp(dot(view, half), 0.0, 1.0));\n let specular = fresnel * blinnPhong(surface.shininess, dot(normal, half)) * surface.specularStrength;\n\n result.value = (surface.albedo / PI + specular) * max(nl, 0.0);\n } else {\n result.value = surface.albedo / PI * max(nl, 0.0);\n }\n\n result.pdf = bsdfPdf(surface, view, light);\n\n return result;\n}\n\n/**\n * Reflectance of a smooth dielectric interface, from both polarisations rather than Schlick's fit: refraction needs the\n * exact cut-off past which nothing gets through. eta is the index the ray leaves over the index it enters.\n */\nfn fresnelDielectric(cosine: f32, eta: f32) -> f32 {\n let c = clamp(cosine, 0.0, 1.0);\n let sin2 = eta * eta * (1.0 - c * c);\n\n if (sin2 >= 1.0) {\n return 1.0;\n }\n\n let t = sqrt(1.0 - sin2);\n let s = (eta * c - t) / (eta * c + t);\n let p = (c - eta * t) / (c + eta * t);\n\n return 0.5 * (s * s + p * p);\n}\n\n/** Where a transmissive surface sends the path: off it, or through it. */\nstruct DielectricSample {\n direction: vec3<f32>,\n weight: vec3<f32>,\n // The path went through the surface instead of reflecting off it.\n crossed: bool,\n // It went through without bending, so the density that found the next hit is still the one before this surface.\n straight: bool,\n valid: bool,\n};\n\n/**\n * The transmissive share of a physical material. Reflection and refraction are chosen in proportion to the Fresnel\n * reflectance on a microfacet drawn from the visible normals, which leaves only the masking of the outgoing direction\n * in the weight. Light is tinted by the base colour once, on the way in, which is where Three applies it.\n *\n * A thin wall (zero thickness, Three's default) bends nothing: what is not reflected carries straight on, exactly as a\n * shadow ray crossing it does in thinCrossing, so the two estimators agree on a window. ior is the index this path\n * sees, which dispersion makes depend on the colour the path carries.\n */\nfn sampleDielectric(surface: Surface, view: vec3<f32>, ior: f32, lobe: f32, u: vec2<f32>) -> DielectricSample {\n let normal = surface.normal;\n var result: DielectricSample;\n result.valid = false;\n result.crossed = false;\n result.straight = false;\n result.weight = vec3<f32>(0.0);\n\n let nv = dot(normal, view);\n\n if (nv <= 0.0) {\n return result;\n }\n\n let alpha = max(surface.roughness * surface.roughness, 1e-4);\n let alpha2 = alpha * alpha;\n let basis = orthonormalBasis(normal);\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), nv);\n let localHalf = sampleVisibleNormal(localView, vec2<f32>(alpha), u);\n let half = basis[0] * localHalf.x + basis[1] * localHalf.y + normal * localHalf.z;\n\n if (surface.thickness <= 0.0) {\n let reflectance = fresnelDielectric(abs(dot(surface.facing, view)), 1.0 / ior);\n\n if (lobe >= reflectance) {\n result.direction = -view;\n result.weight = surface.albedo;\n result.crossed = true;\n result.straight = true;\n result.valid = true;\n\n return result;\n }\n\n result.direction = reflect(-view, half);\n } else {\n let eta = select(ior, 1.0 / ior, surface.front);\n let refracted = refract(-view, half, eta);\n // Past the critical angle refract() returns nothing, and everything reflects.\n let reflectance = select(fresnelDielectric(dot(view, half), eta), 1.0, all(refracted == vec3<f32>(0.0)));\n\n if (lobe >= reflectance) {\n result.direction = normalize(refracted);\n\n let no = -dot(normal, result.direction);\n\n if (no <= 0.0) {\n return result;\n }\n\n result.weight = vec3<f32>(ggxMasking(alpha2, no)) * select(vec3<f32>(1.0), surface.albedo, surface.front);\n result.crossed = true;\n result.valid = true;\n\n return result;\n }\n\n result.direction = reflect(-view, half);\n }\n\n let nl = dot(normal, result.direction);\n\n if (nl <= 0.0) {\n return result;\n }\n\n result.weight = vec3<f32>(ggxMasking(alpha2, nl));\n result.valid = true;\n\n return result;\n}\n\n/** A half vector around the normal distributed as cos^s, which is the Blinn-Phong lobe's own shape. */\nfn sampleBlinnPhongHalf(normal: vec3<f32>, shininess: f32, u: vec2<f32>) -> vec3<f32> {\n let cosine = pow(u.x, 1.0 / (shininess + 1.0));\n let sine = sqrt(max(0.0, 1.0 - cosine * cosine));\n let angle = 2.0 * PI * u.y;\n\n return normalize(orthonormalBasis(normal) * vec3<f32>(sine * cos(angle), sine * sin(angle), cosine));\n}\n\n/** Which way the path carries on, and what its throughput is multiplied by. */\nfn sampleBsdf(surface: Surface, view: vec3<f32>, lobe: f32, u: vec2<f32>) -> BsdfSample {\n let normal = surface.normal;\n var result: BsdfSample;\n result.valid = false;\n result.weight = vec3<f32>(0.0);\n result.pdf = 0.0;\n result.diffuse = true;\n\n if (dot(normal, view) <= 0.0) {\n return result;\n }\n\n // Lambert, and the bounce of a toon surface: Three lights a toon surface's indirect light with a plain Lambert term.\n if ((surface.flags & MATERIAL_PBR) == 0u && surface.kind != KIND_PHONG) {\n result.direction = cosineDirection(normal, u);\n // The cosine weight cancels the Lambert cosine exactly, which leaves the albedo alone.\n result.weight = surface.albedo;\n result.pdf = max(dot(normal, result.direction), 0.0) / PI;\n result.valid = result.pdf > 0.0;\n\n return result;\n }\n\n let weights = lobeWeights(surface, view);\n\n result.diffuse = lobe < weights.x;\n\n if (lobe < weights.x) {\n result.direction = cosineDirection(normal, u);\n } else if (surface.kind == KIND_PHONG) {\n result.direction = reflect(-view, sampleBlinnPhongHalf(normal, surface.shininess, u));\n } else if (lobe < weights.x + weights.y) {\n let basis = specularBasis(surface);\n // The lobe is drawn in its own frame, so the view direction goes into that frame and the result comes back out.\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), dot(view, normal));\n let localHalf = sampleVisibleNormal(localView, specularAlphas(surface), u);\n let half = basis * localHalf;\n\n result.direction = reflect(-view, half);\n } else {\n let coatNormal = surface.clearcoatNormal;\n let basis = orthonormalBasis(coatNormal);\n let alpha = max(surface.clearcoatRoughness * surface.clearcoatRoughness, 1e-4);\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), dot(view, coatNormal));\n let half = basis * sampleVisibleNormal(localView, vec2<f32>(alpha), u);\n\n result.direction = reflect(-view, half);\n }\n\n let evaluated = evaluateBsdf(surface, view, result.direction);\n\n if (evaluated.pdf <= 0.0 || all(evaluated.value <= vec3<f32>(0.0))) {\n return result;\n }\n\n result.pdf = evaluated.pdf;\n result.weight = evaluated.value / evaluated.pdf;\n result.valid = true;\n\n return result;\n}\n\n/**\n * Only the diffuse part of evaluateBsdf, times the cosine: what of the light from this direction the denoiser counts\n * as diffuse. The rest of the value is specular.\n */\nfn diffuseValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32> {\n let normal = surface.normal;\n let nl = dot(normal, light);\n let nv = max(dot(normal, view), 1e-4);\n\n if (surface.kind == KIND_TOON) {\n return surface.albedo / PI * toonRamp(surface.gradientMap, nl);\n }\n\n if (nl <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return surface.albedo / PI * nl;\n }\n\n var diffuse = surface.albedo * (1.0 - surface.metalness) / PI;\n\n if (any(surface.sheenColor > vec3<f32>(0.0))) {\n diffuse = diffuse * (1.0 - max3(surface.sheenColor)\n * max(sheenAlbedo(nv, surface.sheenRoughness), sheenAlbedo(nl, surface.sheenRoughness)));\n }\n\n return diffuse * nl * coatTransmittance(surface, view);\n}\n\n/** How much of the light a surface's specular lobes reflect in all, seen from this view: what that signal is divided by. */\nfn specularAlbedo(surface: Surface, view: vec3<f32>) -> vec3<f32> {\n let nv = clamp(dot(surface.normal, view), 0.0, 1.0);\n\n if (surface.kind == KIND_PHONG) {\n return surface.specularColor * surface.specularStrength;\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return vec3<f32>(0.0);\n }\n\n let dfg = dfgApprox(surface.roughness, nv);\n var albedo = surfaceF0(surface) * dfg.x + vec3<f32>(surface.specularF90 * dfg.y);\n\n if (surface.clearcoat > 0.0) {\n let coatDfg = dfgApprox(surface.clearcoatRoughness, clamp(dot(surface.clearcoatNormal, view), 0.0, 1.0));\n albedo = albedo * coatTransmittance(surface, view) + vec3<f32>(surface.clearcoat * (0.04 * coatDfg.x + coatDfg.y));\n }\n\n // What crosses a transmissive surface is the specular signal too, and it is not filtered by any colour of the surface.\n return mix(albedo, vec3<f32>(1.0), clamp(surface.transmission, 0.0, 1.0));\n}\n";
12
+ export declare const BSDF_WGSL = "\n/** How a surface answers a direction: what it reflects, and how likely sampling was to have chosen it. */\nstruct BsdfEval {\n // The bidirectional distribution times the cosine at the surface, which is what a contribution is multiplied by.\n value: vec3<f32>,\n // Solid angle density of this direction under this renderer's own sampling, for the power heuristic.\n pdf: f32,\n};\n\nstruct BsdfSample {\n direction: vec3<f32>,\n // The distribution over its own density: what the path's throughput is multiplied by.\n weight: vec3<f32>,\n pdf: f32,\n valid: bool,\n // The diffuse lobe chose the direction, rather than a specular one: which of the denoiser's two signals it feeds.\n diffuse: bool,\n};\n\nfn luminanceOf(color: vec3<f32>) -> f32 {\n return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));\n}\n\nfn max3(color: vec3<f32>) -> f32 {\n return max(color.r, max(color.g, color.b));\n}\n\n/** Schlick's approximation, as in Three: the reflectance always reaches one at a grazing angle. */\nfn fresnelSchlick(f0: vec3<f32>, cosine: f32) -> vec3<f32> {\n return schlick3(f0, 1.0, cosine);\n}\n\n/** Reflectance at normal incidence: the dielectric's own, turned towards the base colour as the surface turns metal. */\nfn surfaceF0(surface: Surface) -> vec3<f32> {\n return mix(surface.specularColor, surface.albedo, surface.metalness);\n}\n\n/** GGX, with alpha the square of the roughness, as Three parameterises it. */\nfn ggxDistribution(alpha2: f32, nh: f32) -> f32 {\n let denominator = nh * nh * (alpha2 - 1.0) + 1.0;\n\n return alpha2 / (PI * denominator * denominator);\n}\n\n/** Smith's height correlated visibility, which already carries the 1/(4 nl nv) the lobe would otherwise need. */\nfn ggxVisibility(alpha2: f32, nl: f32, nv: f32) -> f32 {\n return 0.5 / (nl * sqrt(nv * nv * (1.0 - alpha2) + alpha2) + nv * sqrt(nl * nl * (1.0 - alpha2) + alpha2) + 1e-7);\n}\n\n/** The separable masking term, which is the one the visible normal sampling is built on. */\nfn ggxMasking(alpha2: f32, nv: f32) -> f32 {\n return 2.0 * nv / (nv + sqrt(alpha2 + (1.0 - alpha2) * nv * nv));\n}\n\n/** Karis' analytic fit of the GGX directional albedo (Three's DFGApprox): the scale and bias applied to F0 and F90. */\nfn dfgApprox(roughness: f32, nv: f32) -> vec2<f32> {\n let r = roughness * vec4<f32>(-1.0, -0.0275, -0.572, 0.022) + vec4<f32>(1.0, 0.0425, 1.04, -0.04);\n let a004 = min(r.x * r.x, exp2(-9.28 * nv)) * r.x + r.y;\n\n return vec2<f32>(-1.04, 1.04) * a004 + r.zw;\n}\n\n/** Three's IBLSheenBRDF: the Charlie sheen lobe integrated over the hemisphere. */\nfn sheenAlbedo(cosine: f32, roughness: f32) -> f32 {\n let r2 = roughness * roughness;\n let inverse = 1.0 / (roughness + 0.1);\n let a = -1.9362 + 1.0678 * roughness + 0.4573 * r2 - 0.8469 * inverse;\n let b = -0.6014 + 0.5538 * roughness - 0.4670 * r2 - 0.1255 * inverse;\n\n return clamp(exp(a * clamp(cosine, 0.0, 1.0) + b), 0.0, 1.0);\n}\n\n/** Three's D_Charlie times V_Neubelt. */\nfn sheenLobe(roughness: f32, nh: f32, nv: f32, nl: f32) -> f32 {\n let inverse = 1.0 / (roughness * roughness);\n let sin2h = max(1.0 - nh * nh, 0.0078125);\n let distribution = (2.0 + inverse) * pow(sin2h, inverse * 0.5) / (2.0 * PI);\n\n return distribution * clamp(1.0 / (4.0 * (nl + nv - nl * nv)), 0.0, 1.0);\n}\n\n/** The frame the specular lobe is defined in: Three's anisotropy direction when there is one, any frame otherwise. */\nfn specularBasis(surface: Surface) -> mat3x3<f32> {\n let normal = surface.normal;\n\n if (surface.anisotropy > 0.0) {\n let t = surface.anisotropyT - normal * dot(normal, surface.anisotropyT);\n let tangent = normalize(t);\n\n if (all(tangent == tangent) && dot(t, t) > 1e-12) {\n return mat3x3<f32>(tangent, cross(normal, tangent), normal);\n }\n }\n\n return orthonormalBasis(normal);\n}\n\n/** The GGX roughness along the frame's two axes: Three's alphaT along the anisotropy, alpha across it. */\nfn specularAlphas(surface: Surface) -> vec2<f32> {\n let alpha = surface.roughness * surface.roughness;\n\n if (surface.anisotropy > 0.0) {\n return vec2<f32>(max(surface.alphaT, 1e-4), max(alpha, 1e-4));\n }\n\n return vec2<f32>(max(alpha, 1e-4));\n}\n\n/** Anisotropic GGX in the lobe's own frame; with equal alphas it is exactly the isotropic distribution. */\nfn ggxAnisotropic(h: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let x = h.x / alphas.x;\n let y = h.y / alphas.y;\n let d = x * x + y * y + h.z * h.z;\n\n return 1.0 / (PI * alphas.x * alphas.y * d * d);\n}\n\n/** Smith's masking of one direction, in the lobe's frame. */\nfn ggxMaskingAnisotropic(v: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let projected = alphas.x * alphas.x * v.x * v.x + alphas.y * alphas.y * v.y * v.y;\n\n return 2.0 * v.z / (v.z + sqrt(v.z * v.z + projected));\n}\n\n/** Three's V_GGX_SmithCorrelated_Anisotropic, in the lobe's frame. */\nfn ggxVisibilityAnisotropic(l: vec3<f32>, v: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let gv = l.z * length(vec3<f32>(alphas.x * v.x, alphas.y * v.y, v.z));\n let gl = v.z * length(vec3<f32>(alphas.x * l.x, alphas.y * l.y, l.z));\n\n return 0.5 / max(gv + gl, 1e-7);\n}\n\n/**\n * A half vector drawn from the distribution of normals the view direction can actually see (Heitz 2018), which is what\n * keeps the specular lobe from wasting samples on microfacets facing away. The view is in the lobe's frame.\n */\nfn sampleVisibleNormal(view: vec3<f32>, alphas: vec2<f32>, u: vec2<f32>) -> vec3<f32> {\n let stretched = normalize(vec3<f32>(alphas.x * view.x, alphas.y * view.y, view.z));\n let lengthSquared = stretched.x * stretched.x + stretched.y * stretched.y;\n let t1 = select(\n vec3<f32>(1.0, 0.0, 0.0),\n vec3<f32>(-stretched.y, stretched.x, 0.0) * inverseSqrt(max(lengthSquared, 1e-20)),\n lengthSquared > 1e-20);\n let t2 = cross(stretched, t1);\n let radius = sqrt(u.x);\n let angle = 2.0 * PI * u.y;\n let px = radius * cos(angle);\n let raw = radius * sin(angle);\n let lift = 0.5 * (1.0 + stretched.z);\n let py = (1.0 - lift) * sqrt(max(0.0, 1.0 - px * px)) + lift * raw;\n let normal = t1 * px + t2 * py + stretched * sqrt(max(0.0, 1.0 - px * px - py * py));\n\n return normalize(vec3<f32>(alphas.x * normal.x, alphas.y * normal.y, max(normal.z, 1e-6)));\n}\n\n/** What the clearcoat takes back from everything under it, seen from this view: Three's (1 - clearcoat * Fcc). */\nfn coatTransmittance(surface: Surface, view: vec3<f32>) -> f32 {\n if (surface.clearcoat <= 0.0) {\n return 1.0;\n }\n\n let nv = clamp(dot(surface.clearcoatNormal, view), 0.0, 1.0);\n\n return 1.0 - surface.clearcoat * schlick3(vec3<f32>(0.04), 1.0, nv).x;\n}\n\n/**\n * A point inside a volume, where light scatters by the medium's phase function instead of off a surface. Only the\n * integrator makes one: its normal faces back along the ray, its albedo is the medium's, its anisotropy the phase's.\n */\nconst KIND_VOLUME: u32 = 100u;\n\n/** How often each lobe is chosen, as (diffuse, specular, clearcoat): by how much of the reflection each accounts for. */\nfn lobeWeights(surface: Surface, view: vec3<f32>) -> vec3<f32> {\n let nv = max(dot(surface.normal, view), 1e-4);\n\n if (surface.kind == KIND_PHONG) {\n let diffuse = luminanceOf(surface.albedo) + 0.02;\n let specular = luminanceOf(surface.specularColor) * surface.specularStrength * (surface.shininess * 0.125 + 1.0) + 0.02;\n\n return vec3<f32>(diffuse, specular, 0.0) / (diffuse + specular);\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return vec3<f32>(1.0, 0.0, 0.0);\n }\n\n let base = coatTransmittance(surface, view);\n // The floors keep every lobe reachable: the diffuse one also carries the sheen and the multiple scattering term.\n let diffuse = (luminanceOf(surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)))\n + max3(surface.sheenColor)) * base + 0.02;\n let specular = luminanceOf(schlick3(surface.specularColor, surface.specularF90, nv)\n * (1.0 - surface.metalness) + surface.albedo * surface.metalness) * base + 0.05;\n let coat = select(0.0, (1.0 - base) + 0.02, surface.clearcoat > 0.0);\n let total = diffuse + specular + coat;\n\n return vec3<f32>(diffuse, specular, coat) / total;\n}\n\n/** Three's physical direct lighting for one light direction, times the cosine. */\nfn physicalValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32> {\n let normal = surface.normal;\n let nl = dot(normal, light);\n let nv = max(dot(normal, view), 1e-4);\n var value = vec3<f32>(0.0);\n\n if (nl > 0.0) {\n let basis = specularBasis(surface);\n let alphas = specularAlphas(surface);\n let half = normalize(light + view);\n let vh = clamp(dot(view, half), 0.0, 1.0);\n let f0 = surfaceF0(surface);\n var fresnel = schlick3(f0, surface.specularF90, vh);\n\n if (surface.iridescence > 0.0) {\n fresnel = mix(fresnel, surface.iridescenceFresnel, surface.iridescence);\n }\n\n let localLight = vec3<f32>(dot(light, basis[0]), dot(light, basis[1]), nl);\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), nv);\n let localHalf = vec3<f32>(dot(half, basis[0]), dot(half, basis[1]), max(dot(half, normal), 0.0));\n let single = fresnel * (ggxVisibilityAnisotropic(localLight, localView, alphas) * ggxAnisotropic(localHalf, alphas));\n\n // Three's BRDF_GGX_Multiscatter: the energy single scattering loses at high roughness, returned.\n let dfgV = dfgApprox(surface.roughness, nv);\n let dfgL = dfgApprox(surface.roughness, nl);\n let essV = f0 * dfgV.x + surface.specularF90 * dfgV.y;\n let essL = f0 * dfgL.x + surface.specularF90 * dfgL.y;\n let emsV = 1.0 - (dfgV.x + dfgV.y);\n let emsL = 1.0 - (dfgL.x + dfgL.y);\n let average = f0 + (vec3<f32>(1.0) - f0) * 0.047619;\n let multiple = essV * essL * average / (vec3<f32>(1.0) - emsV * emsL * average + 1e-7) * (emsV * emsL);\n\n // What scatters inside the material leaves it elsewhere, by the random walk, not from this point.\n var diffuse = surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)) / PI;\n var specular = single + multiple;\n\n // Three's sheen: its lobe sits on top and takes back the energy it reflects from the layers below.\n if (any(surface.sheenColor > vec3<f32>(0.0))) {\n let energy = 1.0 - max3(surface.sheenColor)\n * max(sheenAlbedo(nv, surface.sheenRoughness), sheenAlbedo(nl, surface.sheenRoughness));\n\n diffuse = diffuse * energy;\n specular = specular * energy\n + surface.sheenColor * sheenLobe(surface.sheenRoughness, clamp(dot(normal, half), 0.0, 1.0), nv, nl);\n }\n\n value = (diffuse + specular) * nl;\n }\n\n if (surface.clearcoat > 0.0) {\n let coatNormal = surface.clearcoatNormal;\n value = value * coatTransmittance(surface, view);\n\n let nlCoat = dot(coatNormal, light);\n let nvCoat = max(dot(coatNormal, view), 1e-4);\n\n if (nlCoat > 0.0) {\n let half = normalize(light + view);\n let alpha = surface.clearcoatRoughness * surface.clearcoatRoughness;\n let alpha2 = alpha * alpha;\n let fresnel = schlick3(vec3<f32>(0.04), 1.0, clamp(dot(view, half), 0.0, 1.0)).x;\n let lobe = fresnel * ggxVisibility(alpha2, nlCoat, nvCoat) * ggxDistribution(alpha2, max(dot(coatNormal, half), 0.0));\n\n value = value + vec3<f32>(surface.clearcoat * lobe * nlCoat);\n }\n }\n\n return value;\n}\n\n/** Three's D_BlinnPhong times its implicit visibility of a quarter: the Phong highlight's distribution. */\nfn blinnPhong(shininess: f32, nh: f32) -> f32 {\n return 0.25 * (shininess * 0.5 + 1.0) / PI * pow(max(nh, 0.0), shininess);\n}\n\n/** Density of the Blinn-Phong lobe's sampling, per solid angle of the light direction. */\nfn blinnPhongPdf(shininess: f32, nh: f32, vh: f32) -> f32 {\n return (shininess + 1.0) / (2.0 * PI) * pow(max(nh, 0.0), shininess) / max(4.0 * vh, 1e-6);\n}\n\n/** Density with which the GGX lobe (VNDF) would have produced this light direction, in the lobe's frame. */\nfn ggxPdf(localLight: vec3<f32>, localView: vec3<f32>, alphas: vec2<f32>) -> f32 {\n let half = normalize(localLight + localView);\n\n if (half.z <= 0.0 || localView.z <= 0.0) {\n return 0.0;\n }\n\n return ggxMaskingAnisotropic(localView, alphas) * ggxAnisotropic(half, alphas) / (4.0 * localView.z);\n}\n\n/** The mixture density of every lobe this surface samples with. */\nfn bsdfPdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> f32 {\n let normal = surface.normal;\n let nl = dot(normal, light);\n let weights = lobeWeights(surface, view);\n var pdf = weights.x * max(nl, 0.0) / PI;\n\n if (surface.kind == KIND_PHONG) {\n let half = normalize(light + view);\n\n return pdf + weights.y * blinnPhongPdf(surface.shininess, dot(normal, half), max(dot(view, half), 0.0));\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return pdf;\n }\n\n let basis = specularBasis(surface);\n let alphas = specularAlphas(surface);\n\n pdf = pdf + weights.y * ggxPdf(\n vec3<f32>(dot(light, basis[0]), dot(light, basis[1]), nl),\n vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), dot(view, normal)),\n alphas);\n\n if (weights.z > 0.0) {\n let coatBasis = orthonormalBasis(surface.clearcoatNormal);\n let alpha = max(surface.clearcoatRoughness * surface.clearcoatRoughness, 1e-4);\n\n pdf = pdf + weights.z * ggxPdf(\n vec3<f32>(dot(light, coatBasis[0]), dot(light, coatBasis[1]), dot(light, surface.clearcoatNormal)),\n vec3<f32>(dot(view, coatBasis[0]), dot(view, coatBasis[1]), dot(view, surface.clearcoatNormal)),\n vec2<f32>(alpha));\n }\n\n return pdf;\n}\n\n/**\n * What the surface sends towards the view direction, and the density of that direction. A Lambert material keeps\n * Three's Lambert term alone, which is all its own shading has; a toon material reads its ramp in place of the cosine.\n */\nfn evaluateBsdf(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> BsdfEval {\n let normal = surface.normal;\n var result: BsdfEval;\n result.value = vec3<f32>(0.0);\n result.pdf = 0.0;\n\n let nl = dot(normal, light);\n let nv = dot(normal, view);\n\n // In a medium every direction is open: the phase function stands for the lobes, and samples itself exactly.\n if (surface.kind == KIND_VOLUME) {\n let phase = henyeyGreenstein(dot(-view, light), surface.anisotropy);\n\n result.value = surface.albedo * phase;\n result.pdf = phase;\n\n return result;\n }\n\n if (nv <= 0.0) {\n return result;\n }\n\n if (surface.kind == KIND_TOON) {\n // Three's toon lighting reads its ramp across the whole sphere of directions, the side facing away included.\n result.value = surface.albedo / PI * toonRamp(surface.gradientMap, nl);\n result.pdf = max(nl, 0.0) / PI;\n\n return result;\n }\n\n // A clearcoat may face a light its base turns away from, so only a surface without one stops here.\n if (nl <= 0.0 && surface.clearcoat <= 0.0) {\n return result;\n }\n\n if ((surface.flags & MATERIAL_PBR) != 0u) {\n result.value = physicalValue(surface, view, light);\n } else if (surface.kind == KIND_PHONG) {\n let half = normalize(light + view);\n let fresnel = schlick3(surface.specularColor, 1.0, clamp(dot(view, half), 0.0, 1.0));\n let specular = fresnel * blinnPhong(surface.shininess, dot(normal, half)) * surface.specularStrength;\n\n result.value = (surface.albedo / PI + specular) * max(nl, 0.0);\n } else {\n result.value = surface.albedo / PI * max(nl, 0.0);\n }\n\n result.pdf = bsdfPdf(surface, view, light);\n\n return result;\n}\n\n/**\n * Reflectance of a smooth dielectric interface, from both polarisations rather than Schlick's fit: refraction needs the\n * exact cut-off past which nothing gets through. eta is the index the ray leaves over the index it enters.\n */\nfn fresnelDielectric(cosine: f32, eta: f32) -> f32 {\n let c = clamp(cosine, 0.0, 1.0);\n let sin2 = eta * eta * (1.0 - c * c);\n\n if (sin2 >= 1.0) {\n return 1.0;\n }\n\n let t = sqrt(1.0 - sin2);\n let s = (eta * c - t) / (eta * c + t);\n let p = (c - eta * t) / (c + eta * t);\n\n return 0.5 * (s * s + p * p);\n}\n\n/** Where a transmissive surface sends the path: off it, or through it. */\nstruct DielectricSample {\n direction: vec3<f32>,\n weight: vec3<f32>,\n // The path went through the surface instead of reflecting off it.\n crossed: bool,\n // It went through without bending, so the density that found the next hit is still the one before this surface.\n straight: bool,\n valid: bool,\n};\n\n/**\n * The transmissive share of a physical material. Reflection and refraction are chosen in proportion to the Fresnel\n * reflectance on a microfacet drawn from the visible normals, which leaves only the masking of the outgoing direction\n * in the weight. Light is tinted by the base colour once, on the way in, which is where Three applies it.\n *\n * A thin wall (zero thickness, Three's default) bends nothing: what is not reflected carries straight on, exactly as a\n * shadow ray crossing it does in thinCrossing, so the two estimators agree on a window. ior is the index this path\n * sees, which dispersion makes depend on the colour the path carries.\n */\nfn sampleDielectric(surface: Surface, view: vec3<f32>, ior: f32, lobe: f32, u: vec2<f32>) -> DielectricSample {\n let normal = surface.normal;\n var result: DielectricSample;\n result.valid = false;\n result.crossed = false;\n result.straight = false;\n result.weight = vec3<f32>(0.0);\n\n let nv = dot(normal, view);\n\n if (nv <= 0.0) {\n return result;\n }\n\n let alpha = max(surface.roughness * surface.roughness, 1e-4);\n let alpha2 = alpha * alpha;\n let basis = orthonormalBasis(normal);\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), nv);\n let localHalf = sampleVisibleNormal(localView, vec2<f32>(alpha), u);\n let half = basis[0] * localHalf.x + basis[1] * localHalf.y + normal * localHalf.z;\n\n if (surface.thickness <= 0.0) {\n let reflectance = fresnelDielectric(abs(dot(surface.facing, view)), 1.0 / ior);\n\n if (lobe >= reflectance) {\n result.direction = -view;\n result.weight = surface.albedo;\n result.crossed = true;\n result.straight = true;\n result.valid = true;\n\n return result;\n }\n\n result.direction = reflect(-view, half);\n } else {\n let eta = select(ior, 1.0 / ior, surface.front);\n let refracted = refract(-view, half, eta);\n // Past the critical angle refract() returns nothing, and everything reflects.\n let reflectance = select(fresnelDielectric(dot(view, half), eta), 1.0, all(refracted == vec3<f32>(0.0)));\n\n if (lobe >= reflectance) {\n result.direction = normalize(refracted);\n\n let no = -dot(normal, result.direction);\n\n if (no <= 0.0) {\n return result;\n }\n\n result.weight = vec3<f32>(ggxMasking(alpha2, no)) * select(vec3<f32>(1.0), surface.albedo, surface.front);\n result.crossed = true;\n result.valid = true;\n\n return result;\n }\n\n result.direction = reflect(-view, half);\n }\n\n let nl = dot(normal, result.direction);\n\n if (nl <= 0.0) {\n return result;\n }\n\n result.weight = vec3<f32>(ggxMasking(alpha2, nl));\n result.valid = true;\n\n return result;\n}\n\n/** A half vector around the normal distributed as cos^s, which is the Blinn-Phong lobe's own shape. */\nfn sampleBlinnPhongHalf(normal: vec3<f32>, shininess: f32, u: vec2<f32>) -> vec3<f32> {\n let cosine = pow(u.x, 1.0 / (shininess + 1.0));\n let sine = sqrt(max(0.0, 1.0 - cosine * cosine));\n let angle = 2.0 * PI * u.y;\n\n return normalize(orthonormalBasis(normal) * vec3<f32>(sine * cos(angle), sine * sin(angle), cosine));\n}\n\n/** Which way the path carries on, and what its throughput is multiplied by. */\nfn sampleBsdf(surface: Surface, view: vec3<f32>, lobe: f32, u: vec2<f32>) -> BsdfSample {\n let normal = surface.normal;\n var result: BsdfSample;\n result.valid = false;\n result.weight = vec3<f32>(0.0);\n result.pdf = 0.0;\n result.diffuse = true;\n\n if (surface.kind == KIND_VOLUME) {\n result.direction = sampleHenyeyGreenstein(-view, surface.anisotropy, u);\n result.weight = surface.albedo;\n result.pdf = henyeyGreenstein(dot(-view, result.direction), surface.anisotropy);\n result.valid = true;\n\n return result;\n }\n\n if (dot(normal, view) <= 0.0) {\n return result;\n }\n\n // Lambert, and the bounce of a toon surface: Three lights a toon surface's indirect light with a plain Lambert term.\n if ((surface.flags & MATERIAL_PBR) == 0u && surface.kind != KIND_PHONG) {\n result.direction = cosineDirection(normal, u);\n // The cosine weight cancels the Lambert cosine exactly, which leaves the albedo alone.\n result.weight = surface.albedo;\n result.pdf = max(dot(normal, result.direction), 0.0) / PI;\n result.valid = result.pdf > 0.0;\n\n return result;\n }\n\n let weights = lobeWeights(surface, view);\n\n result.diffuse = lobe < weights.x;\n\n if (lobe < weights.x) {\n result.direction = cosineDirection(normal, u);\n } else if (surface.kind == KIND_PHONG) {\n result.direction = reflect(-view, sampleBlinnPhongHalf(normal, surface.shininess, u));\n } else if (lobe < weights.x + weights.y) {\n let basis = specularBasis(surface);\n // The lobe is drawn in its own frame, so the view direction goes into that frame and the result comes back out.\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), dot(view, normal));\n let localHalf = sampleVisibleNormal(localView, specularAlphas(surface), u);\n let half = basis * localHalf;\n\n result.direction = reflect(-view, half);\n } else {\n let coatNormal = surface.clearcoatNormal;\n let basis = orthonormalBasis(coatNormal);\n let alpha = max(surface.clearcoatRoughness * surface.clearcoatRoughness, 1e-4);\n let localView = vec3<f32>(dot(view, basis[0]), dot(view, basis[1]), dot(view, coatNormal));\n let half = basis * sampleVisibleNormal(localView, vec2<f32>(alpha), u);\n\n result.direction = reflect(-view, half);\n }\n\n let evaluated = evaluateBsdf(surface, view, result.direction);\n\n if (evaluated.pdf <= 0.0 || all(evaluated.value <= vec3<f32>(0.0))) {\n return result;\n }\n\n result.pdf = evaluated.pdf;\n result.weight = evaluated.value / evaluated.pdf;\n result.valid = true;\n\n return result;\n}\n\n/**\n * Only the diffuse part of evaluateBsdf, times the cosine: what of the light from this direction the denoiser counts\n * as diffuse. The rest of the value is specular.\n */\nfn diffuseValue(surface: Surface, view: vec3<f32>, light: vec3<f32>) -> vec3<f32> {\n let normal = surface.normal;\n let nl = dot(normal, light);\n let nv = max(dot(normal, view), 1e-4);\n\n if (surface.kind == KIND_TOON) {\n return surface.albedo / PI * toonRamp(surface.gradientMap, nl);\n }\n\n // Light scattered in a medium is all diffuse, as the denoiser sees it.\n if (surface.kind == KIND_VOLUME) {\n return surface.albedo * henyeyGreenstein(dot(-view, light), surface.anisotropy);\n }\n\n if (nl <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return surface.albedo / PI * nl;\n }\n\n var diffuse = surface.albedo * ((1.0 - surface.metalness) * (1.0 - surface.subsurface)) / PI;\n\n if (any(surface.sheenColor > vec3<f32>(0.0))) {\n diffuse = diffuse * (1.0 - max3(surface.sheenColor)\n * max(sheenAlbedo(nv, surface.sheenRoughness), sheenAlbedo(nl, surface.sheenRoughness)));\n }\n\n return diffuse * nl * coatTransmittance(surface, view);\n}\n\n/** How much of the light a surface's specular lobes reflect in all, seen from this view: what that signal is divided by. */\nfn specularAlbedo(surface: Surface, view: vec3<f32>) -> vec3<f32> {\n let nv = clamp(dot(surface.normal, view), 0.0, 1.0);\n\n if (surface.kind == KIND_PHONG) {\n return surface.specularColor * surface.specularStrength;\n }\n\n if ((surface.flags & MATERIAL_PBR) == 0u) {\n return vec3<f32>(0.0);\n }\n\n let dfg = dfgApprox(surface.roughness, nv);\n var albedo = surfaceF0(surface) * dfg.x + vec3<f32>(surface.specularF90 * dfg.y);\n\n if (surface.clearcoat > 0.0) {\n let coatDfg = dfgApprox(surface.clearcoatRoughness, clamp(dot(surface.clearcoatNormal, view), 0.0, 1.0));\n albedo = albedo * coatTransmittance(surface, view) + vec3<f32>(surface.clearcoat * (0.04 * coatDfg.x + coatDfg.y));\n }\n\n // What crosses a transmissive surface is the specular signal too, and it is not filtered by any colour of the surface.\n return mix(albedo, vec3<f32>(1.0), clamp(surface.transmission, 0.0, 1.0));\n}\n";
13
13
  //# sourceMappingURL=bsdf.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bsdf.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/bsdf.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,s8tBA8lBrB,CAAC"}
1
+ {"version":3,"file":"bsdf.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/bsdf.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,+twBA8nBrB,CAAC"}
@@ -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 +1 @@
1
- {"version":3,"file":"common.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/common.wgsl.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAwMvB,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"}
@@ -98,6 +98,10 @@ const MATERIAL_OBJECT_NORMAL_MAP: u32 = 256u;
98
98
  const MATERIAL_PATCHED: u32 = 512u;
99
99
  const MATERIAL_ALPHA_HASH: u32 = 2048u;
100
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;
101
105
 
102
106
  const KIND_STANDARD: u32 = ${MATERIAL_KIND.standard}u;
103
107
  const KIND_PHYSICAL: u32 = ${MATERIAL_KIND.physical}u;
@@ -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;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;2BAgBpB,WAAW;0BACZ,UAAU;6BACP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2EzC,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"}