@tinypoly/gpu-renderer 0.0.5 → 0.1.1
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.
- package/ARCHITECTURE.md +59 -3
- package/README.md +53 -4
- package/dist/atmosphere.d.ts +21 -0
- package/dist/atmosphere.d.ts.map +1 -0
- package/dist/atmosphere.js +72 -0
- package/dist/atmosphere.js.map +1 -0
- package/dist/denoise.d.ts +11 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +34 -6
- package/dist/denoise.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +17 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +6 -3
- package/dist/geometry.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +60 -0
- package/dist/lensPass.d.ts.map +1 -0
- package/dist/lensPass.js +235 -0
- package/dist/lensPass.js.map +1 -0
- package/dist/lightTree.d.ts +50 -0
- package/dist/lightTree.d.ts.map +1 -0
- package/dist/lightTree.js +146 -0
- package/dist/lightTree.js.map +1 -0
- package/dist/lights.d.ts +12 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +56 -1
- package/dist/lights.js.map +1 -1
- package/dist/materialLayout.d.ts +24 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +36 -1
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +6 -3
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +16 -10
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +27 -7
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +153 -16
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +6 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +5 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +142 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +100 -0
- package/dist/settings.js.map +1 -0
- package/dist/subsurface.d.ts +76 -0
- package/dist/subsurface.d.ts.map +1 -0
- package/dist/subsurface.js +70 -0
- package/dist/subsurface.js.map +1 -0
- package/dist/volumeContainment.d.ts +17 -0
- package/dist/volumeContainment.d.ts.map +1 -0
- package/dist/volumeContainment.js +61 -0
- package/dist/volumeContainment.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +35 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +4 -0
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +17 -0
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lens.wgsl.js +301 -0
- package/dist/wgsl/lens.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +237 -31
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +406 -25
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/random.wgsl.d.ts +1 -1
- package/dist/wgsl/random.wgsl.d.ts.map +1 -1
- package/dist/wgsl/random.wgsl.js +28 -0
- package/dist/wgsl/random.wgsl.js.map +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
- package/dist/wgsl/sampling.wgsl.js +25 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +21 -1
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/atmosphere.ts +95 -0
- package/src/denoise.ts +44 -6
- package/src/frameLayout.ts +17 -0
- package/src/geometry.ts +6 -2
- package/src/index.ts +13 -0
- package/src/lensPass.ts +299 -0
- package/src/lightTree.ts +218 -0
- package/src/lights.ts +75 -1
- package/src/materialLayout.ts +52 -1
- package/src/pathTracePass.ts +19 -10
- package/src/renderer.ts +206 -24
- package/src/sceneUpload.ts +11 -0
- package/src/settings.ts +240 -0
- package/src/subsurface.ts +153 -0
- package/src/volumeContainment.ts +81 -0
- package/src/wgsl/bsdf.wgsl.ts +35 -3
- package/src/wgsl/common.wgsl.ts +4 -0
- package/src/wgsl/lens.wgsl.ts +300 -0
- package/src/wgsl/lights.wgsl.ts +237 -31
- package/src/wgsl/pathTrace.wgsl.ts +406 -25
- package/src/wgsl/random.wgsl.ts +28 -0
- package/src/wgsl/sampling.wgsl.ts +25 -0
- package/src/wgsl/surface.wgsl.ts +21 -1
package/dist/wgsl/random.wgsl.js
CHANGED
|
@@ -64,6 +64,34 @@ fn toUnit(value: u32) -> f32 {
|
|
|
64
64
|
return min(f32(value) * 2.3283064365386963e-10, 0.99999994);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The next pair of dimensions from a plain hash instead of the scrambled sequence: for the deep dimensions of a random
|
|
69
|
+
* walk, where stratification buys nothing and the sequence's table reads and scrambles cost a step more than its ray.
|
|
70
|
+
* It takes a dimension of its own, so the sequence's draws after it stay where they would have been.
|
|
71
|
+
*/
|
|
72
|
+
fn hashSample2D(rng: ptr<function, PathSampler>) -> vec2<f32> {
|
|
73
|
+
let first = pcgHash((*rng).seed ^ pcgHash((*rng).index * 2654435761u + (*rng).dimension * 0x9e3779b9u));
|
|
74
|
+
(*rng).dimension = (*rng).dimension + 1u;
|
|
75
|
+
|
|
76
|
+
return vec2<f32>(toUnit(first), toUnit(pcgHash(first)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How many vertices of a path draw from the scrambled sequence. Its stratification shows on the camera ray and the
|
|
81
|
+
* first bounces, where the light a pixel sees is decided; past them the light has been averaged by every bounce
|
|
82
|
+
* before, and a plain hash gives numbers as good for a fraction of the cost.
|
|
83
|
+
*/
|
|
84
|
+
const SEQUENCE_DEPTH: u32 = 3u;
|
|
85
|
+
|
|
86
|
+
/** The next pair of dimensions of a path's vertex: from the sequence while the path is shallow, from a hash after. */
|
|
87
|
+
fn pathSample2D(rng: ptr<function, PathSampler>, deep: bool) -> vec2<f32> {
|
|
88
|
+
if (deep) {
|
|
89
|
+
return hashSample2D(rng);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return sample2D(rng);
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
/**
|
|
68
96
|
* The next pair of dimensions, in [0, 1). The sample index is shuffled per pair as well, which pads the two dimensional
|
|
69
97
|
* sequence out to as many dimensions as a path needs without correlating them.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"random.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"random.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/random.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsGrC,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** The geometric primitives the lobes and the light sampling are built from. */
|
|
2
|
-
export declare const SAMPLING_WGSL = "\n/** Branchless orthonormal basis around a unit normal (Duff and others, 2017). */\nfn orthonormalBasis(n: vec3<f32>) -> mat3x3<f32> {\n let s = select(-1.0, 1.0, n.z >= 0.0);\n let a = -1.0 / (s + n.z);\n let b = n.x * n.y * a;\n\n return mat3x3<f32>(\n vec3<f32>(1.0 + s * n.x * n.x * a, s * b, -s * n.x),\n vec3<f32>(b, s + n.y * n.y * a, -n.y),\n n);\n}\n\n/** Cosine weighted direction in the hemisphere around the normal; the weight cancels the Lambert cosine. */\nfn cosineDirection(normal: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n let radius = sqrt(u.x);\n let angle = 2.0 * PI * u.y;\n let local = vec3<f32>(radius * cos(angle), radius * sin(angle), sqrt(max(0.0, 1.0 - u.x)));\n\n return normalize(orthonormalBasis(normal) * local);\n}\n\n/** The unit normal of the triangle's plane; which side it faces does not matter, every emitter is two sided. */\nfn triangleNormal(triangle: u32) -> vec3<f32> {\n let a = triangleVertex(triangle, 0u);\n\n return normalize(cross(triangleVertex(triangle, 1u) - a, triangleVertex(triangle, 2u) - a));\n}\n";
|
|
2
|
+
export declare const SAMPLING_WGSL = "\n/** Branchless orthonormal basis around a unit normal (Duff and others, 2017). */\nfn orthonormalBasis(n: vec3<f32>) -> mat3x3<f32> {\n let s = select(-1.0, 1.0, n.z >= 0.0);\n let a = -1.0 / (s + n.z);\n let b = n.x * n.y * a;\n\n return mat3x3<f32>(\n vec3<f32>(1.0 + s * n.x * n.x * a, s * b, -s * n.x),\n vec3<f32>(b, s + n.y * n.y * a, -n.y),\n n);\n}\n\n/** Cosine weighted direction in the hemisphere around the normal; the weight cancels the Lambert cosine. */\nfn cosineDirection(normal: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n let radius = sqrt(u.x);\n let angle = 2.0 * PI * u.y;\n let local = vec3<f32>(radius * cos(angle), radius * sin(angle), sqrt(max(0.0, 1.0 - u.x)));\n\n return normalize(orthonormalBasis(normal) * local);\n}\n\n/** A direction scattered from forward by the Henyey-Greenstein phase function, which its own sampling cancels. */\nfn sampleHenyeyGreenstein(forward: vec3<f32>, g: f32, u: vec2<f32>) -> vec3<f32> {\n var cosine = 1.0 - 2.0 * u.x;\n\n if (abs(g) > 1e-3) {\n let s = (1.0 - g * g) / (1.0 - g + 2.0 * g * u.x);\n cosine = clamp((1.0 + g * g - s * s) / (2.0 * g), -1.0, 1.0);\n }\n\n let sine = sqrt(max(0.0, 1.0 - cosine * cosine));\n let angle = 2.0 * PI * u.y;\n\n return normalize(orthonormalBasis(forward) * vec3<f32>(sine * cos(angle), sine * sin(angle), cosine));\n}\n\n/**\n * The Henyey-Greenstein phase function, per steradian, for a turn whose cosine is given from the direction of travel:\n * the density sampleHenyeyGreenstein draws with.\n */\nfn henyeyGreenstein(cosine: f32, g: f32) -> f32 {\n let denominator = 1.0 + g * g - 2.0 * g * cosine;\n\n return (1.0 - g * g) / (4.0 * PI * denominator * sqrt(max(denominator, 1e-12)));\n}\n\n/** The unit normal of the triangle's plane; which side it faces does not matter, every emitter is two sided. */\nfn triangleNormal(triangle: u32) -> vec3<f32> {\n let a = triangleVertex(triangle, 0u);\n\n return normalize(cross(triangleVertex(triangle, 1u) - a, triangleVertex(triangle, 2u) - a));\n}\n";
|
|
3
3
|
//# sourceMappingURL=sampling.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sampling.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/sampling.wgsl.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"sampling.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/sampling.wgsl.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,eAAO,MAAM,aAAa,w/DAqDzB,CAAC"}
|
|
@@ -21,6 +21,31 @@ fn cosineDirection(normal: vec3<f32>, u: vec2<f32>) -> vec3<f32> {
|
|
|
21
21
|
return normalize(orthonormalBasis(normal) * local);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** A direction scattered from forward by the Henyey-Greenstein phase function, which its own sampling cancels. */
|
|
25
|
+
fn sampleHenyeyGreenstein(forward: vec3<f32>, g: f32, u: vec2<f32>) -> vec3<f32> {
|
|
26
|
+
var cosine = 1.0 - 2.0 * u.x;
|
|
27
|
+
|
|
28
|
+
if (abs(g) > 1e-3) {
|
|
29
|
+
let s = (1.0 - g * g) / (1.0 - g + 2.0 * g * u.x);
|
|
30
|
+
cosine = clamp((1.0 + g * g - s * s) / (2.0 * g), -1.0, 1.0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let sine = sqrt(max(0.0, 1.0 - cosine * cosine));
|
|
34
|
+
let angle = 2.0 * PI * u.y;
|
|
35
|
+
|
|
36
|
+
return normalize(orthonormalBasis(forward) * vec3<f32>(sine * cos(angle), sine * sin(angle), cosine));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Henyey-Greenstein phase function, per steradian, for a turn whose cosine is given from the direction of travel:
|
|
41
|
+
* the density sampleHenyeyGreenstein draws with.
|
|
42
|
+
*/
|
|
43
|
+
fn henyeyGreenstein(cosine: f32, g: f32) -> f32 {
|
|
44
|
+
let denominator = 1.0 + g * g - 2.0 * g * cosine;
|
|
45
|
+
|
|
46
|
+
return (1.0 - g * g) / (4.0 * PI * denominator * sqrt(max(denominator, 1e-12)));
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
/** The unit normal of the triangle's plane; which side it faces does not matter, every emitter is two sided. */
|
|
25
50
|
fn triangleNormal(triangle: u32) -> vec3<f32> {
|
|
26
51
|
let a = triangleVertex(triangle, 0u);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sampling.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/sampling.wgsl.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"sampling.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/sampling.wgsl.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDvC,CAAC"}
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* CPU renderer follows too: roughness from green, metalness from blue, the alpha map from green, clearcoat from red,
|
|
7
7
|
* its roughness from green, sheen roughness from alpha, and so on.
|
|
8
8
|
*/
|
|
9
|
-
export declare const SURFACE_WGSL = "\nstruct Surface {\n // The shading normal, maps applied, facing the ray.\n normal: vec3<f32>,\n albedo: vec3<f32>,\n // What the surface radiates by itself: its emission, or the whole colour of a material Three does not light.\n emissive: vec3<f32>,\n // The dielectric's reflectance at normal incidence and at grazing angles, before metalness turns it to the albedo.\n specularColor: vec3<f32>,\n specularF90: f32,\n roughness: f32,\n metalness: f32,\n // How much of the surface actually covers this point: a cutout, a transparency or a discarded fragment.\n opacity: f32,\n // Three's diffuseColor.a, which blending other than normal weighs the surface by.\n alpha: f32,\n flags: u32,\n kind: u32,\n // Share of the diffuse layer that lets light through, with metal already taken out of it.\n transmission: f32,\n ior: f32,\n thickness: f32,\n dispersion: f32,\n // What a unit of distance inside the volume absorbs, per channel: zero for a volume that never darkens.\n absorption: vec3<f32>,\n // The interpolated normal facing the ray, before any map: thin walls, shadow rays and the clearcoat read this one.\n facing: vec3<f32>,\n // Whether the ray arrived from the side the normals point to, which is the outside of a closed volume.\n front: bool,\n clearcoat: f32,\n clearcoatRoughness: f32,\n clearcoatNormal: vec3<f32>,\n sheenColor: vec3<f32>,\n sheenRoughness: f32,\n iridescence: f32,\n iridescenceFresnel: vec3<f32>,\n // Anisotropic GGX: its strength, the roughness along the tangent, and the tangent frame it stretches along.\n anisotropy: f32,\n alphaT: f32,\n anisotropyT: vec3<f32>,\n anisotropyB: vec3<f32>,\n shininess: f32,\n // Phong's specular map, which scales its highlight.\n specularStrength: f32,\n gradientMap: u32,\n // The material's own environment map, which the paths leaving it see instead of the scene's.\n envMap: u32,\n envMapIntensity: f32,\n // Basic, Lambert and Phong blend their environment map over the lit colour: what the map shows here, and how much.\n legacyEnv: vec3<f32>,\n legacyAmount: f32,\n combine: u32,\n // Light a light map bakes into this point, which only the camera sees: the path traces the rest itself.\n lightMap: vec3<f32>,\n blending: u32,\n material: u32,\n};\n\n/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */\nfn triangleBarycentric(u: vec2<f32>) -> vec2<f32> {\n let root = sqrt(u.x);\n\n return vec2<f32>(1.0 - root, u.y * root);\n}\n\nfn trianglePointAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n return triangleVertex(triangle, 0u) * (1.0 - bary.x - bary.y)\n + triangleVertex(triangle, 1u) * bary.x\n + triangleVertex(triangle, 2u) * bary.y;\n}\n\nfn triangleUv(triangle: u32, bary: vec2<f32>, channel: u32) -> vec2<f32> {\n return attributeUv(triangle, 0u, channel) * (1.0 - bary.x - bary.y)\n + attributeUv(triangle, 1u, channel) * bary.x\n + attributeUv(triangle, 2u, channel) * bary.y;\n}\n\nfn triangleColor(triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n return attributeColor(triangle, 0u) * (1.0 - bary.x - bary.y)\n + attributeColor(triangle, 1u) * bary.x\n + attributeColor(triangle, 2u) * bary.y;\n}\n\n/** A map slot, sampled through the UV set the texture asks for. Missing maps read as one, so a multiply is always safe. */\nfn sampleMap(index: u32, triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n if (index == NO_TEXTURE) {\n return vec4<f32>(1.0);\n }\n\n return sampleTextureAt(index, triangleUv(triangle, bary, textureInfo(index).channel));\n}\n\n/** Whether the ray sees the side Three calls the front: the winding, which packing keeps counter-clockwise. */\nfn isFrontFace(triangle: u32, direction: vec3<f32>) -> bool {\n return dot(triangleNormal(triangle), direction) < 0.0;\n}\n\n/** Three culls the side a material does not draw. */\nfn isCulled(material: u32, triangle: u32, direction: vec3<f32>) -> bool {\n let side = materials[material].side;\n let front = isFrontFace(triangle, direction);\n\n return (side == SIDE_FRONT && !front) || (side == SIDE_BACK && front);\n}\n\n/** What the triangle radiates at this point, its emissive map included so direct and indirect light agree on it. */\nfn emissionAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n let map = sampleMap(materials[m].emissiveMap, triangle, bary);\n\n return materials[m].emissive.rgb * materials[m].emissive.w * map.rgb;\n}\n\n/** Three's material clipping planes: a point on the far side of one (or of all, with clipIntersection) is cut away. */\nfn isClipped(m: u32, point: vec3<f32>) -> bool {\n let count = materials[m].clipCount;\n\n if (count == 0u) {\n return false;\n }\n\n let intersection = (materials[m].flags & MATERIAL_CLIP_INTERSECTION) != 0u;\n var every = true;\n\n for (var i = 0u; i < count; i = i + 1u) {\n let plane = extraVec4(materials[m].clipStart + i * 4u);\n let outside = dot(plane.xyz, point) + plane.w < 0.0;\n\n if (outside && !intersection) {\n return true;\n }\n\n every = every && outside;\n }\n\n return intersection && every;\n}\n\n/** How large one pixel is at this point, in world units: what a one pixel wide line covers. */\nfn pixelFootprint(point: vec3<f32>) -> f32 {\n let scale = 2.0 / (frame.projection[1][1] * f32(frame.resolution.y));\n\n if (frame.cameraKind == 1u) {\n return scale;\n }\n\n return scale * max(-(frame.worldToCamera * vec4<f32>(point, 1.0)).z, 1e-6);\n}\n\nfn edgeDistance(point: vec3<f32>, a: vec3<f32>, b: vec3<f32>) -> f32 {\n let edge = b - a;\n\n return length(cross(point - a, edge)) / max(length(edge), 1e-20);\n}\n\n/** A wireframe draws only the edges of each triangle, one pixel wide as WebGL draws every line. */\nfn onWireframe(triangle: u32, point: vec3<f32>) -> bool {\n let a = triangleVertex(triangle, 0u);\n let b = triangleVertex(triangle, 1u);\n let c = triangleVertex(triangle, 2u);\n let nearest = min(edgeDistance(point, a, b), min(edgeDistance(point, b, c), edgeDistance(point, c, a)));\n\n return nearest <= 0.5 * pixelFootprint(point);\n}\n\n/** Three's diffuseColor.a: opacity, the colour map's alpha, the alpha map's green and the vertex alpha. */\nfn materialAlpha(m: u32, triangle: u32, bary: vec2<f32>) -> f32 {\n return materialAlphaFrom(m, triangle, bary, sampleMap(materials[m].map, triangle, bary));\n}\n\n/** The same, from the colour map's texel at this point. Multiplied in the order resolveSurface multiplies it. */\nfn materialAlphaFrom(m: u32, triangle: u32, bary: vec2<f32>, base: vec4<f32>) -> f32 {\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n alpha = alpha * triangleColor(triangle, bary).a;\n }\n\n return alpha;\n}\n\n/**\n * How much of the surface is there, from its alpha the way Three's chunks use it: alphaTest cuts, alphaHash keeps the\n * share, and only a transparent material with normal blending lets the rest through; an opaque one sets alpha to one.\n */\nfn coverageOf(m: u32, alpha: f32) -> f32 {\n if (materials[m].alphaTest > 0.0 && alpha < materials[m].alphaTest) {\n return 0.0;\n }\n\n var coverage = 1.0;\n\n if ((materials[m].flags & MATERIAL_ALPHA_HASH) != 0u) {\n coverage = clamp(alpha, 0.0, 1.0);\n }\n\n if ((materials[m].flags & MATERIAL_TRANSPARENT) != 0u && materials[m].blending == BLEND_NORMAL) {\n coverage = coverage * clamp(alpha, 0.0, 1.0);\n }\n\n return coverage;\n}\n\n/** Barycentrics on the triangle's plane of a ray, extrapolated past its edges: where a neighbouring pixel lands. */\nfn planeBarycentric(triangle: u32, origin: vec3<f32>, direction: vec3<f32>, fallback: vec2<f32>) -> vec2<f32> {\n let a = triangleVertex(triangle, 0u);\n let e1 = triangleVertex(triangle, 1u) - a;\n let e2 = triangleVertex(triangle, 2u) - a;\n let n = cross(e1, e2);\n let denominator = dot(n, direction);\n\n if (abs(denominator) < 1e-20) {\n return fallback;\n }\n\n let p = origin + direction * (dot(n, a - origin) / denominator) - a;\n let d00 = dot(e1, e1);\n let d01 = dot(e1, e2);\n let d11 = dot(e2, e2);\n let d20 = dot(p, e1);\n let d21 = dot(p, e2);\n let det = d00 * d11 - d01 * d01;\n\n if (abs(det) < 1e-30) {\n return fallback;\n }\n\n return vec2<f32>((d11 * d20 - d01 * d21) / det, (d00 * d21 - d01 * d20) / det);\n}\n\n/** The camera ray through a point given in normalized device coordinates: origin on the near plane, and its length. */\nfn ndcRay(ndc: vec2<f32>) -> array<vec4<f32>, 2> {\n let nearView = frame.inverseProjection * vec4<f32>(ndc, -1.0, 1.0);\n let farView = frame.inverseProjection * vec4<f32>(ndc, 1.0, 1.0);\n let near = (frame.cameraToWorld * vec4<f32>(nearView.xyz / nearView.w, 1.0)).xyz;\n let far = (frame.cameraToWorld * vec4<f32>(farView.xyz / farView.w, 1.0)).xyz;\n let span = far - near;\n let extent = max(length(span), 1e-20);\n\n return array<vec4<f32>, 2>(vec4<f32>(near, 0.0), vec4<f32>(span / extent, extent));\n}\n\n/** What a fragment shader knows about the point it runs at, in GL's own terms. */\nfn shaderInputAt(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> ShaderInput {\n var input: ShaderInput;\n let point = trianglePointAt(triangle, bary);\n let view = frame.worldToCamera * vec4<f32>(point, 1.0);\n let clip = frame.projection * view;\n let w = select(clip.w, 1e-20, abs(clip.w) < 1e-20);\n let ndc = clip.xyz / w;\n let size = vec2<f32>(frame.resolution);\n let pixel = (ndc.xy * 0.5 + 0.5) * size;\n let right = ndcRay(vec2<f32>(ndc.x + 2.0 / size.x, ndc.y));\n let above = ndcRay(vec2<f32>(ndc.x, ndc.y + 2.0 / size.y));\n\n input.data = materials[m].shaderData;\n input.material = m;\n input.triangle = triangle;\n input.frontFacing = isFrontFace(triangle, direction);\n input.bary = bary;\n input.baryDx = planeBarycentric(triangle, right[0].xyz, right[1].xyz, bary);\n input.baryDy = planeBarycentric(triangle, above[0].xyz, above[1].xyz, bary);\n input.fragCoord = vec4<f32>(pixel, ndc.z * 0.5 + 0.5, 1.0 / w);\n let uv = triangleUv(triangle, bary, 0u);\n input.uv = uv;\n input.fragColor = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n // A point's quad carries gl_PointCoord in its first UV set, with v running up as a texture's does.\n input.pointCoord = vec2<f32>(uv.x, 1.0 - uv.y);\n input.direction = direction;\n input.diffuseColor = vec4<f32>(1.0);\n input.normal = vec3<f32>(0.0, 0.0, 1.0);\n input.geometryNormal = vec3<f32>(0.0, 0.0, 1.0);\n input.roughness = 1.0;\n input.metalness = 0.0;\n input.emissive = vec3<f32>(0.0);\n input.viewPosition = -view.xyz;\n\n return input;\n}\n\n/** World to view space for a direction, which is the space Three's shaders keep their normals in. */\nfn toView(direction: vec3<f32>) -> vec3<f32> {\n return normalize((frame.worldToCamera * vec4<f32>(direction, 0.0)).xyz);\n}\n\nfn toWorld(direction: vec3<f32>) -> vec3<f32> {\n return normalize((frame.cameraToWorld * vec4<f32>(direction, 0.0)).xyz);\n}\n\n/**\n * How much of the surface covers this point for a shadow ray: the same alpha, cut, clipping and wireframe the camera\n * sees, and a shader's own discard. Blending other than normal is handled by the caller.\n */\nfn coverageAt(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> f32 {\n let m = materialIndexOf(triangle);\n let flags = materials[m].flags;\n\n if (materials[m].clipCount > 0u || (flags & MATERIAL_WIREFRAME) != 0u) {\n let point = trianglePointAt(triangle, bary);\n\n if (isClipped(m, point)) {\n return 0.0;\n }\n\n if ((flags & MATERIAL_WIREFRAME) != 0u && !onWireframe(triangle, point)) {\n return 0.0;\n }\n }\n\n var alpha = materialAlpha(m, triangle, bary);\n\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(materials[m].color.rgb, alpha);\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n return 0.0;\n }\n\n alpha = select(output.color.a, output.diffuseColor.a, (flags & MATERIAL_PATCHED) != 0u);\n }\n\n return coverageOf(m, alpha);\n}\n\n/** A tangent frame from the triangle's own UVs, which is what Three's tangents amount to for a flat triangle. */\nstruct TangentFrame {\n tangent: vec3<f32>,\n bitangent: vec3<f32>,\n valid: bool,\n};\n\nfn tangentFrame(triangle: u32, channel: u32, normal: vec3<f32>) -> TangentFrame {\n var tangents: TangentFrame;\n tangents.valid = false;\n\n let p0 = triangleVertex(triangle, 0u);\n let edge1 = triangleVertex(triangle, 1u) - p0;\n let edge2 = triangleVertex(triangle, 2u) - p0;\n let uv0 = attributeUv(triangle, 0u, channel);\n let delta1 = attributeUv(triangle, 1u, channel) - uv0;\n let delta2 = attributeUv(triangle, 2u, channel) - uv0;\n let determinant = delta1.x * delta2.y - delta2.x * delta1.y;\n\n // A triangle with no UV area has no tangent frame to offer.\n if (abs(determinant) < 1e-16) {\n let basis = orthonormalBasis(normal);\n tangents.tangent = basis[0];\n tangents.bitangent = basis[1];\n\n return tangents;\n }\n\n let raw = (edge1 * delta2.y - edge2 * delta1.y) / determinant;\n // Made perpendicular to the shading normal, so a map tilts the interpolated normal rather than the flat one.\n let tangent = normalize(raw - normal * dot(normal, raw));\n\n if (!all(tangent == tangent)) {\n let basis = orthonormalBasis(normal);\n tangents.tangent = basis[0];\n tangents.bitangent = basis[1];\n\n return tangents;\n }\n\n tangents.tangent = tangent;\n tangents.bitangent = cross(normal, tangent) * select(-1.0, 1.0, determinant >= 0.0);\n tangents.valid = true;\n\n return tangents;\n}\n\n/** A tangent space normal map over a normal, scaled the way Three's normalScale scales it. */\nfn tangentNormal(index: u32, triangle: u32, bary: vec2<f32>, normal: vec3<f32>, scale: vec2<f32>) -> vec3<f32> {\n if (index == NO_TEXTURE) {\n return normal;\n }\n\n let channel = textureInfo(index).channel;\n let tangents = tangentFrame(triangle, channel, normal);\n\n if (!tangents.valid) {\n return normal;\n }\n\n let texel = sampleTextureAt(index, triangleUv(triangle, bary, channel)).xyz * 2.0 - 1.0;\n let mapped = normalize(\n tangents.tangent * (texel.x * scale.x)\n + tangents.bitangent * (texel.y * scale.y)\n + normal * texel.z);\n\n return select(mapped, normal, !all(mapped == mapped));\n}\n\n/** The normal the material asks for: an object space or tangent space normal map, or else a bump map. */\nfn mappedNormal(m: u32, triangle: u32, bary: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {\n let normalMap = materials[m].normalMap;\n\n if (normalMap != NO_TEXTURE && (materials[m].flags & MATERIAL_OBJECT_NORMAL_MAP) != 0u) {\n let texel = sampleMap(normalMap, triangle, bary).xyz * 2.0 - 1.0;\n let world = normalize(vec3<f32>(\n dot(materials[m].normalRow0.xyz, texel),\n dot(materials[m].normalRow1.xyz, texel),\n dot(materials[m].normalRow2.xyz, texel)));\n\n // Seen from behind, a double sided surface turns its map around with it.\n return select(world, -world, dot(world, normal) < 0.0);\n }\n\n if (normalMap != NO_TEXTURE) {\n return tangentNormal(normalMap, triangle, bary, normal, vec2<f32>(materials[m].normalScaleX, materials[m].normalScaleY));\n }\n\n let bumpMap = materials[m].bumpMap;\n\n if (bumpMap != NO_TEXTURE) {\n // The CPU renderer's bump: the height's change over one texel, along the tangent frame of the map's UVs.\n let info = textureInfo(bumpMap);\n let channel = info.channel;\n let tangents = tangentFrame(triangle, channel, normal);\n let uv = triangleUv(triangle, bary, channel);\n let du = 1.0 / f32(max(info.size.x, 1u));\n let dv = 1.0 / f32(max(info.size.y, 1u));\n let scale = materials[m].bumpScale;\n let height = sampleTextureAt(bumpMap, uv).x;\n let dx = (sampleTextureAt(bumpMap, uv + vec2<f32>(du, 0.0)).x - height) * scale;\n let dy = (sampleTextureAt(bumpMap, uv + vec2<f32>(0.0, dv)).x - height) * scale;\n let bumped = normalize(normal - (tangents.tangent * dx + tangents.bitangent * dy));\n\n return select(bumped, normal, !all(bumped == bumped));\n }\n\n return normal;\n}\n\n/** The interpolated vertex normal as the mesh authored it, pointing out of a closed volume. */\nfn authoredNormal(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let base = triangle * 9u;\n let n0 = vec3<f32>(attributes[base], attributes[base + 1u], attributes[base + 2u]);\n let n1 = vec3<f32>(attributes[base + 3u], attributes[base + 4u], attributes[base + 5u]);\n let n2 = vec3<f32>(attributes[base + 6u], attributes[base + 7u], attributes[base + 8u]);\n\n return normalize(n0 * (1.0 - bary.x - bary.y) + n1 * bary.x + n2 * bary.y);\n}\n\n/** The interpolated vertex normal, or the face's for flat shading, turned to face the ray. */\nfn interpolatedNormal(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n var normal = authoredNormal(triangle, bary);\n\n if ((materials[m].flags & MATERIAL_FLAT) != 0u || !all(normal == normal)) {\n normal = triangleNormal(triangle);\n }\n\n return select(normal, -normal, dot(normal, direction) > 0.0);\n}\n\n/** Three's transmission at this point: the factor, its map's red channel, and none of it on metal. */\nfn transmissionAt(m: u32, triangle: u32, bary: vec2<f32>, metalness: f32) -> f32 {\n if (materials[m].transmission <= 0.0) {\n return 0.0;\n }\n\n let map = sampleMap(materials[m].transmissionMap, triangle, bary).r;\n\n return clamp(materials[m].transmission * map, 0.0, 1.0) * (1.0 - metalness);\n}\n\n/** Beer-Lambert: the colour reached at the attenuation distance fixes how fast each channel fades. */\nfn absorptionOf(m: u32) -> vec3<f32> {\n let attenuation = materials[m].attenuation;\n\n if (attenuation.w <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n return -log(max(attenuation.rgb, vec3<f32>(1e-6))) / attenuation.w;\n}\n\n/** Three's evalSensitivity: the XYZ response of a thin film, Fourier-fitted, in linear sRGB. */\nfn filmSensitivity(opd: f32, shift: vec3<f32>) -> vec3<f32> {\n let phase = 2.0 * PI * opd * 1.0e-9;\n let value = vec3<f32>(5.4856e-13, 4.4201e-13, 5.2481e-13);\n let position = vec3<f32>(1.6810e+06, 1.7953e+06, 2.2084e+06);\n let variance = vec3<f32>(4.3278e+09, 9.3046e+09, 6.6121e+09);\n var xyz = value * sqrt(2.0 * PI * variance) * cos(position * phase + shift) * exp(-phase * phase * variance);\n\n xyz.x = xyz.x + 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x)\n * exp(-4.5282e+09 * phase * phase);\n xyz = xyz / 1.0685e-7;\n\n return mat3x3<f32>(\n vec3<f32>(3.2404542, -0.9692660, 0.0556434),\n vec3<f32>(-1.5371385, 1.8760108, -0.2040259),\n vec3<f32>(-0.4985314, 0.0415560, 1.0572252)) * xyz;\n}\n\nfn schlick3(f0: vec3<f32>, f90: f32, cosine: f32) -> vec3<f32> {\n let t = 1.0 - cosine;\n let k = t * t * t * t * t;\n\n return f0 + (vec3<f32>(f90) - f0) * k;\n}\n\n/** Three's evalIridescence (Belcour and Barla 2017): the Fresnel of a thin film over a base of reflectance baseF0. */\nfn evalIridescence(filmIor: f32, cosTheta1: f32, thickness: f32, baseF0: vec3<f32>) -> vec3<f32> {\n let ior = mix(1.0, filmIor, smoothstep(0.0, 0.03, thickness));\n let sinTheta2Sq = (1.0 / ior) * (1.0 / ior) * (1.0 - cosTheta1 * cosTheta1);\n let cosTheta2Sq = 1.0 - sinTheta2Sq;\n\n if (cosTheta2Sq < 0.0) {\n return vec3<f32>(1.0);\n }\n\n let cosTheta2 = sqrt(cosTheta2Sq);\n let r0 = ((ior - 1.0) / (ior + 1.0)) * ((ior - 1.0) / (ior + 1.0));\n let r12 = r0 + (1.0 - r0) * pow(1.0 - cosTheta1, 5.0);\n let t121 = 1.0 - r12;\n let phi12 = select(0.0, PI, ior < 1.0);\n let phi21 = PI - phi12;\n let root = sqrt(clamp(baseF0, vec3<f32>(0.0), vec3<f32>(0.9999)));\n let baseIor = (vec3<f32>(1.0) + root) / (vec3<f32>(1.0) - root);\n let r1 = ((baseIor - vec3<f32>(ior)) / (baseIor + vec3<f32>(ior))) * ((baseIor - vec3<f32>(ior)) / (baseIor + vec3<f32>(ior)));\n let r23 = schlick3(r1, 1.0, cosTheta2);\n let phi23 = select(vec3<f32>(0.0), vec3<f32>(PI), baseIor < vec3<f32>(ior));\n let opd = 2.0 * ior * thickness * cosTheta2;\n let phi = vec3<f32>(phi21) + phi23;\n let r123 = clamp(r12 * r23, vec3<f32>(1e-5), vec3<f32>(0.9999));\n let r123Root = sqrt(r123);\n let rs = t121 * t121 * r23 / (vec3<f32>(1.0) - r123);\n var intensity = vec3<f32>(r12) + rs;\n var cm = rs - vec3<f32>(t121);\n\n for (var m = 1; m <= 2; m = m + 1) {\n cm = cm * r123Root;\n intensity = intensity + cm * 2.0 * filmSensitivity(f32(m) * opd, f32(m) * phi);\n }\n\n return max(intensity, vec3<f32>(0.0));\n}\n\n/** Three's toon ramp: the gradient map across dot(n, l) from -1 to 1, or a single step at 0.7 without one. */\nfn toonRamp(gradientMap: u32, cosine: f32) -> f32 {\n let coordinate = cosine * 0.5 + 0.5;\n\n if (gradientMap == NO_TEXTURE) {\n return select(0.7, 1.0, coordinate >= 0.7);\n }\n\n return sampleTextureRaw(gradientMap, vec2<f32>(coordinate, 0.0)).r;\n}\n\n/**\n * Whether a material's coverage can be told from its maps alone, before the rest of the surface is read: no shader that\n * might rewrite the alpha or discard, normal blending, and something that makes it partial at all.\n */\nfn coverageFirst(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return materials[m].program == NO_PROGRAM\n && (blending == BLEND_NORMAL || blending == BLEND_NONE)\n && (materials[m].alphaTest > 0.0 || (flags & (MATERIAL_ALPHA_HASH | MATERIAL_TRANSPARENT)) != 0u);\n}\n\n/** Everything the lobes need about one point of one triangle. */\nfn resolveSurface(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {\n let m = materialIndexOf(triangle);\n\n return resolveSurfaceFrom(triangle, bary, direction, sampleMap(materials[m].map, triangle, bary));\n}\n\n/** The same, from the colour map's texel at this point, for a caller that already read it. */\nfn resolveSurfaceFrom(triangle: u32, bary: vec2<f32>, direction: vec3<f32>, base: vec4<f32>) -> Surface {\n let m = materialIndexOf(triangle);\n let flags = materials[m].flags;\n let kind = materials[m].kind;\n let facing = interpolatedNormal(m, triangle, bary, direction);\n let view = -direction;\n var surface: Surface;\n\n surface.material = m;\n surface.flags = flags;\n surface.kind = kind;\n surface.blending = materials[m].blending;\n surface.facing = facing;\n surface.front = dot(authoredNormal(triangle, bary), direction) < 0.0;\n surface.normal = mappedNormal(m, triangle, bary, facing);\n\n // A normal map may tilt past the horizon, where the surface would be lit from behind itself.\n if (dot(surface.normal, view) <= 0.0) {\n surface.normal = facing;\n }\n\n var albedo = materials[m].color.rgb * base.rgb;\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((flags & MATERIAL_VERTEX_COLORS) != 0u) {\n let color = triangleColor(triangle, bary);\n albedo = albedo * color.rgb;\n alpha = alpha * color.a;\n }\n\n var roughness = materials[m].specular.w * sampleMap(materials[m].roughnessMap, triangle, bary).g;\n var metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n var emissive = emissionAt(triangle, bary);\n\n surface.opacity = 1.0;\n\n // A built-in material's hooks run on its values before lighting, in Three's view space; a shader material's output\n // is the colour itself.\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(albedo, alpha);\n input.normal = toView(surface.normal);\n input.geometryNormal = toView(facing);\n input.roughness = roughness;\n input.metalness = metalness;\n input.emissive = emissive;\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n surface.opacity = 0.0;\n }\n\n if ((flags & MATERIAL_PATCHED) != 0u) {\n albedo = output.diffuseColor.rgb;\n alpha = output.diffuseColor.a;\n roughness = output.roughness;\n metalness = output.metalness;\n emissive = output.emissive;\n let normal = toWorld(output.normal);\n\n if (all(normal == normal)) {\n surface.normal = select(normal, -normal, dot(normal, view) < 0.0);\n }\n } else {\n albedo = output.color.rgb;\n alpha = output.color.a;\n }\n }\n\n surface.opacity = surface.opacity * coverageOf(m, alpha);\n surface.alpha = alpha;\n surface.albedo = albedo;\n surface.roughness = clamp(roughness, 0.0525, 1.0);\n surface.metalness = clamp(metalness, 0.0, 1.0);\n surface.emissive = emissive;\n surface.transmission = transmissionAt(m, triangle, bary, surface.metalness);\n surface.ior = max(materials[m].ior, 1.0);\n surface.thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n surface.dispersion = materials[m].dispersion;\n surface.absorption = absorptionOf(m);\n surface.shininess = materials[m].shininess;\n surface.specularStrength = sampleMap(materials[m].specularMap, triangle, bary).r;\n surface.gradientMap = materials[m].gradientMap;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = materials[m].envMapIntensity;\n surface.legacyAmount = 0.0;\n surface.legacyEnv = vec3<f32>(0.0);\n surface.combine = materials[m].combine;\n surface.lightMap = vec3<f32>(0.0);\n surface.clearcoat = 0.0;\n surface.iridescence = 0.0;\n surface.anisotropy = 0.0;\n surface.sheenColor = vec3<f32>(0.0);\n surface.specularColor = materials[m].specular.rgb;\n surface.specularF90 = 1.0;\n\n if ((flags & MATERIAL_PBR) != 0u) {\n if ((flags & MATERIAL_SPECULAR) != 0u) {\n // A physical material: the index of refraction, the specular colour and intensity and their maps.\n let intensity = materials[m].specularIntensity * sampleMap(materials[m].specularIntensityMap, triangle, bary).a;\n let color = materials[m].specular.rgb * sampleMap(materials[m].specularColorMap, triangle, bary).rgb;\n\n surface.specularColor = min(materials[m].dielectric * color, vec3<f32>(1.0)) * intensity;\n surface.specularF90 = mix(intensity, 1.0, surface.metalness);\n } else {\n // Three's standard material has no index of refraction and reflects 0.04.\n surface.specularColor = vec3<f32>(0.04);\n }\n\n surface.envMap = materials[m].envMap;\n\n if (materials[m].clearcoat > 0.0) {\n surface.clearcoat = clamp(materials[m].clearcoat * sampleMap(materials[m].clearcoatMap, triangle, bary).r, 0.0, 1.0);\n surface.clearcoatRoughness = clamp(\n materials[m].clearcoatRoughness * sampleMap(materials[m].clearcoatRoughnessMap, triangle, bary).g, 0.0525, 1.0);\n // The coat ignores the base's normal map: it has its own, over the interpolated normal.\n surface.clearcoatNormal = tangentNormal(materials[m].clearcoatNormalMap, triangle, bary, facing,\n vec2<f32>(materials[m].clearcoatNormalScaleX, materials[m].clearcoatNormalScaleY));\n\n if (dot(surface.clearcoatNormal, view) <= 0.0) {\n surface.clearcoatNormal = facing;\n }\n }\n\n if (any(materials[m].sheen.rgb > vec3<f32>(0.0))) {\n let map = sampleMap(materials[m].sheenRoughnessMap, triangle, bary).a;\n surface.sheenColor = materials[m].sheen.rgb * sampleMap(materials[m].sheenColorMap, triangle, bary).rgb;\n surface.sheenRoughness = clamp(materials[m].sheen.w, 0.0001, 1.0) * map;\n }\n\n if (materials[m].iridescence > 0.0) {\n var thickness = materials[m].iridescenceThicknessMax;\n\n if (materials[m].iridescenceThicknessMap != NO_TEXTURE) {\n thickness = mix(materials[m].iridescenceThicknessMin, materials[m].iridescenceThicknessMax,\n sampleMap(materials[m].iridescenceThicknessMap, triangle, bary).g);\n }\n\n if (thickness > 0.0) {\n surface.iridescence = clamp(\n materials[m].iridescence * sampleMap(materials[m].iridescenceMap, triangle, bary).r, 0.0, 1.0);\n\n let nv = clamp(dot(surface.normal, view), 0.0, 1.0);\n surface.iridescenceFresnel = mix(\n evalIridescence(materials[m].iridescenceIor, nv, thickness, surface.specularColor),\n evalIridescence(materials[m].iridescenceIor, nv, thickness, albedo),\n surface.metalness);\n }\n }\n\n if (materials[m].anisotropy != 0.0) {\n let rotation = materials[m].anisotropyRotation;\n var vector = vec2<f32>(cos(rotation), sin(rotation)) * materials[m].anisotropy;\n let channel = select(0u, textureInfo(materials[m].anisotropyMap).channel, materials[m].anisotropyMap != NO_TEXTURE);\n let tangents = tangentFrame(triangle, channel, surface.normal);\n\n if (materials[m].anisotropyMap != NO_TEXTURE) {\n let polar = sampleMap(materials[m].anisotropyMap, triangle, bary).rgb;\n let turn = mat2x2<f32>(vector.x, vector.y, -vector.y, vector.x);\n vector = turn * normalize(2.0 * polar.rg - vec2<f32>(1.0)) * polar.b;\n }\n\n let strength = length(vector);\n let along = select(vector / max(strength, 1e-20), vec2<f32>(1.0, 0.0), strength == 0.0);\n\n surface.anisotropy = clamp(strength, 0.0, 1.0);\n surface.alphaT = mix(surface.roughness * surface.roughness, 1.0, surface.anisotropy * surface.anisotropy);\n surface.anisotropyT = tangents.tangent * along.x + tangents.bitangent * along.y;\n surface.anisotropyB = tangents.bitangent * along.x - tangents.tangent * along.y;\n }\n } else if (materials[m].envMap != NO_TEXTURE\n && (kind == KIND_BASIC || kind == KIND_LAMBERT || kind == KIND_PHONG)) {\n // Basic, Lambert and Phong blend their own map over the lit colour, mirrored or bent the way the map says.\n let info = textureInfo(materials[m].envMap);\n let bent = refract(direction, surface.normal, materials[m].refractionRatio);\n let lookup = select(reflect(direction, surface.normal), bent, (info.flags & 16u) != 0u && any(bent != vec3<f32>(0.0)));\n\n surface.legacyEnv = sampleEquirect(materials[m].envMap, lookup).rgb * materials[m].envMapIntensity;\n surface.legacyAmount = clamp(materials[m].reflectivity * surface.specularStrength, 0.0, 1.0);\n }\n\n if (materials[m].lightMap != NO_TEXTURE) {\n let baked = sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity;\n surface.lightMap = baked * albedo * (1.0 - surface.metalness) / PI;\n }\n\n // A material Three does not light reads as its own colour, which in a path tracer is the same as emitting it.\n if ((flags & MATERIAL_LIT) == 0u) {\n var color = albedo;\n\n if (kind == KIND_NORMAL) {\n color = toView(surface.normal) * 0.5 + 0.5;\n } else if (kind == KIND_DEPTH) {\n let clip = frame.projection * (frame.worldToCamera * vec4<f32>(trianglePointAt(triangle, bary), 1.0));\n color = vec3<f32>(1.0 - (clip.z / clip.w * 0.5 + 0.5));\n } else if (kind == KIND_MATCAP) {\n let point = trianglePointAt(triangle, bary);\n let viewDirection = normalize(-(frame.worldToCamera * vec4<f32>(point, 1.0)).xyz);\n let x = normalize(vec3<f32>(viewDirection.z, 0.0, -viewDirection.x));\n let y = cross(viewDirection, x);\n let normal = toView(surface.normal);\n let uv = vec2<f32>(dot(x, normal), dot(y, normal)) * 0.495 + 0.5;\n var matcap = vec3<f32>(mix(0.2, 0.8, uv.y));\n\n if (materials[m].matcap != NO_TEXTURE) {\n matcap = sampleTextureRaw(materials[m].matcap, uv).rgb;\n }\n\n color = albedo * matcap;\n } else if (kind == KIND_BASIC && materials[m].lightMap != NO_TEXTURE) {\n // A basic material with a light map shows the map's light instead of full brightness.\n color = albedo * sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity / PI;\n }\n\n if (surface.legacyAmount > 0.0) {\n let amount = surface.legacyAmount;\n\n if (surface.combine == 1u) {\n color = mix(color, surface.legacyEnv, amount);\n } else if (surface.combine == 2u) {\n color = color + surface.legacyEnv * amount;\n } else {\n color = mix(color, color * surface.legacyEnv, amount);\n }\n\n surface.legacyAmount = 0.0;\n }\n\n surface.emissive = surface.emissive + color;\n surface.lightMap = vec3<f32>(0.0);\n }\n\n return surface;\n}\n";
|
|
9
|
+
export declare const SURFACE_WGSL = "\nstruct Surface {\n // The shading normal, maps applied, facing the ray.\n normal: vec3<f32>,\n albedo: vec3<f32>,\n // What the surface radiates by itself: its emission, or the whole colour of a material Three does not light.\n emissive: vec3<f32>,\n // The dielectric's reflectance at normal incidence and at grazing angles, before metalness turns it to the albedo.\n specularColor: vec3<f32>,\n specularF90: f32,\n roughness: f32,\n metalness: f32,\n // How much of the surface actually covers this point: a cutout, a transparency or a discarded fragment.\n opacity: f32,\n // Three's diffuseColor.a, which blending other than normal weighs the surface by.\n alpha: f32,\n flags: u32,\n kind: u32,\n // Share of the diffuse layer that lets light through, with metal already taken out of it.\n transmission: f32,\n ior: f32,\n thickness: f32,\n dispersion: f32,\n // What a unit of distance inside the volume absorbs, per channel: zero for a volume that never darkens.\n absorption: vec3<f32>,\n // The interpolated normal facing the ray, before any map: thin walls, shadow rays and the clearcoat read this one.\n facing: vec3<f32>,\n // Whether the ray arrived from the side the normals point to, which is the outside of a closed volume.\n front: bool,\n clearcoat: f32,\n clearcoatRoughness: f32,\n clearcoatNormal: vec3<f32>,\n sheenColor: vec3<f32>,\n sheenRoughness: f32,\n iridescence: f32,\n iridescenceFresnel: vec3<f32>,\n // Anisotropic GGX: its strength, the roughness along the tangent, and the tangent frame it stretches along.\n anisotropy: f32,\n alphaT: f32,\n anisotropyT: vec3<f32>,\n anisotropyB: vec3<f32>,\n shininess: f32,\n // Phong's specular map, which scales its highlight.\n specularStrength: f32,\n gradientMap: u32,\n // The material's own environment map, which the paths leaving it see instead of the scene's.\n envMap: u32,\n envMapIntensity: f32,\n // Basic, Lambert and Phong blend their environment map over the lit colour: what the map shows here, and how much.\n legacyEnv: vec3<f32>,\n legacyAmount: f32,\n combine: u32,\n // Light a light map bakes into this point, which only the camera sees: the path traces the rest itself.\n lightMap: vec3<f32>,\n blending: u32,\n material: u32,\n // Share of the diffuse layer that scatters inside the material instead of at its surface.\n subsurface: f32,\n};\n\n/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */\nfn triangleBarycentric(u: vec2<f32>) -> vec2<f32> {\n let root = sqrt(u.x);\n\n return vec2<f32>(1.0 - root, u.y * root);\n}\n\nfn trianglePointAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n return triangleVertex(triangle, 0u) * (1.0 - bary.x - bary.y)\n + triangleVertex(triangle, 1u) * bary.x\n + triangleVertex(triangle, 2u) * bary.y;\n}\n\nfn triangleUv(triangle: u32, bary: vec2<f32>, channel: u32) -> vec2<f32> {\n return attributeUv(triangle, 0u, channel) * (1.0 - bary.x - bary.y)\n + attributeUv(triangle, 1u, channel) * bary.x\n + attributeUv(triangle, 2u, channel) * bary.y;\n}\n\nfn triangleColor(triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n return attributeColor(triangle, 0u) * (1.0 - bary.x - bary.y)\n + attributeColor(triangle, 1u) * bary.x\n + attributeColor(triangle, 2u) * bary.y;\n}\n\n/** A map slot, sampled through the UV set the texture asks for. Missing maps read as one, so a multiply is always safe. */\nfn sampleMap(index: u32, triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n if (index == NO_TEXTURE) {\n return vec4<f32>(1.0);\n }\n\n return sampleTextureAt(index, triangleUv(triangle, bary, textureInfo(index).channel));\n}\n\n/** Whether the ray sees the side Three calls the front: the winding, which packing keeps counter-clockwise. */\nfn isFrontFace(triangle: u32, direction: vec3<f32>) -> bool {\n return dot(triangleNormal(triangle), direction) < 0.0;\n}\n\n/** Three culls the side a material does not draw. */\nfn isCulled(material: u32, triangle: u32, direction: vec3<f32>) -> bool {\n let side = materials[material].side;\n let front = isFrontFace(triangle, direction);\n\n return (side == SIDE_FRONT && !front) || (side == SIDE_BACK && front);\n}\n\n/** What the triangle radiates at this point, its emissive map included so direct and indirect light agree on it. */\nfn emissionAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n // An unlit material emits its colour as the camera would see it, so a bounce ray finds what a light sample counted.\n if ((materials[m].flags & MATERIAL_UNLIT_LIGHT) != 0u) {\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n }\n\n let map = sampleMap(materials[m].emissiveMap, triangle, bary);\n\n return materials[m].emissive.rgb * materials[m].emissive.w * map.rgb;\n}\n\n/** Three's material clipping planes: a point on the far side of one (or of all, with clipIntersection) is cut away. */\nfn isClipped(m: u32, point: vec3<f32>) -> bool {\n let count = materials[m].clipCount;\n\n if (count == 0u) {\n return false;\n }\n\n let intersection = (materials[m].flags & MATERIAL_CLIP_INTERSECTION) != 0u;\n var every = true;\n\n for (var i = 0u; i < count; i = i + 1u) {\n let plane = extraVec4(materials[m].clipStart + i * 4u);\n let outside = dot(plane.xyz, point) + plane.w < 0.0;\n\n if (outside && !intersection) {\n return true;\n }\n\n every = every && outside;\n }\n\n return intersection && every;\n}\n\n/** How large one pixel is at this point, in world units: what a one pixel wide line covers. */\nfn pixelFootprint(point: vec3<f32>) -> f32 {\n let scale = 2.0 / (frame.projection[1][1] * f32(frame.resolution.y));\n\n if (frame.cameraKind == 1u) {\n return scale;\n }\n\n return scale * max(-(frame.worldToCamera * vec4<f32>(point, 1.0)).z, 1e-6);\n}\n\nfn edgeDistance(point: vec3<f32>, a: vec3<f32>, b: vec3<f32>) -> f32 {\n let edge = b - a;\n\n return length(cross(point - a, edge)) / max(length(edge), 1e-20);\n}\n\n/** A wireframe draws only the edges of each triangle, one pixel wide as WebGL draws every line. */\nfn onWireframe(triangle: u32, point: vec3<f32>) -> bool {\n let a = triangleVertex(triangle, 0u);\n let b = triangleVertex(triangle, 1u);\n let c = triangleVertex(triangle, 2u);\n let nearest = min(edgeDistance(point, a, b), min(edgeDistance(point, b, c), edgeDistance(point, c, a)));\n\n return nearest <= 0.5 * pixelFootprint(point);\n}\n\n/** Three's diffuseColor.a: opacity, the colour map's alpha, the alpha map's green and the vertex alpha. */\nfn materialAlpha(m: u32, triangle: u32, bary: vec2<f32>) -> f32 {\n return materialAlphaFrom(m, triangle, bary, sampleMap(materials[m].map, triangle, bary));\n}\n\n/** The same, from the colour map's texel at this point. Multiplied in the order resolveSurface multiplies it. */\nfn materialAlphaFrom(m: u32, triangle: u32, bary: vec2<f32>, base: vec4<f32>) -> f32 {\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n alpha = alpha * triangleColor(triangle, bary).a;\n }\n\n return alpha;\n}\n\n/**\n * How much of the surface is there, from its alpha the way Three's chunks use it: alphaTest cuts, alphaHash keeps the\n * share, and only a transparent material with normal blending lets the rest through; an opaque one sets alpha to one.\n */\nfn coverageOf(m: u32, alpha: f32) -> f32 {\n if (materials[m].alphaTest > 0.0 && alpha < materials[m].alphaTest) {\n return 0.0;\n }\n\n var coverage = 1.0;\n\n if ((materials[m].flags & MATERIAL_ALPHA_HASH) != 0u) {\n coverage = clamp(alpha, 0.0, 1.0);\n }\n\n if ((materials[m].flags & MATERIAL_TRANSPARENT) != 0u && materials[m].blending == BLEND_NORMAL) {\n coverage = coverage * clamp(alpha, 0.0, 1.0);\n }\n\n return coverage;\n}\n\n/** Barycentrics on the triangle's plane of a ray, extrapolated past its edges: where a neighbouring pixel lands. */\nfn planeBarycentric(triangle: u32, origin: vec3<f32>, direction: vec3<f32>, fallback: vec2<f32>) -> vec2<f32> {\n let a = triangleVertex(triangle, 0u);\n let e1 = triangleVertex(triangle, 1u) - a;\n let e2 = triangleVertex(triangle, 2u) - a;\n let n = cross(e1, e2);\n let denominator = dot(n, direction);\n\n if (abs(denominator) < 1e-20) {\n return fallback;\n }\n\n let p = origin + direction * (dot(n, a - origin) / denominator) - a;\n let d00 = dot(e1, e1);\n let d01 = dot(e1, e2);\n let d11 = dot(e2, e2);\n let d20 = dot(p, e1);\n let d21 = dot(p, e2);\n let det = d00 * d11 - d01 * d01;\n\n if (abs(det) < 1e-30) {\n return fallback;\n }\n\n return vec2<f32>((d11 * d20 - d01 * d21) / det, (d00 * d21 - d01 * d20) / det);\n}\n\n/** The camera ray through a point given in normalized device coordinates: origin on the near plane, and its length. */\nfn ndcRay(ndc: vec2<f32>) -> array<vec4<f32>, 2> {\n let nearView = frame.inverseProjection * vec4<f32>(ndc, -1.0, 1.0);\n let farView = frame.inverseProjection * vec4<f32>(ndc, 1.0, 1.0);\n let near = (frame.cameraToWorld * vec4<f32>(nearView.xyz / nearView.w, 1.0)).xyz;\n let far = (frame.cameraToWorld * vec4<f32>(farView.xyz / farView.w, 1.0)).xyz;\n let span = far - near;\n let extent = max(length(span), 1e-20);\n\n return array<vec4<f32>, 2>(vec4<f32>(near, 0.0), vec4<f32>(span / extent, extent));\n}\n\n/** What a fragment shader knows about the point it runs at, in GL's own terms. */\nfn shaderInputAt(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> ShaderInput {\n var input: ShaderInput;\n let point = trianglePointAt(triangle, bary);\n let view = frame.worldToCamera * vec4<f32>(point, 1.0);\n let clip = frame.projection * view;\n let w = select(clip.w, 1e-20, abs(clip.w) < 1e-20);\n let ndc = clip.xyz / w;\n let size = vec2<f32>(frame.resolution);\n let pixel = (ndc.xy * 0.5 + 0.5) * size;\n let right = ndcRay(vec2<f32>(ndc.x + 2.0 / size.x, ndc.y));\n let above = ndcRay(vec2<f32>(ndc.x, ndc.y + 2.0 / size.y));\n\n input.data = materials[m].shaderData;\n input.material = m;\n input.triangle = triangle;\n input.frontFacing = isFrontFace(triangle, direction);\n input.bary = bary;\n input.baryDx = planeBarycentric(triangle, right[0].xyz, right[1].xyz, bary);\n input.baryDy = planeBarycentric(triangle, above[0].xyz, above[1].xyz, bary);\n input.fragCoord = vec4<f32>(pixel, ndc.z * 0.5 + 0.5, 1.0 / w);\n let uv = triangleUv(triangle, bary, 0u);\n input.uv = uv;\n input.fragColor = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n // A point's quad carries gl_PointCoord in its first UV set, with v running up as a texture's does.\n input.pointCoord = vec2<f32>(uv.x, 1.0 - uv.y);\n input.direction = direction;\n input.diffuseColor = vec4<f32>(1.0);\n input.normal = vec3<f32>(0.0, 0.0, 1.0);\n input.geometryNormal = vec3<f32>(0.0, 0.0, 1.0);\n input.roughness = 1.0;\n input.metalness = 0.0;\n input.emissive = vec3<f32>(0.0);\n input.viewPosition = -view.xyz;\n\n return input;\n}\n\n/** World to view space for a direction, which is the space Three's shaders keep their normals in. */\nfn toView(direction: vec3<f32>) -> vec3<f32> {\n return normalize((frame.worldToCamera * vec4<f32>(direction, 0.0)).xyz);\n}\n\nfn toWorld(direction: vec3<f32>) -> vec3<f32> {\n return normalize((frame.cameraToWorld * vec4<f32>(direction, 0.0)).xyz);\n}\n\n/**\n * How much of the surface covers this point for a shadow ray: the same alpha, cut, clipping and wireframe the camera\n * sees, and a shader's own discard. Blending other than normal is handled by the caller.\n */\nfn coverageAt(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> f32 {\n let m = materialIndexOf(triangle);\n let flags = materials[m].flags;\n\n if (materials[m].clipCount > 0u || (flags & MATERIAL_WIREFRAME) != 0u) {\n let point = trianglePointAt(triangle, bary);\n\n if (isClipped(m, point)) {\n return 0.0;\n }\n\n if ((flags & MATERIAL_WIREFRAME) != 0u && !onWireframe(triangle, point)) {\n return 0.0;\n }\n }\n\n var alpha = materialAlpha(m, triangle, bary);\n\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(materials[m].color.rgb, alpha);\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n return 0.0;\n }\n\n alpha = select(output.color.a, output.diffuseColor.a, (flags & MATERIAL_PATCHED) != 0u);\n }\n\n return coverageOf(m, alpha);\n}\n\n/** A tangent frame from the triangle's own UVs, which is what Three's tangents amount to for a flat triangle. */\nstruct TangentFrame {\n tangent: vec3<f32>,\n bitangent: vec3<f32>,\n valid: bool,\n};\n\nfn tangentFrame(triangle: u32, channel: u32, normal: vec3<f32>) -> TangentFrame {\n var tangents: TangentFrame;\n tangents.valid = false;\n\n let p0 = triangleVertex(triangle, 0u);\n let edge1 = triangleVertex(triangle, 1u) - p0;\n let edge2 = triangleVertex(triangle, 2u) - p0;\n let uv0 = attributeUv(triangle, 0u, channel);\n let delta1 = attributeUv(triangle, 1u, channel) - uv0;\n let delta2 = attributeUv(triangle, 2u, channel) - uv0;\n let determinant = delta1.x * delta2.y - delta2.x * delta1.y;\n\n // A triangle with no UV area has no tangent frame to offer.\n if (abs(determinant) < 1e-16) {\n let basis = orthonormalBasis(normal);\n tangents.tangent = basis[0];\n tangents.bitangent = basis[1];\n\n return tangents;\n }\n\n let raw = (edge1 * delta2.y - edge2 * delta1.y) / determinant;\n // Made perpendicular to the shading normal, so a map tilts the interpolated normal rather than the flat one.\n let tangent = normalize(raw - normal * dot(normal, raw));\n\n if (!all(tangent == tangent)) {\n let basis = orthonormalBasis(normal);\n tangents.tangent = basis[0];\n tangents.bitangent = basis[1];\n\n return tangents;\n }\n\n tangents.tangent = tangent;\n tangents.bitangent = cross(normal, tangent) * select(-1.0, 1.0, determinant >= 0.0);\n tangents.valid = true;\n\n return tangents;\n}\n\n/** A tangent space normal map over a normal, scaled the way Three's normalScale scales it. */\nfn tangentNormal(index: u32, triangle: u32, bary: vec2<f32>, normal: vec3<f32>, scale: vec2<f32>) -> vec3<f32> {\n if (index == NO_TEXTURE) {\n return normal;\n }\n\n let channel = textureInfo(index).channel;\n let tangents = tangentFrame(triangle, channel, normal);\n\n if (!tangents.valid) {\n return normal;\n }\n\n let texel = sampleTextureAt(index, triangleUv(triangle, bary, channel)).xyz * 2.0 - 1.0;\n let mapped = normalize(\n tangents.tangent * (texel.x * scale.x)\n + tangents.bitangent * (texel.y * scale.y)\n + normal * texel.z);\n\n return select(mapped, normal, !all(mapped == mapped));\n}\n\n/** The normal the material asks for: an object space or tangent space normal map, or else a bump map. */\nfn mappedNormal(m: u32, triangle: u32, bary: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {\n let normalMap = materials[m].normalMap;\n\n if (normalMap != NO_TEXTURE && (materials[m].flags & MATERIAL_OBJECT_NORMAL_MAP) != 0u) {\n let texel = sampleMap(normalMap, triangle, bary).xyz * 2.0 - 1.0;\n let world = normalize(vec3<f32>(\n dot(materials[m].normalRow0.xyz, texel),\n dot(materials[m].normalRow1.xyz, texel),\n dot(materials[m].normalRow2.xyz, texel)));\n\n // Seen from behind, a double sided surface turns its map around with it.\n return select(world, -world, dot(world, normal) < 0.0);\n }\n\n if (normalMap != NO_TEXTURE) {\n return tangentNormal(normalMap, triangle, bary, normal, vec2<f32>(materials[m].normalScaleX, materials[m].normalScaleY));\n }\n\n let bumpMap = materials[m].bumpMap;\n\n if (bumpMap != NO_TEXTURE) {\n // The CPU renderer's bump: the height's change over one texel, along the tangent frame of the map's UVs.\n let info = textureInfo(bumpMap);\n let channel = info.channel;\n let tangents = tangentFrame(triangle, channel, normal);\n let uv = triangleUv(triangle, bary, channel);\n let du = 1.0 / f32(max(info.size.x, 1u));\n let dv = 1.0 / f32(max(info.size.y, 1u));\n let scale = materials[m].bumpScale;\n let height = sampleTextureAt(bumpMap, uv).x;\n let dx = (sampleTextureAt(bumpMap, uv + vec2<f32>(du, 0.0)).x - height) * scale;\n let dy = (sampleTextureAt(bumpMap, uv + vec2<f32>(0.0, dv)).x - height) * scale;\n let bumped = normalize(normal - (tangents.tangent * dx + tangents.bitangent * dy));\n\n return select(bumped, normal, !all(bumped == bumped));\n }\n\n return normal;\n}\n\n/** The interpolated vertex normal as the mesh authored it, pointing out of a closed volume. */\nfn authoredNormal(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let base = triangle * 9u;\n let n0 = vec3<f32>(attributes[base], attributes[base + 1u], attributes[base + 2u]);\n let n1 = vec3<f32>(attributes[base + 3u], attributes[base + 4u], attributes[base + 5u]);\n let n2 = vec3<f32>(attributes[base + 6u], attributes[base + 7u], attributes[base + 8u]);\n\n return normalize(n0 * (1.0 - bary.x - bary.y) + n1 * bary.x + n2 * bary.y);\n}\n\n/** The interpolated vertex normal, or the face's for flat shading, turned to face the ray. */\nfn interpolatedNormal(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n var normal = authoredNormal(triangle, bary);\n\n if ((materials[m].flags & MATERIAL_FLAT) != 0u || !all(normal == normal)) {\n normal = triangleNormal(triangle);\n }\n\n return select(normal, -normal, dot(normal, direction) > 0.0);\n}\n\n/** Three's transmission at this point: the factor, its map's red channel, and none of it on metal. */\nfn transmissionAt(m: u32, triangle: u32, bary: vec2<f32>, metalness: f32) -> f32 {\n if (materials[m].transmission <= 0.0) {\n return 0.0;\n }\n\n let map = sampleMap(materials[m].transmissionMap, triangle, bary).r;\n\n return clamp(materials[m].transmission * map, 0.0, 1.0) * (1.0 - metalness);\n}\n\n/** Beer-Lambert: the colour reached at the attenuation distance fixes how fast each channel fades. */\nfn absorptionOf(m: u32) -> vec3<f32> {\n let attenuation = materials[m].attenuation;\n\n if (attenuation.w <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n return -log(max(attenuation.rgb, vec3<f32>(1e-6))) / attenuation.w;\n}\n\n/** Three's evalSensitivity: the XYZ response of a thin film, Fourier-fitted, in linear sRGB. */\nfn filmSensitivity(opd: f32, shift: vec3<f32>) -> vec3<f32> {\n let phase = 2.0 * PI * opd * 1.0e-9;\n let value = vec3<f32>(5.4856e-13, 4.4201e-13, 5.2481e-13);\n let position = vec3<f32>(1.6810e+06, 1.7953e+06, 2.2084e+06);\n let variance = vec3<f32>(4.3278e+09, 9.3046e+09, 6.6121e+09);\n var xyz = value * sqrt(2.0 * PI * variance) * cos(position * phase + shift) * exp(-phase * phase * variance);\n\n xyz.x = xyz.x + 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x)\n * exp(-4.5282e+09 * phase * phase);\n xyz = xyz / 1.0685e-7;\n\n return mat3x3<f32>(\n vec3<f32>(3.2404542, -0.9692660, 0.0556434),\n vec3<f32>(-1.5371385, 1.8760108, -0.2040259),\n vec3<f32>(-0.4985314, 0.0415560, 1.0572252)) * xyz;\n}\n\nfn schlick3(f0: vec3<f32>, f90: f32, cosine: f32) -> vec3<f32> {\n let t = 1.0 - cosine;\n let k = t * t * t * t * t;\n\n return f0 + (vec3<f32>(f90) - f0) * k;\n}\n\n/** Three's evalIridescence (Belcour and Barla 2017): the Fresnel of a thin film over a base of reflectance baseF0. */\nfn evalIridescence(filmIor: f32, cosTheta1: f32, thickness: f32, baseF0: vec3<f32>) -> vec3<f32> {\n let ior = mix(1.0, filmIor, smoothstep(0.0, 0.03, thickness));\n let sinTheta2Sq = (1.0 / ior) * (1.0 / ior) * (1.0 - cosTheta1 * cosTheta1);\n let cosTheta2Sq = 1.0 - sinTheta2Sq;\n\n if (cosTheta2Sq < 0.0) {\n return vec3<f32>(1.0);\n }\n\n let cosTheta2 = sqrt(cosTheta2Sq);\n let r0 = ((ior - 1.0) / (ior + 1.0)) * ((ior - 1.0) / (ior + 1.0));\n let r12 = r0 + (1.0 - r0) * pow(1.0 - cosTheta1, 5.0);\n let t121 = 1.0 - r12;\n let phi12 = select(0.0, PI, ior < 1.0);\n let phi21 = PI - phi12;\n let root = sqrt(clamp(baseF0, vec3<f32>(0.0), vec3<f32>(0.9999)));\n let baseIor = (vec3<f32>(1.0) + root) / (vec3<f32>(1.0) - root);\n let r1 = ((baseIor - vec3<f32>(ior)) / (baseIor + vec3<f32>(ior))) * ((baseIor - vec3<f32>(ior)) / (baseIor + vec3<f32>(ior)));\n let r23 = schlick3(r1, 1.0, cosTheta2);\n let phi23 = select(vec3<f32>(0.0), vec3<f32>(PI), baseIor < vec3<f32>(ior));\n let opd = 2.0 * ior * thickness * cosTheta2;\n let phi = vec3<f32>(phi21) + phi23;\n let r123 = clamp(r12 * r23, vec3<f32>(1e-5), vec3<f32>(0.9999));\n let r123Root = sqrt(r123);\n let rs = t121 * t121 * r23 / (vec3<f32>(1.0) - r123);\n var intensity = vec3<f32>(r12) + rs;\n var cm = rs - vec3<f32>(t121);\n\n for (var m = 1; m <= 2; m = m + 1) {\n cm = cm * r123Root;\n intensity = intensity + cm * 2.0 * filmSensitivity(f32(m) * opd, f32(m) * phi);\n }\n\n return max(intensity, vec3<f32>(0.0));\n}\n\n/** Three's toon ramp: the gradient map across dot(n, l) from -1 to 1, or a single step at 0.7 without one. */\nfn toonRamp(gradientMap: u32, cosine: f32) -> f32 {\n let coordinate = cosine * 0.5 + 0.5;\n\n if (gradientMap == NO_TEXTURE) {\n return select(0.7, 1.0, coordinate >= 0.7);\n }\n\n return sampleTextureRaw(gradientMap, vec2<f32>(coordinate, 0.0)).r;\n}\n\n/**\n * Whether a material's coverage can be told from its maps alone, before the rest of the surface is read: no shader that\n * might rewrite the alpha or discard, normal blending, and something that makes it partial at all.\n */\nfn coverageFirst(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return materials[m].program == NO_PROGRAM\n && (blending == BLEND_NORMAL || blending == BLEND_NONE)\n && (materials[m].alphaTest > 0.0 || (flags & (MATERIAL_ALPHA_HASH | MATERIAL_TRANSPARENT)) != 0u);\n}\n\n/** Everything the lobes need about one point of one triangle. */\nfn resolveSurface(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {\n let m = materialIndexOf(triangle);\n\n return resolveSurfaceFrom(triangle, bary, direction, sampleMap(materials[m].map, triangle, bary));\n}\n\n/** The same, from the colour map's texel at this point, for a caller that already read it. */\nfn resolveSurfaceFrom(triangle: u32, bary: vec2<f32>, direction: vec3<f32>, base: vec4<f32>) -> Surface {\n let m = materialIndexOf(triangle);\n let flags = materials[m].flags;\n let kind = materials[m].kind;\n let facing = interpolatedNormal(m, triangle, bary, direction);\n let view = -direction;\n var surface: Surface;\n\n surface.material = m;\n surface.flags = flags;\n surface.kind = kind;\n surface.blending = materials[m].blending;\n surface.facing = facing;\n surface.front = dot(authoredNormal(triangle, bary), direction) < 0.0;\n surface.normal = mappedNormal(m, triangle, bary, facing);\n\n // A normal map may tilt past the horizon, where the surface would be lit from behind itself.\n if (dot(surface.normal, view) <= 0.0) {\n surface.normal = facing;\n }\n\n var albedo = materials[m].color.rgb * base.rgb;\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((flags & MATERIAL_VERTEX_COLORS) != 0u) {\n let color = triangleColor(triangle, bary);\n albedo = albedo * color.rgb;\n alpha = alpha * color.a;\n }\n\n var roughness = materials[m].specular.w * sampleMap(materials[m].roughnessMap, triangle, bary).g;\n var metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n var emissive = vec3<f32>(0.0);\n\n // An unlit light's colour is added as what it shows, below; emissionAt gives the same colour to light samples.\n if ((flags & MATERIAL_UNLIT_LIGHT) == 0u) {\n emissive = emissionAt(triangle, bary);\n }\n\n surface.opacity = 1.0;\n\n // A built-in material's hooks run on its values before lighting, in Three's view space; a shader material's output\n // is the colour itself.\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(albedo, alpha);\n input.normal = toView(surface.normal);\n input.geometryNormal = toView(facing);\n input.roughness = roughness;\n input.metalness = metalness;\n input.emissive = emissive;\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n surface.opacity = 0.0;\n }\n\n if ((flags & MATERIAL_PATCHED) != 0u) {\n albedo = output.diffuseColor.rgb;\n alpha = output.diffuseColor.a;\n roughness = output.roughness;\n metalness = output.metalness;\n emissive = output.emissive;\n let normal = toWorld(output.normal);\n\n if (all(normal == normal)) {\n surface.normal = select(normal, -normal, dot(normal, view) < 0.0);\n }\n } else {\n albedo = output.color.rgb;\n alpha = output.color.a;\n }\n }\n\n surface.opacity = surface.opacity * coverageOf(m, alpha);\n surface.alpha = alpha;\n surface.albedo = albedo;\n surface.roughness = clamp(roughness, 0.0525, 1.0);\n surface.metalness = clamp(metalness, 0.0, 1.0);\n surface.emissive = emissive;\n surface.transmission = transmissionAt(m, triangle, bary, surface.metalness);\n surface.ior = max(materials[m].ior, 1.0);\n surface.thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n surface.dispersion = materials[m].dispersion;\n surface.subsurface = select(0.0, materials[m].subsurface.w, frame.subsurfaceEnabled != 0u);\n surface.absorption = absorptionOf(m);\n surface.shininess = materials[m].shininess;\n surface.specularStrength = sampleMap(materials[m].specularMap, triangle, bary).r;\n surface.gradientMap = materials[m].gradientMap;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = materials[m].envMapIntensity;\n surface.legacyAmount = 0.0;\n surface.legacyEnv = vec3<f32>(0.0);\n surface.combine = materials[m].combine;\n surface.lightMap = vec3<f32>(0.0);\n surface.clearcoat = 0.0;\n surface.iridescence = 0.0;\n surface.anisotropy = 0.0;\n surface.sheenColor = vec3<f32>(0.0);\n surface.specularColor = materials[m].specular.rgb;\n surface.specularF90 = 1.0;\n\n if ((flags & MATERIAL_PBR) != 0u) {\n if ((flags & MATERIAL_SPECULAR) != 0u) {\n // A physical material: the index of refraction, the specular colour and intensity and their maps.\n let intensity = materials[m].specularIntensity * sampleMap(materials[m].specularIntensityMap, triangle, bary).a;\n let color = materials[m].specular.rgb * sampleMap(materials[m].specularColorMap, triangle, bary).rgb;\n\n surface.specularColor = min(materials[m].dielectric * color, vec3<f32>(1.0)) * intensity;\n surface.specularF90 = mix(intensity, 1.0, surface.metalness);\n } else {\n // Three's standard material has no index of refraction and reflects 0.04.\n surface.specularColor = vec3<f32>(0.04);\n }\n\n surface.envMap = materials[m].envMap;\n\n if (materials[m].clearcoat > 0.0) {\n surface.clearcoat = clamp(materials[m].clearcoat * sampleMap(materials[m].clearcoatMap, triangle, bary).r, 0.0, 1.0);\n surface.clearcoatRoughness = clamp(\n materials[m].clearcoatRoughness * sampleMap(materials[m].clearcoatRoughnessMap, triangle, bary).g, 0.0525, 1.0);\n // The coat ignores the base's normal map: it has its own, over the interpolated normal.\n surface.clearcoatNormal = tangentNormal(materials[m].clearcoatNormalMap, triangle, bary, facing,\n vec2<f32>(materials[m].clearcoatNormalScaleX, materials[m].clearcoatNormalScaleY));\n\n if (dot(surface.clearcoatNormal, view) <= 0.0) {\n surface.clearcoatNormal = facing;\n }\n }\n\n if (any(materials[m].sheen.rgb > vec3<f32>(0.0))) {\n let map = sampleMap(materials[m].sheenRoughnessMap, triangle, bary).a;\n surface.sheenColor = materials[m].sheen.rgb * sampleMap(materials[m].sheenColorMap, triangle, bary).rgb;\n surface.sheenRoughness = clamp(materials[m].sheen.w, 0.0001, 1.0) * map;\n }\n\n if (materials[m].iridescence > 0.0) {\n var thickness = materials[m].iridescenceThicknessMax;\n\n if (materials[m].iridescenceThicknessMap != NO_TEXTURE) {\n thickness = mix(materials[m].iridescenceThicknessMin, materials[m].iridescenceThicknessMax,\n sampleMap(materials[m].iridescenceThicknessMap, triangle, bary).g);\n }\n\n if (thickness > 0.0) {\n surface.iridescence = clamp(\n materials[m].iridescence * sampleMap(materials[m].iridescenceMap, triangle, bary).r, 0.0, 1.0);\n\n let nv = clamp(dot(surface.normal, view), 0.0, 1.0);\n surface.iridescenceFresnel = mix(\n evalIridescence(materials[m].iridescenceIor, nv, thickness, surface.specularColor),\n evalIridescence(materials[m].iridescenceIor, nv, thickness, albedo),\n surface.metalness);\n }\n }\n\n if (materials[m].anisotropy != 0.0) {\n let rotation = materials[m].anisotropyRotation;\n var vector = vec2<f32>(cos(rotation), sin(rotation)) * materials[m].anisotropy;\n let channel = select(0u, textureInfo(materials[m].anisotropyMap).channel, materials[m].anisotropyMap != NO_TEXTURE);\n let tangents = tangentFrame(triangle, channel, surface.normal);\n\n if (materials[m].anisotropyMap != NO_TEXTURE) {\n let polar = sampleMap(materials[m].anisotropyMap, triangle, bary).rgb;\n let turn = mat2x2<f32>(vector.x, vector.y, -vector.y, vector.x);\n vector = turn * normalize(2.0 * polar.rg - vec2<f32>(1.0)) * polar.b;\n }\n\n let strength = length(vector);\n let along = select(vector / max(strength, 1e-20), vec2<f32>(1.0, 0.0), strength == 0.0);\n\n surface.anisotropy = clamp(strength, 0.0, 1.0);\n surface.alphaT = mix(surface.roughness * surface.roughness, 1.0, surface.anisotropy * surface.anisotropy);\n surface.anisotropyT = tangents.tangent * along.x + tangents.bitangent * along.y;\n surface.anisotropyB = tangents.bitangent * along.x - tangents.tangent * along.y;\n }\n } else if (materials[m].envMap != NO_TEXTURE\n && (kind == KIND_BASIC || kind == KIND_LAMBERT || kind == KIND_PHONG)) {\n // Basic, Lambert and Phong blend their own map over the lit colour, mirrored or bent the way the map says.\n let info = textureInfo(materials[m].envMap);\n let bent = refract(direction, surface.normal, materials[m].refractionRatio);\n let lookup = select(reflect(direction, surface.normal), bent, (info.flags & 16u) != 0u && any(bent != vec3<f32>(0.0)));\n\n surface.legacyEnv = sampleEquirect(materials[m].envMap, lookup).rgb * materials[m].envMapIntensity;\n surface.legacyAmount = clamp(materials[m].reflectivity * surface.specularStrength, 0.0, 1.0);\n }\n\n if (materials[m].lightMap != NO_TEXTURE) {\n let baked = sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity;\n surface.lightMap = baked * albedo * (1.0 - surface.metalness) / PI;\n }\n\n // A material Three does not light reads as its own colour, which in a path tracer is the same as emitting it.\n if ((flags & MATERIAL_LIT) == 0u) {\n var color = albedo;\n\n if (kind == KIND_NORMAL) {\n color = toView(surface.normal) * 0.5 + 0.5;\n } else if (kind == KIND_DEPTH) {\n let clip = frame.projection * (frame.worldToCamera * vec4<f32>(trianglePointAt(triangle, bary), 1.0));\n color = vec3<f32>(1.0 - (clip.z / clip.w * 0.5 + 0.5));\n } else if (kind == KIND_MATCAP) {\n let point = trianglePointAt(triangle, bary);\n let viewDirection = normalize(-(frame.worldToCamera * vec4<f32>(point, 1.0)).xyz);\n let x = normalize(vec3<f32>(viewDirection.z, 0.0, -viewDirection.x));\n let y = cross(viewDirection, x);\n let normal = toView(surface.normal);\n let uv = vec2<f32>(dot(x, normal), dot(y, normal)) * 0.495 + 0.5;\n var matcap = vec3<f32>(mix(0.2, 0.8, uv.y));\n\n if (materials[m].matcap != NO_TEXTURE) {\n matcap = sampleTextureRaw(materials[m].matcap, uv).rgb;\n }\n\n color = albedo * matcap;\n } else if (kind == KIND_BASIC && materials[m].lightMap != NO_TEXTURE) {\n // A basic material with a light map shows the map's light instead of full brightness.\n color = albedo * sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity / PI;\n }\n\n if (surface.legacyAmount > 0.0) {\n let amount = surface.legacyAmount;\n\n if (surface.combine == 1u) {\n color = mix(color, surface.legacyEnv, amount);\n } else if (surface.combine == 2u) {\n color = color + surface.legacyEnv * amount;\n } else {\n color = mix(color, color * surface.legacyEnv, amount);\n }\n\n surface.legacyAmount = 0.0;\n }\n\n surface.emissive = surface.emissive + color;\n surface.lightMap = vec3<f32>(0.0);\n }\n\n return surface;\n}\n";
|
|
10
10
|
//# sourceMappingURL=surface.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/surface.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"surface.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/surface.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,2ohCA2zBxB,CAAC"}
|
|
@@ -62,6 +62,8 @@ struct Surface {
|
|
|
62
62
|
lightMap: vec3<f32>,
|
|
63
63
|
blending: u32,
|
|
64
64
|
material: u32,
|
|
65
|
+
// Share of the diffuse layer that scatters inside the material instead of at its surface.
|
|
66
|
+
subsurface: f32,
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */
|
|
@@ -114,6 +116,18 @@ fn isCulled(material: u32, triangle: u32, direction: vec3<f32>) -> bool {
|
|
|
114
116
|
/** What the triangle radiates at this point, its emissive map included so direct and indirect light agree on it. */
|
|
115
117
|
fn emissionAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {
|
|
116
118
|
let m = materialIndexOf(triangle);
|
|
119
|
+
|
|
120
|
+
// An unlit material emits its colour as the camera would see it, so a bounce ray finds what a light sample counted.
|
|
121
|
+
if ((materials[m].flags & MATERIAL_UNLIT_LIGHT) != 0u) {
|
|
122
|
+
var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;
|
|
123
|
+
|
|
124
|
+
if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {
|
|
125
|
+
color = color * triangleColor(triangle, bary).rgb;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return color;
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
let map = sampleMap(materials[m].emissiveMap, triangle, bary);
|
|
118
132
|
|
|
119
133
|
return materials[m].emissive.rgb * materials[m].emissive.w * map.rgb;
|
|
@@ -611,7 +625,12 @@ fn resolveSurfaceFrom(triangle: u32, bary: vec2<f32>, direction: vec3<f32>, base
|
|
|
611
625
|
|
|
612
626
|
var roughness = materials[m].specular.w * sampleMap(materials[m].roughnessMap, triangle, bary).g;
|
|
613
627
|
var metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;
|
|
614
|
-
var emissive =
|
|
628
|
+
var emissive = vec3<f32>(0.0);
|
|
629
|
+
|
|
630
|
+
// An unlit light's colour is added as what it shows, below; emissionAt gives the same colour to light samples.
|
|
631
|
+
if ((flags & MATERIAL_UNLIT_LIGHT) == 0u) {
|
|
632
|
+
emissive = emissionAt(triangle, bary);
|
|
633
|
+
}
|
|
615
634
|
|
|
616
635
|
surface.opacity = 1.0;
|
|
617
636
|
|
|
@@ -658,6 +677,7 @@ fn resolveSurfaceFrom(triangle: u32, bary: vec2<f32>, direction: vec3<f32>, base
|
|
|
658
677
|
surface.ior = max(materials[m].ior, 1.0);
|
|
659
678
|
surface.thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;
|
|
660
679
|
surface.dispersion = materials[m].dispersion;
|
|
680
|
+
surface.subsurface = select(0.0, materials[m].subsurface.w, frame.subsurfaceEnabled != 0u);
|
|
661
681
|
surface.absorption = absorptionOf(m);
|
|
662
682
|
surface.shininess = materials[m].shininess;
|
|
663
683
|
surface.specularStrength = sampleMap(materials[m].specularMap, triangle, bary).r;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/surface.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"surface.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/surface.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2zBtC,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { PackedEnvironment } from "./environment.js";
|
|
2
|
+
import type { GpuRenderSettings } from "./settings.js";
|
|
3
|
+
import type { SerializedLight } from "./upstream.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How much more each colour scatters than green, at 680, 550 and 440 nm. Clean air scatters as the inverse fourth
|
|
7
|
+
* power of the wavelength (Rayleigh), haze nearly alike (Mie, an Angstrom exponent of about 1.3).
|
|
8
|
+
*/
|
|
9
|
+
const RAYLEIGH = [(550 / 680) ** 4, 1, (550 / 440) ** 4];
|
|
10
|
+
const MIE = [(550 / 680) ** 1.3, 1, (550 / 440) ** 1.3];
|
|
11
|
+
|
|
12
|
+
/** Koschmieder: a dark object fades to 2 percent contrast over the visibility, so green's extinction is ln 50 over it. */
|
|
13
|
+
const KOSCHMIEDER = Math.log(50);
|
|
14
|
+
|
|
15
|
+
/** Extinction per scene unit of red, green and blue for the atmosphere setting, or nothing when it is off. */
|
|
16
|
+
export function atmosphereExtinction(atmosphere: GpuRenderSettings["atmosphere"]): [number, number, number] {
|
|
17
|
+
const visibility = Number(atmosphere.visibility);
|
|
18
|
+
|
|
19
|
+
if (!atmosphere.enabled || !(visibility > 0)) return [0, 0, 0];
|
|
20
|
+
|
|
21
|
+
const haze = Math.min(1, Math.max(0, Number(atmosphere.haze) || 0));
|
|
22
|
+
const scale = KOSCHMIEDER / visibility;
|
|
23
|
+
|
|
24
|
+
return [0, 1, 2].map(k => scale * (RAYLEIGH[k] + (MIE[k] - RAYLEIGH[k]) * haze)) as [number, number, number];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The brightest directional light of a scene, which lights the air: its direction towards the light and irradiance. */
|
|
28
|
+
export function brightestSun(lights: readonly SerializedLight[]): { direction: number[]; irradiance: number[] } | null {
|
|
29
|
+
let best: { direction: number[]; irradiance: number[] } | null = null;
|
|
30
|
+
let strongest = 0;
|
|
31
|
+
|
|
32
|
+
for (const light of lights) {
|
|
33
|
+
if (light.type !== "DirectionalLight" || !(light.intensity > 0)) continue;
|
|
34
|
+
|
|
35
|
+
const irradiance = light.color.map(channel => channel * light.intensity);
|
|
36
|
+
const strength = 0.2126 * irradiance[0] + 0.7152 * irradiance[1] + 0.0722 * irradiance[2];
|
|
37
|
+
const axis = [0, 1, 2].map(k => light.position[k] - light.target[k]);
|
|
38
|
+
const size = Math.hypot(...axis);
|
|
39
|
+
|
|
40
|
+
if (strength > strongest && size > 0) {
|
|
41
|
+
strongest = strength;
|
|
42
|
+
best = { direction: axis.map(value => value / size), irradiance };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return best;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const averages = new WeakMap<PackedEnvironment, [number, number, number]>();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The radiance of an image averaged over the whole sphere: what air scattering every direction alike sends towards the
|
|
53
|
+
* camera. Each row weighs by the sine of its colatitude, the share of the sphere it covers.
|
|
54
|
+
*/
|
|
55
|
+
export function averageRadiance(environment: PackedEnvironment): [number, number, number] {
|
|
56
|
+
const known = averages.get(environment);
|
|
57
|
+
|
|
58
|
+
if (known) return known;
|
|
59
|
+
|
|
60
|
+
const { width, height, data } = environment.texture;
|
|
61
|
+
const sum = [0, 0, 0];
|
|
62
|
+
let weight = 0;
|
|
63
|
+
// A byte image is stored as it was encoded; only a float one holds linear light, as an HDR image always does.
|
|
64
|
+
const scale = data instanceof Float32Array ? 1 : 1 / 255;
|
|
65
|
+
|
|
66
|
+
for (let y = 0; y < height; y++) {
|
|
67
|
+
const sine = Math.sin((y + 0.5) / height * Math.PI);
|
|
68
|
+
|
|
69
|
+
for (let x = 0; x < width; x++) {
|
|
70
|
+
const at = (y * width + x) * 4;
|
|
71
|
+
|
|
72
|
+
sum[0] += data[at] * scale * sine;
|
|
73
|
+
sum[1] += data[at + 1] * scale * sine;
|
|
74
|
+
sum[2] += data[at + 2] * scale * sine;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
weight += sine * width;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const average = sum.map(channel => channel / Math.max(weight, 1e-12)) as [number, number, number];
|
|
81
|
+
|
|
82
|
+
averages.set(environment, average);
|
|
83
|
+
|
|
84
|
+
return average;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A vertical gradient averaged over the sphere. Directions are spread evenly in height over a sphere, so the share of
|
|
89
|
+
* the top colour is the mean of h^exponent for h even in [0, 1]: one over exponent plus one.
|
|
90
|
+
*/
|
|
91
|
+
export function gradientAverage(top: number[], bottom: number[], exponent: number): [number, number, number] {
|
|
92
|
+
const share = 1 / (Math.max(exponent, 0) + 1);
|
|
93
|
+
|
|
94
|
+
return [0, 1, 2].map(k => bottom[k] + (top[k] - bottom[k]) * share) as [number, number, number];
|
|
95
|
+
}
|
package/src/denoise.ts
CHANGED
|
@@ -15,6 +15,16 @@ export interface DenoiserSettings {
|
|
|
15
15
|
onlyNoisy: boolean;
|
|
16
16
|
/** Clamp a pixel brighter than every one of its neighbours to the brightest of them, before filtering. */
|
|
17
17
|
antiFirefly: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* How far the filter lets go of the normal and depth tests over a material that scatters light inside, from 0 to 1.
|
|
20
|
+
* Light leaves such a surface after wandering under it, so its shading hardly follows the normal at each point.
|
|
21
|
+
*/
|
|
22
|
+
subsurface: number;
|
|
23
|
+
/**
|
|
24
|
+
* The smallest footprint a smooth reflection is filtered over, in pixels; 0 leaves a mirror alone. Within it, taps
|
|
25
|
+
* also have to agree on how far away the reflected thing is, so the edge of a reflected object stays sharp.
|
|
26
|
+
*/
|
|
27
|
+
mirrorRadius: number;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
export const DEFAULT_DENOISER: Readonly<DenoiserSettings> = Object.freeze({
|
|
@@ -23,6 +33,8 @@ export const DEFAULT_DENOISER: Readonly<DenoiserSettings> = Object.freeze({
|
|
|
23
33
|
edge: 2,
|
|
24
34
|
onlyNoisy: true,
|
|
25
35
|
antiFirefly: true,
|
|
36
|
+
subsurface: 1,
|
|
37
|
+
mirrorRadius: 3,
|
|
26
38
|
});
|
|
27
39
|
|
|
28
40
|
/** The settings with `input` applied, each field kept in range. */
|
|
@@ -35,6 +47,8 @@ export function mergeDenoiser(base: DenoiserSettings, input: Partial<DenoiserSet
|
|
|
35
47
|
edge: Math.max(0.01, Number(merged.edge) || DEFAULT_DENOISER.edge),
|
|
36
48
|
onlyNoisy: Boolean(merged.onlyNoisy),
|
|
37
49
|
antiFirefly: Boolean(merged.antiFirefly),
|
|
50
|
+
subsurface: Math.min(1, Math.max(0, Number(merged.subsurface) || 0)),
|
|
51
|
+
mirrorRadius: Math.min(16, Math.max(0, Number(merged.mirrorRadius) || 0)),
|
|
38
52
|
};
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -77,7 +91,8 @@ struct Params {
|
|
|
77
91
|
converged: f32,
|
|
78
92
|
pixelScale: f32,
|
|
79
93
|
orthographic: u32,
|
|
80
|
-
|
|
94
|
+
subsurface: f32,
|
|
95
|
+
mirrorRadius: f32,
|
|
81
96
|
};
|
|
82
97
|
|
|
83
98
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
@@ -132,6 +147,11 @@ fn guideRoughness(index: u32) -> f32 {
|
|
|
132
147
|
return accumulation[2u * params.count + index].w / samples(index);
|
|
133
148
|
}
|
|
134
149
|
|
|
150
|
+
/** How much of the first surface scatters light inside, averaged over the pixel's samples. */
|
|
151
|
+
fn guideSubsurface(index: u32) -> f32 {
|
|
152
|
+
return accumulation[5u * params.count + index].w / samples(index);
|
|
153
|
+
}
|
|
154
|
+
|
|
135
155
|
fn specularDistance(index: u32) -> f32 {
|
|
136
156
|
return accumulation[4u * params.count + index].w / samples(index);
|
|
137
157
|
}
|
|
@@ -282,11 +302,22 @@ fn atrous(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
282
302
|
let normal = guide.xyz;
|
|
283
303
|
let depth = guide.w;
|
|
284
304
|
let roughness = roughnesses[index];
|
|
285
|
-
let diffuseSigma = params.edge * sqrt(max(localVariance(pixel, false), 0.0))
|
|
305
|
+
let diffuseSigma = params.edge * sqrt(max(localVariance(pixel, false), 0.0))
|
|
306
|
+
* (1.0 + guideSubsurface(index) * params.subsurface) + 1e-6;
|
|
286
307
|
let specularSigma = params.edge * sqrt(max(localVariance(pixel, true), 0.0)) + 1e-6;
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
308
|
+
// A smooth reflection is filtered over at least the mirror radius, by taps that see the reflected thing at the same
|
|
309
|
+
// distance: that test keeps the edges of what it reflects, which lets the normal test relax over a curved mirror,
|
|
310
|
+
// where neighbouring normals part fast and would otherwise leave the reflection unfiltered.
|
|
311
|
+
let mirror = params.mirrorRadius > 0.0 && roughness < 0.3;
|
|
312
|
+
let radius = select(specularRadius(index, depth), max(specularRadius(index, depth), params.mirrorRadius), mirror);
|
|
313
|
+
let reflected = specularDistance(index);
|
|
314
|
+
// A smoother surface's reflection turns faster with its normal: the normal test sharpens with it, short of shutting
|
|
315
|
+
// out the neighbours of a curved mirror altogether once the reflected distance guards it.
|
|
316
|
+
let specularPower = min(128.0 / max(roughness * roughness, 0.03), select(4096.0, 64.0, mirror));
|
|
317
|
+
// Under a surface that scatters light inside, the normal and the depth say little about the light that leaves it.
|
|
318
|
+
let scattering = guideSubsurface(index) * params.subsurface;
|
|
319
|
+
let diffusePower = mix(128.0, 1.0, scattering);
|
|
320
|
+
let depthSlack = mix(1.0, 4.0, scattering);
|
|
290
321
|
let step = i32(params.step);
|
|
291
322
|
var diffuse = vec4<f32>(0.0);
|
|
292
323
|
var specular = vec4<f32>(0.0);
|
|
@@ -320,14 +351,19 @@ fn atrous(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
320
351
|
let offset = length(vec2<f32>(f32(dx * step), f32(dy * step)));
|
|
321
352
|
let alignment = max(dot(normal, otherGuide.xyz), 0.0);
|
|
322
353
|
let depthWeight = exp(-abs(depth - otherGuide.w) / (0.02 * depth * offset + 1e-3));
|
|
354
|
+
let diffuseDepth = exp(-abs(depth - otherGuide.w) / (0.02 * depthSlack * depth * offset + 1e-3));
|
|
323
355
|
|
|
324
|
-
wd = wd * pow(alignment,
|
|
356
|
+
wd = wd * pow(alignment, diffusePower) * diffuseDepth
|
|
325
357
|
* exp(-abs(luminance(d.rgb) - luminance(diffuseCenter.rgb)) / diffuseSigma);
|
|
326
358
|
|
|
327
359
|
ws = ws * pow(alignment, specularPower) * depthWeight
|
|
328
360
|
* exp(-abs(otherRoughness - roughness) * 10.0)
|
|
329
361
|
* exp(-abs(luminance(s.rgb) - luminance(specularCenter.rgb)) / specularSigma)
|
|
330
362
|
* select(0.0, exp(-offset * offset / (radius * radius)), radius >= 0.5);
|
|
363
|
+
|
|
364
|
+
if (mirror) {
|
|
365
|
+
ws = ws * exp(-abs(specularDistance(other) - reflected) / (0.05 * reflected + 1e-3));
|
|
366
|
+
}
|
|
331
367
|
}
|
|
332
368
|
|
|
333
369
|
diffuse = diffuse + vec4<f32>(d.rgb * wd, d.w * wd * wd);
|
|
@@ -477,6 +513,8 @@ export class DenoisePass {
|
|
|
477
513
|
floats[7] = CONVERGED_ERROR;
|
|
478
514
|
floats[8] = view.pixelScale;
|
|
479
515
|
words[9] = view.orthographic ? 1 : 0;
|
|
516
|
+
floats[10] = settings.subsurface;
|
|
517
|
+
floats[11] = settings.mirrorRadius;
|
|
480
518
|
this.device.queue.writeBuffer(this.params[k], 0, values);
|
|
481
519
|
});
|
|
482
520
|
}
|
package/src/frameLayout.ts
CHANGED
|
@@ -32,6 +32,11 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
32
32
|
{ name: "areaLightCount", type: "u32", doc: "How many leading lights are emissive triangles." },
|
|
33
33
|
{ name: "lightCdf", type: "u32", doc: "Where every light's cumulative probability starts in the texture buffer." },
|
|
34
34
|
{ name: "lightTriangles", type: "u32", doc: "Where every emissive light's triangle starts in the texture buffer." },
|
|
35
|
+
{ name: "localLightCount", type: "u32", doc: "How many leading lights sit in the scene, which the light tree picks." },
|
|
36
|
+
{ name: "localLightPick", type: "f32", doc: "The chance of picking one of them, the last one's cumulative probability." },
|
|
37
|
+
{ name: "lightTree", type: "u32", doc: "Where the light tree's nodes start in the texture buffer; zero picks by power alone." },
|
|
38
|
+
{ name: "lightList", type: "u32", doc: "Where the lights of the tree's leaves are listed in the texture buffer." },
|
|
39
|
+
{ name: "lightLeaves", type: "u32", doc: "Where the leaf of every local light starts in the texture buffer." },
|
|
35
40
|
{ name: "clampIndirect", type: "f32", doc: "Maximum luminance one indirect contribution may add; zero leaves it." },
|
|
36
41
|
{ name: "shadowSpread", type: "f32", doc: "Tangent of half the angular diameter shadow rays spread over." },
|
|
37
42
|
{ name: "shadowsEnabled", type: "u32" },
|
|
@@ -56,6 +61,18 @@ export const FRAME_LAYOUT = layoutStruct("Frame", [
|
|
|
56
61
|
{ name: "volumetricData", type: "u32" },
|
|
57
62
|
{ name: "adaptiveMin", type: "u32", doc: "Samples a pixel takes before it may stop as converged; 0 never stops." },
|
|
58
63
|
{ name: "adaptiveThreshold", type: "f32", doc: "The error under which a pixel and its neighbours count as converged." },
|
|
64
|
+
{ name: "lensDistortion", type: "f32", doc: "Radial distortion of the camera ray: positive is barrel, negative pincushion." },
|
|
65
|
+
{ name: "rouletteThreshold", type: "f32", doc: "Throughput under which a path may be ended by chance; zero never." },
|
|
66
|
+
{ name: "rouletteStart", type: "u32", doc: "Bounces every path takes before the roulette may end it." },
|
|
67
|
+
{ name: "subsurfaceDepth", type: "u32", doc: "Bounces at which light still walks inside a material; deeper it reflects." },
|
|
68
|
+
{ name: "subsurfaceEnabled", type: "u32", doc: "Whether materials that scatter light inside do; 0 renders them opaque." },
|
|
69
|
+
{ name: "bevelEnabled", type: "u32", doc: "Whether materials with a bevel radius round their edges." },
|
|
70
|
+
{ name: "volumesEnabled", type: "u32", doc: "Whether volumes scatter light; 0 leaves their meshes out altogether." },
|
|
71
|
+
{ name: "cameraVolume", type: "u32", doc: "The volume material the camera stands inside, or NO_MATERIAL." },
|
|
72
|
+
{ name: "atmosphereColor", type: "vec4f", doc: "rgb is the radiance the air scatters towards the camera, w 1 when it is on." },
|
|
73
|
+
{ name: "atmosphereExtinction", type: "vec4f", doc: "rgb is how much of each colour the air takes per scene unit, w the haze." },
|
|
74
|
+
{ name: "atmosphereSun", type: "vec4f", doc: "xyz is the direction towards the brightest directional light." },
|
|
75
|
+
{ name: "atmosphereSunColor", type: "vec4f", doc: "rgb is that light's irradiance, which the air scatters." },
|
|
59
76
|
{ name: "clipPlane", type: "vec4f", doc: "A plane the camera ray drops surfaces behind, for planar captures; w = 0 and xyz = 0 is none." },
|
|
60
77
|
]);
|
|
61
78
|
|
package/src/geometry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { clipToWorld, transform4, worldToClip, type CameraMatrices } from "./cam
|
|
|
2
2
|
import {
|
|
3
3
|
isMaterialVisible,
|
|
4
4
|
MATERIAL_KIND,
|
|
5
|
+
MATERIAL_UNLIT_LIGHT,
|
|
5
6
|
materialProperties,
|
|
6
7
|
NO_PLACEMENT,
|
|
7
8
|
NO_PROGRAM,
|
|
@@ -525,9 +526,12 @@ export function packGeometry(
|
|
|
525
526
|
materials: new Uint32Array(triangleMaterials),
|
|
526
527
|
triangleCount: triangleMaterials.length,
|
|
527
528
|
materialColors: new Float32Array(properties.flatMap(m => [...m.color, m.opacity])),
|
|
528
|
-
// Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs.
|
|
529
|
+
// Only a lit or plainly emissive surface is a light: a shader's colour is not known until it runs. An unlit
|
|
530
|
+
// material of a fixed colour emits that colour, so it is one too.
|
|
529
531
|
materialEmissive: new Float32Array(properties.flatMap(m =>
|
|
530
|
-
m.kind === MATERIAL_KIND.shader ? [0, 0, 0, 0]
|
|
532
|
+
m.kind === MATERIAL_KIND.shader ? [0, 0, 0, 0]
|
|
533
|
+
: (m.flags & MATERIAL_UNLIT_LIGHT) !== 0 ? [...m.color, 1]
|
|
534
|
+
: [...m.emissive, m.emissiveIntensity])),
|
|
531
535
|
materialProperties: properties,
|
|
532
536
|
materialPlacements: entries.map(entry => entry.placement),
|
|
533
537
|
materialCount: properties.length,
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,19 @@ export { attachCanvas } from "./canvas.js";
|
|
|
21
21
|
export { DEFAULT_DENOISER } from "./denoise.js";
|
|
22
22
|
export type { DenoiserSettings } from "./denoise.js";
|
|
23
23
|
|
|
24
|
+
// The settings only this renderer reads, beside the CPU renderer's, and the defaults it renders with.
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_GPU_RENDER_SETTINGS,
|
|
27
|
+
effectiveExposure,
|
|
28
|
+
mergeGpuSettings,
|
|
29
|
+
physicalExposure,
|
|
30
|
+
whiteBalanceMatrix,
|
|
31
|
+
} from "./settings.js";
|
|
32
|
+
|
|
33
|
+
export type { GpuRenderSettings, GpuRenderSettingsInput } from "./settings.js";
|
|
34
|
+
export { subsurfaceMedium } from "./subsurface.js";
|
|
35
|
+
export type { SubsurfaceInput, SubsurfaceMaterial, SubsurfaceMedium } from "./subsurface.js";
|
|
36
|
+
|
|
24
37
|
export type { Bucket, Listener } from "./upstream.js";
|
|
25
38
|
|
|
26
39
|
// Scene packing, published because the sizes it produces are what a host checks against a device's limits.
|