@tinypoly/gpu-renderer 0.2.0 → 0.2.2
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/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +2 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/grass.d.ts +106 -0
- package/dist/grass.d.ts.map +1 -0
- package/dist/grass.js +350 -0
- package/dist/grass.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/materialLayout.d.ts +6 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +2 -0
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +11 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +4 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +25 -4
- package/dist/renderer.js.map +1 -1
- package/dist/shaderProgram.d.ts.map +1 -1
- package/dist/shaderProgram.js +10 -1
- package/dist/shaderProgram.js.map +1 -1
- package/dist/subsurface.d.ts +2 -0
- package/dist/subsurface.d.ts.map +1 -1
- package/dist/subsurface.js +3 -2
- package/dist/subsurface.js.map +1 -1
- 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 +39 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/grass.wgsl.d.ts +14 -0
- package/dist/wgsl/grass.wgsl.d.ts.map +1 -0
- package/dist/wgsl/grass.wgsl.js +456 -0
- package/dist/wgsl/grass.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 +5 -0
- 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 +34 -5
- package/dist/wgsl/pathTrace.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 +2 -0
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +2 -2
- package/src/frameLayout.ts +2 -0
- package/src/grass.ts +523 -0
- package/src/index.ts +2 -0
- package/src/materialLayout.ts +7 -0
- package/src/pathTracePass.ts +11 -9
- package/src/renderer.ts +36 -7
- package/src/shaderProgram.ts +11 -1
- package/src/subsurface.ts +3 -2
- package/src/wgsl/bsdf.wgsl.ts +39 -3
- package/src/wgsl/grass.wgsl.ts +456 -0
- package/src/wgsl/lights.wgsl.ts +5 -0
- package/src/wgsl/pathTrace.wgsl.ts +34 -5
- package/src/wgsl/surface.wgsl.ts +2 -0
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
* reflection: culled faces, the background, Three's fog, a light map's baked light and the coverage a transparent
|
|
11
11
|
* background turns into alpha. Transmission keeps a path on that line, as Three's transmission shows what is behind.
|
|
12
12
|
*/
|
|
13
|
-
export declare const PATH_TRACE_WGSL = "\n/** Light from an ambient or hemisphere light, which this renderer turns into an equally bright surrounding. */\nfn ambientRadiance(direction: vec3<f32>) -> vec3<f32> {\n let latitude = clamp(dot(direction, frame.ambientAxis.xyz) * 0.5 + 0.5, 0.0, 1.0);\n\n return mix(frame.ambientGround.rgb, frame.ambientSky.rgb, latitude) + probeRadiance(direction);\n}\n\n/**\n * A LightProbe's light. Three's coefficients are radiance projected on the SH3 basis, which its shader convolves with\n * the cosine to get irradiance; evaluated directly they are the surrounding that delivers exactly that irradiance.\n */\nfn probeRadiance(d: vec3<f32>) -> vec3<f32> {\n let radiance = frame.probe0.rgb * 0.282095\n + frame.probe1.rgb * (0.488603 * d.y)\n + frame.probe2.rgb * (0.488603 * d.z)\n + frame.probe3.rgb * (0.488603 * d.x)\n + frame.probe4.rgb * (1.092548 * d.x * d.y)\n + frame.probe5.rgb * (1.092548 * d.y * d.z)\n + frame.probe6.rgb * (0.315392 * (3.0 * d.z * d.z - 1.0))\n + frame.probe7.rgb * (1.092548 * d.x * d.z)\n + frame.probe8.rgb * (0.546274 * (d.x * d.x - d.y * d.y));\n\n return max(radiance, vec3<f32>(0.0));\n}\n\n/** What the camera sees where nothing is in the way, as the background setting says. */\nfn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {\n if (frame.background.w == 1.0) {\n return frame.background.rgb;\n }\n\n // A procedural sky paints the background with the strength it lights with, and its foreground over it. The CPU\n // renderer leaves that strength out of what the camera sees, which a renderer exposing for the light cannot: the sky\n // that lights a wall five times brighter than itself would show as a dark band behind it.\n if (frame.skyProgram != NO_PROGRAM || frame.skyIntensity > 0.0) {\n if (frame.skyBackground == 0u) {\n return vec3<f32>(0.0);\n }\n\n if (frame.skyProgram != NO_PROGRAM) {\n var input: ShaderInput;\n input.data = frame.skyData;\n input.direction = direction;\n input.diffuseColor = vec4<f32>(1.0);\n\n return runProgram(frame.skyProgram, input).color.rgb * frame.skyIntensity;\n }\n\n return environmentRadiance(direction);\n }\n\n return environmentRadiance(direction);\n}\n\n/**\n * How much of the sky a bounce ray that escaped keeps. Direct sampling aims at the bright parts of an image too, so the\n * two share it; when direct sampling took all of it, as it does from a toon surface, the bounce keeps none.\n */\nfn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>) -> f32 {\n if (frame.environmentPick <= 0.0) {\n return 1.0;\n }\n\n if (lightsOwnDirect) {\n return 0.0;\n }\n\n if (bsdfPdf <= 0.0) {\n return 1.0;\n }\n\n // The sky was aimed at from the vertex the bounce left, through its windows as well when that vertex is inside.\n return misWeight(bsdfPdf, frame.environmentPick * skyPdf(scatteredAt, direction, portalChance(scatteredAt)));\n}\n\n/** Scales a contribution down to the luminance ceiling, which trades a little bias for the fireflies it removes. */\nfn clampIndirect(contribution: vec3<f32>) -> vec3<f32> {\n let luminance = luminanceOf(contribution);\n\n if (frame.clampIndirect <= 0.0 || luminance <= frame.clampIndirect) {\n return contribution;\n }\n\n return contribution * (frame.clampIndirect / luminance);\n}\n\n/**\n * How much of the emission a bounce ray found the renderer keeps. Direct sampling had its own chance at this triangle,\n * and the heuristic splits the two so neither counts it twice.\n */\nfn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>,\n normal: vec3<f32>) -> f32 {\n if (frame.areaLightCount == 0u) {\n return 1.0;\n }\n\n // From the vertex the bounce left, which is where direct sampling would have picked it from.\n let areaPdf = lightPdfOfTriangle(hit.triangle, scatteredAt, normal);\n\n if (areaPdf <= 0.0) {\n return 1.0;\n }\n\n if (lightsOwnDirect) {\n return 0.0;\n }\n\n if (bsdfPdf <= 0.0) {\n return 1.0;\n }\n\n let cosLight = max(abs(dot(triangleNormal(hit.triangle), direction)), 1e-6);\n\n return misWeight(bsdfPdf, areaPdf * hit.distance * hit.distance / cosLight);\n}\n\n/**\n * The normal the light tree weighs lights by, or none where light from behind the surface counts too: a point in a\n * volume, a toon ramp read all round, a clearcoat that may face what its base turns from, and transmission.\n */\nfn treeNormal(surface: Surface) -> vec3<f32> {\n if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0) {\n return vec3<f32>(0.0);\n }\n\n return surface.normal;\n}\n\n/** One shadow ray from the surface towards a light, weighted against the chance the bounce would have found it too. */\nfn directLight(\n surface: Surface,\n position: vec3<f32>,\n view: vec3<f32>,\n pick: f32,\n u: vec2<f32>,\n rng: ptr<function, PathSampler>,\n diffusePart: ptr<function, vec3<f32>>,\n medium: f32) -> vec3<f32> {\n *diffusePart = vec3<f32>(0.0);\n\n let chosen = sampleLight(position, treeNormal(surface), pick, u);\n\n if (!chosen.valid) {\n return vec3<f32>(0.0);\n }\n\n // A material with its own environment map sees that map in place of the scene's surroundings.\n if (chosen.kind == LIGHT_ENVIRONMENT && surface.envMap != NO_TEXTURE) {\n return vec3<f32>(0.0);\n }\n\n let reflected = evaluateBsdf(surface, view, chosen.direction);\n\n if (all(reflected.value <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n\n let toon = surface.kind == KIND_TOON;\n // Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.\n let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);\n let through = lightTransmittance(position, chosen, rng, skip, medium);\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n\n // A light that no bounce ray could have found keeps its whole contribution; an emissive triangle shares it. A toon\n // surface takes all of its direct light this way, because its bounce is not the lobe that lit it.\n let weight = select(1.0, misWeight(chosen.misPdf, reflected.pdf), chosen.misPdf > 0.0 && reflected.pdf > 0.0 && !toon);\n\n let light = chosen.value * through * weight;\n\n // The share of it the diffuse lobe reflects, which the denoiser filters apart from the specular one.\n *diffusePart = min(diffuseValue(surface, view, chosen.direction), reflected.value) * light;\n\n return reflected.value * light;\n}\n\n/** Three's fog factor at a view depth: linear between near and far, or exponential squared. */\nfn fogFactor(depth: f32, kind: f32) -> f32 {\n if (kind == 2.0) {\n let density = frame.fogParams.z;\n\n return clamp(1.0 - exp(-density * density * depth * depth), 0.0, 1.0);\n }\n\n return smoothstep(frame.fogParams.x, frame.fogParams.y, depth);\n}\n\n/** The colour the diffuse signal is divided by at this surface; white where the surface shows what it emits. */\nfn diffuseGuide(surface: Surface) -> vec3<f32> {\n if ((surface.flags & MATERIAL_LIT) == 0u) {\n return vec3<f32>(1.0);\n }\n\n return surface.albedo * (1.0 - surface.metalness) * (1.0 - clamp(surface.transmission, 0.0, 1.0));\n}\n\n/**\n * How often a vertex on a material that scatters inside takes the walk into it instead of its surface lobes: by the\n * share of the light each accounts for, as lobeWeights chooses between lobes. Never all of the time, so the specular\n * layer on top stays reachable.\n */\nfn subsurfaceChance(surface: Surface, view: vec3<f32>) -> f32 {\n if (surface.subsurface <= 0.0 || (surface.flags & MATERIAL_PBR) == 0u) {\n return 0.0;\n }\n\n let diffuse = luminanceOf(surface.albedo * (1.0 - surface.metalness)) * coatTransmittance(surface, view);\n let inside = diffuse * surface.subsurface;\n let outside = diffuse * (1.0 - surface.subsurface) + luminanceOf(specularAlbedo(surface, view)) + 0.02;\n\n return clamp(inside / max(inside + outside, 1e-6), 0.0, 0.95);\n}\n\n/**\n * The single scattering albedo that gives this multiple scattering albedo in a semi-infinite medium: van de Hulst's\n * relation, inverted as Cycles inverts it for its random walk. What the material's colour says it reflects in all\n * is what light keeps at each scattering inside it.\n */\nfn subsurfaceAlbedo(albedo: vec3<f32>) -> vec3<f32> {\n let a = clamp(albedo, vec3<f32>(0.0), vec3<f32>(0.999));\n let s = 4.09712 + 4.20863 * a - sqrt(9.59217 + 41.6808 * a + 17.7126 * a * a);\n\n return vec3<f32>(1.0) - s * s;\n}\n\n/**\n * How far the walk goes before it next scatters, drawn for one colour picked in proportion to what the path still\n * carries of it. choice keeps those odds, which the next step weighs its distance by over all three colours, so each\n * colour keeps its own mean free path and the estimate stays exact (spectral MIS, as Cycles' random walk does it).\n */\nfn walkDistance(throughput: vec3<f32>, sigma: vec3<f32>, u: vec2<f32>, choice: ptr<function, vec3<f32>>) -> f32 {\n let weights = max(throughput, vec3<f32>(0.0));\n let total = weights.x + weights.y + weights.z;\n let odds = select(vec3<f32>(1.0 / 3.0), weights / total, total > 0.0);\n let chosen = select(select(sigma.z, sigma.y, u.x < odds.x + odds.y), sigma.x, u.x < odds.x);\n\n *choice = odds;\n\n return -log(max(1.0 - u.y, 1e-12)) / chosen;\n}\n\n/**\n * Where a walk leaves the material: a white Lambert surface facing out, as Cycles leaves its random walk. The colour\n * was taken inside, scattering by scattering, so nothing tints the light again on the way out.\n */\nfn subsurfaceExit(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {\n var surface: Surface;\n // Turned to face against the reversed ray: outwards, the side the walk is leaving through.\n let out = interpolatedNormal(m, triangle, bary, -direction);\n\n surface.normal = out;\n surface.facing = out;\n surface.albedo = vec3<f32>(1.0);\n surface.flags = MATERIAL_LIT;\n surface.kind = KIND_LAMBERT;\n surface.opacity = 1.0;\n surface.alpha = 1.0;\n surface.blending = BLEND_NORMAL;\n surface.material = m;\n surface.roughness = 1.0;\n surface.ior = 1.5;\n surface.specularF90 = 1.0;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = 1.0;\n surface.gradientMap = NO_TEXTURE;\n\n return surface;\n}\n\n/** A point inside a volume where the light scatters: the medium's phase function in place of a surface's lobes. */\nfn volumePoint(direction: vec3<f32>, albedo: vec3<f32>, anisotropy: f32) -> Surface {\n var surface: Surface;\n\n surface.normal = -direction;\n surface.facing = -direction;\n surface.albedo = albedo;\n surface.flags = MATERIAL_LIT;\n surface.kind = KIND_VOLUME;\n surface.anisotropy = anisotropy;\n surface.opacity = 1.0;\n surface.alpha = 1.0;\n surface.blending = BLEND_NORMAL;\n surface.material = NO_MATERIAL;\n surface.roughness = 1.0;\n surface.ior = 1.0;\n surface.specularF90 = 1.0;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = 1.0;\n surface.gradientMap = NO_TEXTURE;\n\n return surface;\n}\n\n/**\n * The shading normal with the edges near it rounded, as Cycles' bevel node rounds them, by one probe a sample. A\n * point is drawn on a disk of the bevel radius around the shaded point, across an axis picked along the normal half\n * the time and along each tangent a quarter; a short ray along that axis through it finds the surface of the same\n * material there, and that surface's normal turns this one by as much as it differs from the normal here. Over the\n * samples a pixel takes, the normal blends from one face to the next within the radius of every edge, so a box reads\n * as a machined one. A probe that finds nothing, or a normal turned away from the view, leaves the normal as it was.\n */\nfn bevelNormal(m: u32, point: vec3<f32>, surface: Surface, view: vec3<f32>, radius: f32, u: vec2<f32>,\n v: vec2<f32>) -> vec3<f32> {\n let basis = orthonormalBasis(surface.facing);\n var axis = basis[2];\n var first = basis[0];\n var second = basis[1];\n\n if (v.x >= 0.5) {\n axis = select(basis[1], basis[0], v.x < 0.75);\n first = select(basis[2], basis[1], v.x < 0.75);\n second = select(basis[0], basis[2], v.x < 0.75);\n }\n\n let reach = radius * sqrt(u.x);\n let angle = 2.0 * PI * u.y;\n let across = point + first * (reach * cos(angle)) + second * (reach * sin(angle));\n // Along the axis one way or the other at random, from the far edge of the radius across it.\n let way = select(-1.0, 1.0, v.y < 0.5);\n let hit = traceScene(across - axis * (radius * way), axis * way, 2.0 * radius);\n\n if (hit.triangle == NO_HIT || materialIndexOf(hit.triangle) != m) {\n return surface.normal;\n }\n\n // The authored normal there, turned the way this side's own is: the facing normal is the authored one, or its reverse.\n let there = authoredNormal(hit.triangle, vec2<f32>(hit.u, hit.v)) * select(-1.0, 1.0, surface.front);\n let turned = normalize(surface.normal + there - surface.facing);\n\n return select(surface.normal, turned, dot(turned, view) > 0.0 && all(turned == turned));\n}\n\nstruct PathResult {\n radiance: vec3<f32>,\n alpha: f32,\n // How far along the camera ray the first surface it stopped at is, or 1e30 when it stopped at none.\n depth: f32,\n // The denoiser's two signals: what of the radiance came through the first surface's diffuse lobe (and what the camera\n // sees directly), the rest being specular, and what guides each at that surface.\n diffuse: vec3<f32>,\n diffuseAlbedo: vec3<f32>,\n specularAlbedo: vec3<f32>,\n normal: vec3<f32>,\n // Along the camera ray to the first surface, its roughness, and how far the specular continuation went from it.\n guideDistance: f32,\n roughness: f32,\n specularDistance: f32,\n // How much of the first surface scatters light inside, which the denoiser relaxes its tests over.\n subsurface: f32,\n};\n\n/** How many surfaces a path may pass through without scattering (cutouts, culled faces, glass) before it gives up. */\nconst MAX_STEPS: u32 = 64u;\n\n/** Scatterings one walk inside a material may take; the roulette ends most walks well before. */\nconst MAX_WALK: u32 = 128u;\n\nfn traceRadiance(\n rayOrigin: vec3<f32>, rayDirection: vec3<f32>, rayLength: f32, rng: ptr<function, PathSampler>) -> PathResult {\n var radiance = vec3<f32>(0.0);\n var throughput = vec3<f32>(1.0);\n var origin = rayOrigin;\n var direction = rayDirection;\n var limit = rayLength;\n // Solid angle density of the current direction; zero for the camera ray, which no light sampling competes with.\n var bsdfPdf = 0.0;\n // What the volume the ray is travelling through absorbs per unit of distance; zero outside any.\n var absorption = vec3<f32>(0.0);\n // Still on the camera's own line of sight, which transmission keeps a path on.\n var primary = true;\n // Still the camera ray itself, in a straight line: only this one is culled and clipped by the far plane.\n var straight = true;\n var alpha = 0.0;\n var bounce = 0u;\n // The vertex the path last scattered at took all of its direct light by sampling lights: a toon surface.\n var lightsOwnDirect = false;\n // Where the path last scattered and the normal the light tree weighed lights by there, for the heuristic's other half.\n var lastPosition = rayOrigin;\n var lastNormal = vec3<f32>(0.0);\n // That vertex's own environment map, which a ray escaping from it sees instead of the scene's.\n var escapeMap = NO_TEXTURE;\n var escapeIntensity = 1.0;\n // Which colour a dispersive path carries, once a dispersive volume has split it; all three before that.\n var channel = vec3<f32>(1.0);\n // Three's fog, laid over everything the camera sees through the first surface.\n var fogged = false;\n var fogAmount = 0.0;\n var fogStart = vec3<f32>(0.0);\n var fogThroughput = vec3<f32>(0.0);\n // The air between the camera and the first surface it sees: aerial perspective.\n var hazed = false;\n var hazeDistance = 0.0;\n var hazeStart = vec3<f32>(0.0);\n var hazeThroughput = vec3<f32>(0.0);\n var hazeDirection = vec3<f32>(0.0);\n // A built-in material's hooks after <opaque_fragment> edit the colour the camera sees of it, which is everything the\n // path gathers from that surface on.\n var afterMaterial = NO_MATERIAL;\n var afterTriangle = 0u;\n var afterBary = vec2<f32>(0.0);\n var afterDirection = vec3<f32>(0.0);\n var afterAlbedo = vec4<f32>(0.0);\n var afterStart = vec3<f32>(0.0);\n var afterThroughput = vec3<f32>(0.0);\n var depth = 1e30;\n // The denoiser's guides, taken at the first surface the camera ray stops at.\n var guided = false;\n var diffuseAlbedo = vec3<f32>(1.0);\n var specularAlbedoGuide = vec3<f32>(1.0);\n var guideNormal = vec3<f32>(0.0);\n var guideDistance = 1e6;\n var guideRoughness = 1.0;\n var guideSubsurface = 0.0;\n // Where the path first scattered, which decides which of the two signals the rest of it feeds.\n var split = false;\n var splitBase = vec3<f32>(0.0);\n var splitDirect = vec3<f32>(0.0);\n var splitDirectDiffuse = vec3<f32>(0.0);\n var splitDiffuse = true;\n var waitingForSpecular = false;\n var specularDistance = 1e6;\n // Inside a material that scatters light, between two scatterings: its extinction and single scattering albedo per\n // colour, its anisotropy, the odds each colour had when the current step's distance was drawn, and the steps so far.\n var inMedium = false;\n var sigma = vec3<f32>(0.0);\n var walkAlbedo = vec3<f32>(0.0);\n var walkG = 0.0;\n var walkOdds = vec3<f32>(0.0);\n var walkSteps = 0u;\n // Inside a volume: its density per unit of distance, what it keeps at each scattering and its anisotropy. A camera\n // inside one starts there.\n var inVolume = false;\n var volumeDensity = 0.0;\n var volumeAlbedo = vec3<f32>(0.0);\n var volumeG = 0.0;\n\n if (VOLUMES && frame.cameraVolume != NO_MATERIAL && frame.volumesEnabled != 0u) {\n inVolume = true;\n volumeDensity = materials[frame.cameraVolume].volume.w;\n volumeAlbedo = materials[frame.cameraVolume].volume.rgb;\n volumeG = materials[frame.cameraVolume].volumeAnisotropy;\n }\n\n for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {\n let hit = traceScene(origin, direction, limit);\n\n // A step of the walk inside a material that ends in the medium: the light scatters there, keeping each colour's\n // share of it. It needs three numbers, which a plain hash gives for a fraction of what the sequence costs.\n if (SUBSURFACE_WALK && inMedium && hit.triangle == NO_HIT) {\n let travelled = exp(-sigma * limit);\n let density = dot(walkOdds, sigma * travelled);\n // A new direction, the next distance (a colour and how far), and the roulette's chance.\n let turn = hashSample2D(rng);\n let next = hashSample2D(rng);\n let chance = hashSample2D(rng).x;\n\n throughput = throughput * sigma * walkAlbedo * travelled / max(density, 1e-20);\n walkSteps = walkSteps + 1u;\n\n let survival = max3(throughput);\n\n if (walkSteps >= MAX_WALK || survival <= 0.0) {\n break;\n }\n\n if (survival < 0.2) {\n if (chance > survival) {\n break;\n }\n\n throughput = throughput / survival;\n }\n\n origin = origin + direction * limit;\n direction = sampleHenyeyGreenstein(direction, walkG, turn);\n limit = walkDistance(throughput, sigma, next, &walkOdds);\n continue;\n }\n\n // Every vertex draws the same dimensions in the same order, whatever the path then does with them: from the\n // sequence for the first vertices, from a hash past SEQUENCE_DEPTH bounces.\n let deep = bounce >= SEQUENCE_DEPTH;\n let coverage = pathSample2D(rng, deep);\n let pick = pathSample2D(rng, deep);\n let lightSample = pathSample2D(rng, deep);\n let lobe = pathSample2D(rng, deep);\n let bounceSample = pathSample2D(rng, deep);\n var mediumSample = vec2<f32>(0.0);\n\n if (VOLUMES) {\n mediumSample = pathSample2D(rng, deep);\n }\n\n // The walk left the material here, and the surface below is its way out rather than what the material shows.\n var exiting = false;\n\n if (SUBSURFACE_WALK && inMedium) {\n // The walk reached the surface before scattering again: the chance of travelling that far, per colour.\n let travelled = exp(-sigma * hit.distance);\n\n throughput = throughput * travelled / max(dot(walkOdds, travelled), 1e-20);\n inMedium = false;\n exiting = true;\n }\n\n // Inside a volume, the medium may stop the ray before whatever it hit. The distance is drawn now that the segment's\n // length is known, which a homogeneous medium allows; one grey density makes the weight of either outcome one.\n var scatterAt = -1.0;\n\n if (VOLUMES && inVolume && !exiting) {\n let free = -log(max(1.0 - mediumSample.x, 1e-12)) / max(volumeDensity, 1e-12);\n\n if (free < select(hit.distance, limit, hit.triangle == NO_HIT)) {\n scatterAt = free;\n }\n }\n\n let scattering = scatterAt >= 0.0;\n\n // The specular continuation's first hit: how far the reflection the denoiser filters actually is.\n if (waitingForSpecular) {\n specularDistance = select(min(hit.distance, 1e6), 1e6, hit.triangle == NO_HIT);\n waitingForSpecular = false;\n }\n\n if (hit.triangle == NO_HIT && !scattering) {\n if (primary) {\n // A transparent background leaves the pixel empty, which its alpha says.\n if (frame.background.w != 2.0) {\n alpha = 1.0;\n radiance = radiance + throughput * backgroundRadiance(direction);\n }\n } else {\n var sky = ambientRadiance(direction);\n\n if (escapeMap != NO_TEXTURE) {\n sky = sky + sampleEquirect(escapeMap, direction).rgb * escapeIntensity;\n } else {\n sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect, lastPosition);\n }\n\n let contribution = throughput * sky;\n\n radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);\n }\n\n break;\n }\n\n let travel = select(hit.distance, scatterAt, scattering);\n\n throughput = throughput * exp(-absorption * travel);\n\n let bary = vec2<f32>(hit.u, hit.v);\n let point = origin + direction * travel;\n var m = 0u;\n\n if (!scattering) {\n m = materialIndexOf(hit.triangle);\n }\n\n var surface: Surface;\n\n if (SUBSURFACE_WALK && exiting) {\n surface = subsurfaceExit(m, hit.triangle, bary, direction);\n // Seen from outside, along its normal: the side the light leaves through.\n direction = -surface.normal;\n } else if (VOLUMES && scattering) {\n surface = volumePoint(direction, volumeAlbedo, volumeG);\n } else {\n\n // A volume's boundary is not a surface: the ray goes in or out, and the medium is what it meets.\n if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {\n if (dot(authoredNormal(hit.triangle, bary), direction) < 0.0) {\n if (frame.volumesEnabled != 0u) {\n inVolume = true;\n volumeDensity = materials[m].volume.w;\n volumeAlbedo = materials[m].volume.rgb;\n volumeG = materials[m].volumeAnisotropy;\n }\n } else {\n inVolume = false;\n }\n\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n // Three culls the side a material does not draw. Only the camera's rays are culled: a mesh still casts shadows and\n // holds light in from both sides.\n // A planar capture drops what lies behind its plane, for the camera's own ray.\n let clipped = any(frame.clipPlane != vec4<f32>(0.0)) && dot(frame.clipPlane.xyz, point) + frame.clipPlane.w < 0.0;\n\n if (straight && (clipped || isCulled(m, hit.triangle, direction))) {\n origin = point + direction * RAY_EPSILON;\n limit = limit - hit.distance;\n continue;\n }\n\n if (isClipped(m, point)\n || ((materials[m].flags & MATERIAL_WIREFRAME) != 0u && !onWireframe(hit.triangle, point))) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n let base = sampleMap(materials[m].map, hit.triangle, bary);\n\n // A cutout lets the ray through before anything else about the surface is read: the normal map, the other maps and\n // the lobes would only be thrown away. The draw and the comparison are the ones made below, so nothing changes.\n if (coverageFirst(m)) {\n let opacity = coverageOf(m, materialAlphaFrom(m, hit.triangle, bary, base));\n\n if (opacity < 1.0 && coverage.x >= opacity) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n }\n\n surface = resolveSurfaceFrom(hit.triangle, bary, direction, base);\n\n // Deep in the path, light reflects off a material that scatters inside as off a diffuse one of the same colour.\n if (SUBSURFACE_WALK && bounce >= frame.subsurfaceDepth) {\n surface.subsurface = 0.0;\n }\n\n if (BEVEL && frame.bevelEnabled != 0u && materials[m].bevel > 0.0) {\n let disk = hashSample2D(rng);\n let axis = hashSample2D(rng);\n\n surface.normal = bevelNormal(m, point, surface, -direction, materials[m].bevel, disk, axis);\n }\n }\n\n // Blending other than normal: the surface adds its light to, or filters, whatever is behind it, and the path goes on.\n if (surface.blending == BLEND_ADDITIVE || surface.blending == BLEND_MULTIPLY || surface.blending == BLEND_SUBTRACTIVE) {\n // Three blends these with the fragment's alpha whether or not the material is transparent.\n let strength = surface.opacity * clamp(surface.alpha, 0.0, 1.0);\n let color = select(surface.albedo, surface.emissive, (surface.flags & MATERIAL_LIT) == 0u);\n\n if (surface.blending == BLEND_ADDITIVE) {\n var added = surface.emissive;\n\n if ((surface.flags & MATERIAL_LIT) != 0u) {\n var unused: vec3<f32>;\n added = added + directLight(surface, point + surface.normal * RAY_EPSILON, -direction, pick.x, lightSample, rng,\n &unused, select(0.0, volumeDensity, inVolume));\n }\n\n radiance = radiance + throughput * added * strength;\n } else if (surface.blending == BLEND_MULTIPLY) {\n throughput = throughput * mix(vec3<f32>(1.0), color, strength);\n } else {\n throughput = throughput * (vec3<f32>(1.0) - color * strength);\n }\n\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n // A surface that only partly covers its triangle lets the ray straight through the rest of the time. The direction\n // does not change, so the density that found it still stands and the next hit is weighted against the same one.\n if (surface.opacity < 1.0 && coverage.x >= surface.opacity) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n // Three's ShadowMaterial shows its colour only where a light is blocked, and nothing elsewhere.\n if (surface.kind == KIND_SHADOW) {\n let shadow = shadowMaskAt(point + surface.facing * RAY_EPSILON, rng);\n\n if (coverage.y >= shadow * clamp(materials[m].color.w, 0.0, 1.0)) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n if (primary) {\n alpha = 1.0;\n }\n\n radiance = radiance + throughput * surface.albedo;\n break;\n }\n\n if (straight && depth >= 1e30) {\n depth = length(point - rayOrigin);\n }\n\n // A window the camera ray goes straight through is not what the camera sees: what lies behind it is, so the\n // denoiser takes its guides there, and the air its distance. Glass has next to no diffuse colour and one flat\n // normal, which would leave everything seen through it unfiltered. The window's own guides stand until then, for\n // the paths that reflect off it instead.\n let window = surface.transmission > 0.0 && surface.thickness <= 0.0;\n\n if (straight && !guided) {\n guided = !window;\n diffuseAlbedo = diffuseGuide(surface);\n specularAlbedoGuide = select(specularAlbedo(surface, -direction), vec3<f32>(1.0), (surface.flags & MATERIAL_LIT) == 0u);\n guideNormal = surface.normal;\n guideDistance = length(point - rayOrigin);\n guideRoughness = surface.roughness;\n guideSubsurface = select(0.0, surface.subsurface, SUBSURFACE_WALK);\n\n if (surface.kind == KIND_PHONG) {\n // Phong's exponent as the roughness of the GGX lobe that is about as wide.\n guideRoughness = pow(2.0 / (surface.shininess + 2.0), 0.25);\n }\n }\n\n if (primary) {\n alpha = 1.0;\n\n if (!scattering && !fogged && afterMaterial == NO_MATERIAL && materials[m].afterProgram != NO_PROGRAM) {\n afterMaterial = m;\n afterTriangle = hit.triangle;\n afterBary = bary;\n afterDirection = direction;\n afterAlbedo = vec4<f32>(surface.albedo, surface.alpha);\n afterStart = radiance;\n afterThroughput = throughput;\n }\n\n if (!fogged) {\n let everySurface = frame.fogParams.w > 0.0;\n let kind = frame.fogColor.w;\n\n if (everySurface || (kind > 0.0 && (surface.flags & MATERIAL_FOG) != 0u)) {\n let depth = -(frame.worldToCamera * vec4<f32>(point, 1.0)).z;\n\n fogged = true;\n fogAmount = fogFactor(depth, select(kind, 1.0, everySurface));\n fogStart = radiance;\n fogThroughput = throughput;\n }\n }\n\n if (!hazed && !window && frame.atmosphereColor.w > 0.0) {\n hazed = true;\n // From the eye, where the camera ray only starts on the near plane; an orthographic view has no eye.\n hazeDistance = length(point - select(frame.cameraToWorld[3].xyz, rayOrigin, frame.cameraKind == 1u));\n hazeStart = radiance;\n hazeThroughput = throughput;\n hazeDirection = direction;\n }\n\n radiance = radiance + throughput * surface.lightMap;\n }\n\n if (any(surface.emissive > vec3<f32>(0.0))) {\n let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect, lastPosition, lastNormal);\n\n // Seen by the camera or one bounce away this is direct light and stays exact; deeper it may be clamped.\n radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);\n }\n\n // Three does not light a basic, points, line or sprite material, so nothing carries on from one.\n // Past the last bounce a path still crosses a window straight on, which scatters nothing, to find the sky behind\n // it: cut there, the daylight a room gets through its glazing would go missing from every last bounce.\n let spent = bounce >= frame.bounces;\n\n if ((surface.flags & MATERIAL_LIT) == 0u || (spent && !window)) {\n break;\n }\n\n // Lambert and Phong blend their own environment map over the lit colour, which here is a share of the path.\n if (surface.legacyAmount > 0.0) {\n let amount = surface.legacyAmount;\n\n if (surface.combine == 1u) {\n radiance = radiance + throughput * surface.legacyEnv * amount;\n throughput = throughput * (1.0 - amount);\n } else if (surface.combine == 2u) {\n radiance = radiance + throughput * surface.legacyEnv * amount;\n } else {\n throughput = throughput * mix(vec3<f32>(1.0), surface.legacyEnv, amount);\n }\n }\n\n let view = -direction;\n var next = direction;\n var nextOrigin = point + direction * RAY_EPSILON;\n\n // Transmission replaces part of the diffuse layer, so that share of the time the surface is a dielectric instead.\n // Choosing it in proportion to its weight leaves both halves unweighted. The opaque half aims at a light; on a\n // window that lets only part of the light through, it does so on every path, weighed by its share, rather than on\n // the paths that chose it: a sunlit pane seen against a dim room would otherwise be lit on one path in five and\n // dark on the rest, which is the grain glass shows.\n let dielectric = surface.transmission > 0.0 && lobe.y < surface.transmission;\n let windowLight = window && !spent && surface.transmission < 1.0 && surface.subsurface <= 0.0;\n\n if (!dielectric || windowLight) {\n var chance = 0.0;\n\n if (!dielectric) {\n // Light that enters the material: a random walk inside it from here, in place of the share of the diffuse\n // layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.\n // The window's opaque share is a surface like any other, which a spent path stops at.\n if (spent) {\n break;\n }\n\n if (SUBSURFACE_WALK) {\n chance = subsurfaceChance(surface, view);\n }\n\n if (SUBSURFACE_WALK && lobe.x < chance) {\n let medium = materials[surface.material].subsurface;\n\n throughput = throughput\n * ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);\n sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));\n walkAlbedo = subsurfaceAlbedo(surface.albedo);\n walkG = materials[surface.material].subsurfaceAnisotropy;\n walkSteps = 0u;\n\n // Light that scattered inside is diffuse light, which is the signal it feeds.\n if (!split) {\n split = true;\n splitBase = radiance;\n splitDiffuse = true;\n }\n\n inMedium = true;\n primary = false;\n straight = false;\n bsdfPdf = 0.0;\n lightsOwnDirect = false;\n escapeMap = NO_TEXTURE;\n bounce = bounce + 1u;\n direction = cosineDirection(-surface.facing, bounceSample);\n origin = point - surface.facing * RAY_EPSILON;\n limit = walkDistance(throughput, sigma, pick, &walkOdds);\n continue;\n }\n\n throughput = throughput / (1.0 - chance);\n }\n\n let position = point + surface.normal * RAY_EPSILON;\n var directDiffuse: vec3<f32>;\n let lit = select(throughput, throughput * (1.0 - surface.transmission), windowLight);\n let direct = lit * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,\n select(0.0, volumeDensity, inVolume));\n let added = select(direct, clampIndirect(direct), bounce > 0u);\n let before = radiance;\n\n radiance = radiance + added;\n\n if (!dielectric) {\n let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);\n\n // The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.\n if (!split) {\n split = true;\n splitBase = before;\n splitDirect = added;\n splitDirectDiffuse = lit * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),\n bounce > 0u);\n splitDiffuse = !scattered.valid || scattered.diffuse;\n waitingForSpecular = scattered.valid && !scattered.diffuse;\n }\n\n if (!scattered.valid) {\n break;\n }\n\n throughput = throughput * scattered.weight;\n bsdfPdf = scattered.pdf;\n lastPosition = position;\n lastNormal = treeNormal(surface);\n lightsOwnDirect = surface.kind == KIND_TOON;\n escapeMap = surface.envMap;\n escapeIntensity = surface.envMapIntensity;\n next = scattered.direction;\n nextOrigin = position;\n primary = false;\n straight = false;\n bounce = bounce + 1u;\n }\n }\n\n if (dielectric) {\n var ior = surface.ior;\n\n // Three's dispersion: each colour refracts with its own index, spread around the material's. The path picks one\n // colour the first time it enters such a volume and carries only that one from then on.\n if (surface.dispersion > 0.0 && surface.thickness > 0.0) {\n if (all(channel == vec3<f32>(1.0))) {\n let which = min(u32(coverage.y * 3.0), 2u);\n\n channel = vec3<f32>(select(0.0, 1.0, which == 0u), select(0.0, 1.0, which == 1u), select(0.0, 1.0, which == 2u));\n throughput = throughput * channel * 3.0;\n }\n\n let spread = (ior - 1.0) * 0.025 * surface.dispersion;\n\n ior = dot(channel, vec3<f32>(ior - spread, ior, ior + spread));\n }\n\n let crossing = sampleDielectric(surface, view, ior, lobe.y / surface.transmission, bounceSample);\n\n // A reflection off the glass is a scattering, which a spent path no longer has.\n if (!crossing.valid || (spent && !crossing.straight)) {\n break;\n }\n\n // Bending or reflecting off glass is the specular signal.\n if (!split && !crossing.straight) {\n split = true;\n splitBase = radiance;\n splitDiffuse = false;\n waitingForSpecular = true;\n }\n\n throughput = throughput * crossing.weight;\n\n // Straight through a thin wall the direction stands, and with it the density that will weigh the next hit.\n if (!crossing.straight) {\n straight = false;\n next = crossing.direction;\n nextOrigin = point + surface.normal * select(RAY_EPSILON, -RAY_EPSILON, crossing.crossed);\n // A direction no light sampling could have produced: whatever the path finds next keeps its whole value.\n bsdfPdf = 0.0;\n lightsOwnDirect = false;\n escapeMap = surface.envMap;\n escapeIntensity = surface.envMapIntensity;\n\n if (crossing.crossed) {\n absorption = select(vec3<f32>(0.0), surface.absorption, surface.front);\n } else {\n primary = false;\n }\n }\n\n if (!primary) {\n bounce = bounce + 1u;\n }\n }\n\n // A path that carries almost nothing is ended by chance rather than by a cut, which keeps the estimate unbiased.\n let survival = max(throughput.r, max(throughput.g, throughput.b));\n\n if (survival <= 0.0) {\n break;\n }\n\n if (survival < frame.rouletteThreshold && bounce >= frame.rouletteStart) {\n if (pick.y > survival) {\n break;\n }\n\n throughput = throughput / survival;\n }\n\n origin = nextOrigin;\n limit = select(1e30, limit - hit.distance, straight);\n direction = next;\n }\n\n // The diffuse signal: everything before the first scatter, its diffuse direct light, and the rest if the diffuse lobe\n // carried the path on. A path that never scattered is all diffuse, which is what the camera sees directly.\n var diffuse = radiance;\n\n if (split) {\n let after = radiance - splitBase - splitDirect;\n diffuse = splitBase + splitDirectDiffuse + select(vec3<f32>(0.0), after, splitDiffuse);\n }\n\n if (afterMaterial != NO_MATERIAL && all(afterThroughput > vec3<f32>(0.0))) {\n var input = shaderInputAt(afterMaterial, afterTriangle, afterBary, afterDirection);\n input.data = materials[afterMaterial].afterData;\n input.diffuseColor = afterAlbedo;\n input.fragColor = vec4<f32>((radiance - afterStart) / afterThroughput, 1.0);\n\n let edited = runProgram(materials[afterMaterial].afterProgram, input);\n\n radiance = afterStart + afterThroughput * edited.color.rgb;\n // A hook rewrites the colour as a whole: there is no split left to keep.\n diffuse = radiance;\n }\n\n // Three lays its fog after the hooks, over what they made of the surface.\n if (fogged) {\n let fog = frame.fogColor.rgb * fogThroughput * fogAmount;\n\n radiance = fogStart + (radiance - fogStart) * (1.0 - fogAmount) + fog;\n // What came before the fogged surface is diffuse, so the fog's own colour joins that signal.\n diffuse = fogStart + (diffuse - fogStart) * (1.0 - fogAmount) + fog;\n }\n\n // The air takes its share of what lies behind it and adds the light it scatters towards the camera; the background,\n // which already is the sky, is left as it is.\n if (hazed) {\n let through = exp(-frame.atmosphereExtinction.rgb * hazeDistance);\n // The sun lights the air by the phase function of what fills it: Rayleigh's for clean air, a forward lobe for haze\n // (Henyey-Greenstein at 0.76), which is what makes the air glow towards the sun.\n let turn = dot(hazeDirection, frame.atmosphereSun.xyz);\n let phase = mix(3.0 / (16.0 * PI) * (1.0 + turn * turn), henyeyGreenstein(turn, 0.76), frame.atmosphereExtinction.w);\n let air = frame.atmosphereColor.rgb + frame.atmosphereSunColor.rgb * phase;\n let glow = air * hazeThroughput * (vec3<f32>(1.0) - through);\n\n radiance = hazeStart + (radiance - hazeStart) * through + glow;\n diffuse = hazeStart + (diffuse - hazeStart) * through + glow;\n }\n\n var result: PathResult;\n result.radiance = radiance;\n result.alpha = alpha;\n result.depth = depth;\n result.diffuse = diffuse;\n result.diffuseAlbedo = diffuseAlbedo;\n result.specularAlbedo = specularAlbedoGuide;\n result.normal = guideNormal;\n result.guideDistance = guideDistance;\n result.roughness = guideRoughness;\n result.specularDistance = specularDistance;\n result.subsurface = guideSubsurface;\n\n return result;\n}\n\n/** A point on the lens: a disk, or Three-free polygon bokeh shaped as the CPU renderer shapes it. */\nfn lensPoint(u: vec2<f32>) -> vec2<f32> {\n var radius = sqrt(u.x) * frame.lens.x;\n let angle = u.y * 2.0 * PI;\n let blades = frame.lens.z;\n\n if (blades >= 3.0) {\n let wedge = 2.0 * PI / blades;\n radius = radius * cos(PI / blades) / cos((angle - floor(angle / wedge) * wedge) - PI / blades);\n }\n\n return vec2<f32>(cos(angle), sin(angle)) * radius;\n}\n\n/**\n * Where a sample's camera ray starts, which way it points and how far it may go, for any camera: the ray is cast through\n * the inverse of the camera's own projection, from the near plane to the far one, which clip it as Three clips a\n * fragment. A lens moves the origin across its aperture and aims at the point in focus.\n */\nfn cameraRay(pixel: vec2<u32>, jitter: vec2<f32>, lens: vec2<f32>) -> array<vec4<f32>, 2> {\n let uv = (vec2<f32>(pixel) + jitter) / vec2<f32>(frame.resolution);\n var ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);\n\n // A lens's radial distortion: the pixel sees the scene at a radius scaled by the square of its own, which is one at\n // the corners. Positive reaches further out at the edges (barrel), negative less far (pincushion).\n if (frame.lensDistortion != 0.0) {\n let aspect = f32(frame.resolution.x) / f32(frame.resolution.y);\n let radius2 = (ndc.x * ndc.x * aspect * aspect + ndc.y * ndc.y) / (aspect * aspect + 1.0);\n\n ndc = ndc * (1.0 + frame.lensDistortion * radius2);\n }\n\n let ray = ndcRay(ndc);\n\n if (frame.lens.x <= 0.0 || frame.cameraKind == 1u) {\n return ray;\n }\n\n let position = frame.cameraToWorld[3].xyz;\n let forward = -normalize(frame.cameraToWorld[2].xyz);\n let focus = position + ray[1].xyz * (frame.lens.y / max(dot(ray[1].xyz, forward), 1e-6));\n let offset = lensPoint(lens);\n let origin = position + normalize(frame.cameraToWorld[0].xyz) * offset.x + normalize(frame.cameraToWorld[1].xyz) * offset.y;\n\n return array<vec4<f32>, 2>(vec4<f32>(origin, 0.0), vec4<f32>(normalize(focus - origin), 1e30));\n}\n\n/** The window depth GL would store for the first surface the camera ray stopped at: one where it stopped at none. */\nfn windowDepth(point: vec3<f32>, distance: f32) -> f32 {\n if (distance >= 1e30) {\n return 1.0;\n }\n\n let clip = frame.projection * (frame.worldToCamera * vec4<f32>(point, 1.0));\n\n return clamp(clip.z / clip.w * 0.5 + 0.5, 0.0, 1.0);\n}\n\n/** smoothstep as the CPU renderer writes it, which also holds when the two edges meet. */\nfn edge(low: f32, high: f32, value: f32) -> f32 {\n let t = select(select(0.0, 1.0, value >= high), clamp((value - low) / (high - low), 0.0, 1.0), high > low);\n\n return t * t * (3.0 - 2.0 * t);\n}\n\n/** How much of what lies at this point the fog lets through to the camera. */\nfn fogVisibility(point: vec3<f32>) -> f32 {\n let everySurface = frame.fogParams.w > 0.0;\n let kind = frame.fogColor.w;\n\n if (!everySurface && kind <= 0.0) {\n return 1.0;\n }\n\n let depth = -(frame.worldToCamera * vec4<f32>(point, 1.0)).z;\n\n return 1.0 - fogFactor(depth, select(kind, 1.0, everySurface));\n}\n\n/** The most volumetric lights one view ray marches through. */\nconst MAX_VOLUMETRIC: u32 = 8u;\n\n/**\n * Steps of one march. The CPU renderer marches 64 once per pixel; here every path marches, a few hundred times a\n * pixel, each at its own random offset, so a few steps a path add up to a finer integral than 64 once, at a fraction of\n * the shadow rays.\n */\nconst MARCH_STEPS: u32 = 8u;\n\n/**\n * The CPU renderer's volumetric lights (volumetricLight.js): one march across the union of every light's\n * reach along the view ray, Henyey-Greenstein scattering towards the camera, a shadow ray for a spot light that casts\n * one, and the same compression of the sum before it is added over the image.\n */\nfn volumetricGlow(origin: vec3<f32>, direction: vec3<f32>, distance: f32, jitter: f32) -> vec3<f32> {\n let count = min(frame.volumetricCount, MAX_VOLUMETRIC);\n let far = min(distance, 40.0);\n var tStart = far;\n var tEnd = 0.0;\n\n for (var k = 0u; k < count; k = k + 1u) {\n let at = frame.volumetricData + k * 20u;\n let position = extraVec4(at).xyz;\n let spot = extraFloat(at + 3u) > 0.5;\n let axis = extraVec4(at + 4u).xyz;\n let radius = extraFloat(at + 7u);\n let outer = extraFloat(at + 15u);\n let delta = origin - position;\n let b = dot(delta, direction);\n let disc = b * b - dot(delta, delta) + radius * radius;\n\n if (disc <= 0.0) {\n continue;\n }\n\n let start = max(0.0, -b - sqrt(disc));\n let end = min(far, -b + sqrt(disc));\n\n if (end <= start) {\n continue;\n }\n\n var cuts = array<f32, 4>(start, end, end, end);\n var cutCount = 2u;\n let cone = cos(outer);\n\n if (spot) {\n let ar = dot(axis, direction);\n let ac = dot(axis, delta);\n let cos2 = cone * cone;\n let qa = ar * ar - cos2;\n let qb = ar * ac - dot(direction, delta) * cos2;\n let qc = ac * ac - dot(delta, delta) * cos2;\n let discriminant = qb * qb - qa * qc;\n var roots = array<f32, 2>(-1.0, -1.0);\n\n if (abs(qa) < 1e-8) {\n if (abs(qb) > 1e-8) {\n roots[0] = -qc / (2.0 * qb);\n }\n } else if (discriminant >= 0.0) {\n roots[0] = (-qb - sqrt(discriminant)) / qa;\n roots[1] = (-qb + sqrt(discriminant)) / qa;\n }\n\n for (var r = 0u; r < 2u; r = r + 1u) {\n if (roots[r] > start && roots[r] < end) {\n cuts[cutCount] = roots[r];\n cutCount = cutCount + 1u;\n }\n }\n\n // Sort the few cuts in place.\n for (var i = 1u; i < cutCount; i = i + 1u) {\n var j = i;\n\n loop {\n if (j == 0u || cuts[j - 1u] <= cuts[j]) {\n break;\n }\n\n let swap = cuts[j];\n cuts[j] = cuts[j - 1u];\n cuts[j - 1u] = swap;\n j = j - 1u;\n }\n }\n }\n\n for (var segment = 1u; segment < cutCount; segment = segment + 1u) {\n let lo = cuts[segment - 1u];\n let hi = cuts[segment];\n\n if (spot && dot(normalize(delta + direction * ((lo + hi) * 0.5)), axis) < cone) {\n continue;\n }\n\n tStart = min(tStart, lo);\n tEnd = max(tEnd, hi);\n }\n }\n\n if (tEnd <= tStart) {\n return vec3<f32>(0.0);\n }\n\n let step = (tEnd - tStart) / f32(MARCH_STEPS);\n var transmittance: array<f32, 8>;\n\n for (var k = 0u; k < count; k = k + 1u) {\n transmittance[k] = exp(-extraFloat(frame.volumetricData + k * 20u + 11u) * tStart);\n }\n\n var result = vec3<f32>(0.0);\n\n for (var i = 0u; i < MARCH_STEPS; i = i + 1u) {\n let t = tStart + (f32(i) + jitter) * step;\n let p = origin + direction * t;\n let fogged = fogVisibility(p);\n\n if (fogged <= 0.0001) {\n break;\n }\n\n var lit = false;\n\n for (var k = 0u; k < count; k = k + 1u) {\n if (transmittance[k] <= 0.003) {\n continue;\n }\n\n lit = true;\n\n let at = frame.volumetricData + k * 20u;\n let position = extraVec4(at).xyz;\n let spot = extraFloat(at + 3u) > 0.5;\n let axis = extraVec4(at + 4u).xyz;\n let color = extraVec4(at + 8u).rgb;\n let density = extraFloat(at + 11u);\n let angle = extraFloat(at + 12u);\n let penumbra = extraFloat(at + 13u);\n let range = extraFloat(at + 14u);\n let outer = extraFloat(at + 15u);\n let spread = extraFloat(at + 16u);\n let anisotropy = extraFloat(at + 17u);\n let casts = extraFloat(at + 18u) > 0.5;\n let toLight = position - p;\n let dist = length(toLight);\n let toward = toLight / max(dist, 1e-20);\n var attenuation = exp(-dist / (select(10.0, range, range > 0.0) * 0.3));\n\n if (range > 0.0) {\n attenuation = attenuation * (1.0 - edge(range * 0.8, range, dist));\n }\n\n if (spot) {\n let along = -dot(toward, axis);\n let inner = cos(angle * (1.0 - penumbra));\n let core = edge(cos(angle), inner, along);\n let scattered = spread * pow(edge(cos(outer), inner, along), 2.0);\n\n attenuation = attenuation * select(core, max(core, scattered), spread > 0.0);\n }\n\n if (attenuation <= 0.0001) {\n continue;\n }\n\n // Shadowing follows Three's volumetric lights, which only have shadow maps for spots.\n if (spot && casts && frame.shadowsEnabled != 0u && traceAny(p, toward, dist * (1.0 - 1e-3)) != NO_HIT) {\n continue;\n }\n\n let cosine = dot(toward, direction);\n let g = anisotropy;\n let phase = select((1.0 - g * g) / pow(1.0 + g * g - 2.0 * g * cosine, 1.5), 1.0, g == 0.0);\n\n // The air between the camera and this point, exactly: a long step evaluated at its start would overstate it.\n result = result + color * (attenuation * density * step * exp(-density * t) * fogged * phase * 1.5);\n }\n\n if (!lit) {\n break;\n }\n\n for (var k = 0u; k < count; k = k + 1u) {\n transmittance[k] = transmittance[k] * exp(-extraFloat(frame.volumetricData + k * 20u + 11u) * step);\n }\n }\n\n return result / (vec3<f32>(1.0) + result);\n}\n\n/**\n * How far a pixel's estimate may still be from its mean: the standard error of its luminance, over the square root of\n * the luminance, which is roughly how visible that error is once the image is encoded for display.\n */\nfn pixelError(pixel: vec2<u32>) -> f32 {\n let index = pixel.y * frame.resolution.x + pixel.x;\n let sum = accumulation[index];\n\n if (sum.w < 2.0) {\n return 1e30;\n }\n\n let squares = accumulation[frame.pixelCount + index];\n let diffuse = accumulation[4u * frame.pixelCount + index].rgb;\n\n // Each of the denoiser's signals has its own variance; the pixel has converged when both have.\n return max(\n channelError(luminanceOf(diffuse) / sum.w, squares.z / sum.w, sum.w),\n channelError(luminanceOf(sum.rgb - diffuse) / sum.w, squares.w / sum.w, sum.w));\n}\n\nfn channelError(mean: f32, square: f32, samples: f32) -> f32 {\n let variance = max(square - mean * mean, 0.0);\n\n return sqrt(variance / samples) / sqrt(max(mean, 0.0) + 1e-3);\n}\n\n/**\n * Whether a pixel has converged, judged over its neighbourhood: a rare bright path lands in one pixel, and a pixel\n * judged alone would stop where its neighbours had not yet seen one and leave that area too dark.\n */\nfn converged(pixel: vec2<u32>) -> bool {\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let x = clamp(i32(pixel.x) + dx, 0, i32(frame.resolution.x) - 1);\n let y = clamp(i32(pixel.y) + dy, 0, i32(frame.resolution.y) - 1);\n\n if (pixelError(vec2<u32>(u32(x), u32(y))) > frame.adaptiveThreshold) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= frame.bandSize.x || id.y >= frame.bandSize.y) {\n return;\n }\n\n let pixel = frame.bandOrigin + id.xy;\n\n if (pixel.x >= frame.resolution.x || pixel.y >= frame.resolution.y) {\n return;\n }\n\n let index = pixel.y * frame.resolution.x + pixel.x;\n\n // Adaptive sampling: a pixel whose neighbourhood has converged takes no more samples, which leaves the pass to\n // the pixels that still need them.\n if (frame.adaptiveMin > 0u && accumulation[index].w >= f32(frame.adaptiveMin) && converged(pixel)) {\n return;\n }\n\n var radiance = vec3<f32>(0.0);\n var alpha = 0.0;\n var depthSum = 0.0;\n var diffuseSquares = 0.0;\n var specularSquares = 0.0;\n var diffuseSum = vec3<f32>(0.0);\n var diffuseAlbedoSum = vec3<f32>(0.0);\n var specularAlbedoSum = vec3<f32>(0.0);\n var normalSum = vec3<f32>(0.0);\n var distanceSum = 0.0;\n var roughnessSum = 0.0;\n var specularDistanceSum = 0.0;\n var subsurfaceSum = 0.0;\n\n for (var s = 0u; s < frame.sampleCount; s = s + 1u) {\n var rng = makeSampler(pixel, frame.sampleIndex + s);\n let jitter = sample2D(&rng);\n let lens = sample2D(&rng);\n let ray = cameraRay(pixel, jitter, lens);\n let result = traceRadiance(ray[0].xyz, ray[1].xyz, ray[1].w, &rng);\n var sampleAlpha = result.alpha;\n\n var sampled = result.radiance;\n var sampledDiffuse = result.diffuse;\n\n if (frame.volumetricCount > 0u) {\n let glow = volumetricGlow(ray[0].xyz, ray[1].xyz, result.depth, sample2D(&rng).x);\n\n // As the CPU renderer composites it: added over the image, and raising the coverage just enough to hold it.\n sampled = sampled + glow;\n sampledDiffuse = sampledDiffuse + glow;\n sampleAlpha = max(sampleAlpha, max3(glow));\n }\n\n let sampledSpecular = sampled - sampledDiffuse;\n\n radiance = radiance + sampled;\n diffuseSum = diffuseSum + sampledDiffuse;\n diffuseSquares = diffuseSquares + luminanceOf(sampledDiffuse) * luminanceOf(sampledDiffuse);\n specularSquares = specularSquares + luminanceOf(sampledSpecular) * luminanceOf(sampledSpecular);\n diffuseAlbedoSum = diffuseAlbedoSum + result.diffuseAlbedo;\n specularAlbedoSum = specularAlbedoSum + result.specularAlbedo;\n normalSum = normalSum + result.normal;\n distanceSum = distanceSum + result.guideDistance;\n roughnessSum = roughnessSum + result.roughness;\n specularDistanceSum = specularDistanceSum + result.specularDistance;\n subsurfaceSum = subsurfaceSum + result.subsurface;\n\n alpha = alpha + sampleAlpha;\n depthSum = depthSum + windowDepth(ray[0].xyz + ray[1].xyz * min(result.depth, 1e20), result.depth);\n }\n\n let coverage = accumulation[frame.pixelCount + index];\n accumulation[index] = accumulation[index] + vec4<f32>(radiance, f32(frame.sampleCount));\n accumulation[frame.pixelCount + index] = vec4<f32>(\n coverage.x + alpha, coverage.y + depthSum, coverage.z + diffuseSquares, coverage.w + specularSquares);\n // The denoiser's signals and guides, summed like the radiance so they average over the pixel's samples.\n let pc = frame.pixelCount;\n accumulation[2u * pc + index] = accumulation[2u * pc + index] + vec4<f32>(diffuseAlbedoSum, roughnessSum);\n accumulation[3u * pc + index] = accumulation[3u * pc + index] + vec4<f32>(normalSum, distanceSum);\n accumulation[4u * pc + index] = accumulation[4u * pc + index] + vec4<f32>(diffuseSum, specularDistanceSum);\n accumulation[5u * pc + index] = accumulation[5u * pc + index] + vec4<f32>(specularAlbedoSum, subsurfaceSum);\n // This row still has work: every pixel that traced writes the same value, so the race is harmless.\n accumulation[6u * pc + pixel.y] = vec4<f32>(1.0);\n}\n";
|
|
13
|
+
export declare const PATH_TRACE_WGSL = "\n/** Light from an ambient or hemisphere light, which this renderer turns into an equally bright surrounding. */\nfn ambientRadiance(direction: vec3<f32>) -> vec3<f32> {\n let latitude = clamp(dot(direction, frame.ambientAxis.xyz) * 0.5 + 0.5, 0.0, 1.0);\n\n return mix(frame.ambientGround.rgb, frame.ambientSky.rgb, latitude) + probeRadiance(direction);\n}\n\n/**\n * A LightProbe's light. Three's coefficients are radiance projected on the SH3 basis, which its shader convolves with\n * the cosine to get irradiance; evaluated directly they are the surrounding that delivers exactly that irradiance.\n */\nfn probeRadiance(d: vec3<f32>) -> vec3<f32> {\n let radiance = frame.probe0.rgb * 0.282095\n + frame.probe1.rgb * (0.488603 * d.y)\n + frame.probe2.rgb * (0.488603 * d.z)\n + frame.probe3.rgb * (0.488603 * d.x)\n + frame.probe4.rgb * (1.092548 * d.x * d.y)\n + frame.probe5.rgb * (1.092548 * d.y * d.z)\n + frame.probe6.rgb * (0.315392 * (3.0 * d.z * d.z - 1.0))\n + frame.probe7.rgb * (1.092548 * d.x * d.z)\n + frame.probe8.rgb * (0.546274 * (d.x * d.x - d.y * d.y));\n\n return max(radiance, vec3<f32>(0.0));\n}\n\n/** What the camera sees where nothing is in the way, as the background setting says. */\nfn backgroundRadiance(direction: vec3<f32>) -> vec3<f32> {\n if (frame.background.w == 1.0) {\n return frame.background.rgb;\n }\n\n // A procedural sky paints the background with the strength it lights with, and its foreground over it. The CPU\n // renderer leaves that strength out of what the camera sees, which a renderer exposing for the light cannot: the sky\n // that lights a wall five times brighter than itself would show as a dark band behind it.\n if (frame.skyProgram != NO_PROGRAM || frame.skyIntensity > 0.0) {\n if (frame.skyBackground == 0u) {\n return vec3<f32>(0.0);\n }\n\n if (frame.skyProgram != NO_PROGRAM) {\n var input: ShaderInput;\n input.data = frame.skyData;\n input.direction = direction;\n input.diffuseColor = vec4<f32>(1.0);\n\n return runProgram(frame.skyProgram, input).color.rgb * frame.skyIntensity;\n }\n\n return environmentRadiance(direction);\n }\n\n return environmentRadiance(direction);\n}\n\n/**\n * How much of the sky a bounce ray that escaped keeps. Direct sampling aims at the bright parts of an image too, so the\n * two share it; when direct sampling took all of it, as it does from a toon surface, the bounce keeps none.\n */\nfn escapeWeight(direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>) -> f32 {\n if (frame.environmentPick <= 0.0) {\n return 1.0;\n }\n\n if (lightsOwnDirect) {\n return 0.0;\n }\n\n if (bsdfPdf <= 0.0) {\n return 1.0;\n }\n\n // The sky was aimed at from the vertex the bounce left, through its windows as well when that vertex is inside.\n return misWeight(bsdfPdf, frame.environmentPick * skyPdf(scatteredAt, direction, portalChance(scatteredAt)));\n}\n\n/** Scales a contribution down to the luminance ceiling, which trades a little bias for the fireflies it removes. */\nfn clampIndirect(contribution: vec3<f32>) -> vec3<f32> {\n let luminance = luminanceOf(contribution);\n\n if (frame.clampIndirect <= 0.0 || luminance <= frame.clampIndirect) {\n return contribution;\n }\n\n return contribution * (frame.clampIndirect / luminance);\n}\n\n/**\n * How much of the emission a bounce ray found the renderer keeps. Direct sampling had its own chance at this triangle,\n * and the heuristic splits the two so neither counts it twice.\n */\nfn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect: bool, scatteredAt: vec3<f32>,\n normal: vec3<f32>) -> f32 {\n if (frame.areaLightCount == 0u) {\n return 1.0;\n }\n\n // From the vertex the bounce left, which is where direct sampling would have picked it from.\n let areaPdf = lightPdfOfTriangle(hit.triangle, scatteredAt, normal);\n\n if (areaPdf <= 0.0) {\n return 1.0;\n }\n\n if (lightsOwnDirect) {\n return 0.0;\n }\n\n if (bsdfPdf <= 0.0) {\n return 1.0;\n }\n\n let cosLight = max(abs(dot(triangleNormal(hit.triangle), direction)), 1e-6);\n\n return misWeight(bsdfPdf, areaPdf * hit.distance * hit.distance / cosLight);\n}\n\n/**\n * The normal the light tree weighs lights by, or none where light from behind the surface counts too: a point in a\n * volume, a toon ramp read all round, a clearcoat that may face what its base turns from, and transmission.\n */\nfn treeNormal(surface: Surface) -> vec3<f32> {\n if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0\n || (GRASS && any(surface.translucent > vec3<f32>(0.0)))) {\n return vec3<f32>(0.0);\n }\n\n return surface.normal;\n}\n\n/** One shadow ray from the surface towards a light, weighted against the chance the bounce would have found it too. */\nfn directLight(\n surface: Surface,\n position: vec3<f32>,\n view: vec3<f32>,\n pick: f32,\n u: vec2<f32>,\n rng: ptr<function, PathSampler>,\n diffusePart: ptr<function, vec3<f32>>,\n medium: f32) -> vec3<f32> {\n *diffusePart = vec3<f32>(0.0);\n\n let chosen = sampleLight(position, treeNormal(surface), pick, u);\n\n if (!chosen.valid) {\n return vec3<f32>(0.0);\n }\n\n // A material with its own environment map sees that map in place of the scene's surroundings.\n if (chosen.kind == LIGHT_ENVIRONMENT && surface.envMap != NO_TEXTURE) {\n return vec3<f32>(0.0);\n }\n\n let reflected = evaluateBsdf(surface, view, chosen.direction);\n\n if (all(reflected.value <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n\n let toon = surface.kind == KIND_TOON;\n // Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.\n let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);\n var shadowFrom = position;\n\n // Light from behind a translucent surface is looked for from its far side, past the surface itself.\n if (GRASS && any(surface.translucent > vec3<f32>(0.0)) && dot(chosen.direction, surface.facing) < 0.0) {\n shadowFrom = position - (surface.normal + surface.facing) * RAY_EPSILON;\n }\n\n let through = lightTransmittance(shadowFrom, chosen, rng, skip, medium);\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n\n // A light that no bounce ray could have found keeps its whole contribution; an emissive triangle shares it. A toon\n // surface takes all of its direct light this way, because its bounce is not the lobe that lit it.\n let weight = select(1.0, misWeight(chosen.misPdf, reflected.pdf), chosen.misPdf > 0.0 && reflected.pdf > 0.0 && !toon);\n\n let light = chosen.value * through * weight;\n\n // The share of it the diffuse lobe reflects, which the denoiser filters apart from the specular one.\n *diffusePart = min(diffuseValue(surface, view, chosen.direction), reflected.value) * light;\n\n return reflected.value * light;\n}\n\n/** Three's fog factor at a view depth: linear between near and far, or exponential squared. */\nfn fogFactor(depth: f32, kind: f32) -> f32 {\n if (kind == 2.0) {\n let density = frame.fogParams.z;\n\n return clamp(1.0 - exp(-density * density * depth * depth), 0.0, 1.0);\n }\n\n return smoothstep(frame.fogParams.x, frame.fogParams.y, depth);\n}\n\n/** The colour the diffuse signal is divided by at this surface; white where the surface shows what it emits. */\nfn diffuseGuide(surface: Surface) -> vec3<f32> {\n if ((surface.flags & MATERIAL_LIT) == 0u) {\n return vec3<f32>(1.0);\n }\n\n return surface.albedo * (1.0 - surface.metalness) * (1.0 - clamp(surface.transmission, 0.0, 1.0));\n}\n\n/**\n * How often a vertex on a material that scatters inside takes the walk into it instead of its surface lobes: by the\n * share of the light each accounts for, as lobeWeights chooses between lobes. Never all of the time, so the specular\n * layer on top stays reachable.\n */\nfn subsurfaceChance(surface: Surface, view: vec3<f32>) -> f32 {\n if (surface.subsurface <= 0.0 || (surface.flags & MATERIAL_PBR) == 0u) {\n return 0.0;\n }\n\n let diffuse = luminanceOf(surface.albedo * (1.0 - surface.metalness)) * coatTransmittance(surface, view);\n let inside = diffuse * surface.subsurface;\n let outside = diffuse * (1.0 - surface.subsurface) + luminanceOf(specularAlbedo(surface, view)) + 0.02;\n\n return clamp(inside / max(inside + outside, 1e-6), 0.0, 0.95);\n}\n\n/**\n * The single scattering albedo that gives this multiple scattering albedo in a semi-infinite medium: van de Hulst's\n * relation, inverted as Cycles inverts it for its random walk. What the material's colour says it reflects in all\n * is what light keeps at each scattering inside it.\n */\nfn subsurfaceAlbedo(albedo: vec3<f32>) -> vec3<f32> {\n let a = clamp(albedo, vec3<f32>(0.0), vec3<f32>(0.999));\n let s = 4.09712 + 4.20863 * a - sqrt(9.59217 + 41.6808 * a + 17.7126 * a * a);\n\n return vec3<f32>(1.0) - s * s;\n}\n\n/**\n * How far the walk goes before it next scatters, drawn for one colour picked in proportion to what the path still\n * carries of it. choice keeps those odds, which the next step weighs its distance by over all three colours, so each\n * colour keeps its own mean free path and the estimate stays exact (spectral MIS, as Cycles' random walk does it).\n */\nfn walkDistance(throughput: vec3<f32>, sigma: vec3<f32>, u: vec2<f32>, choice: ptr<function, vec3<f32>>) -> f32 {\n let weights = max(throughput, vec3<f32>(0.0));\n let total = weights.x + weights.y + weights.z;\n let odds = select(vec3<f32>(1.0 / 3.0), weights / total, total > 0.0);\n let chosen = select(select(sigma.z, sigma.y, u.x < odds.x + odds.y), sigma.x, u.x < odds.x);\n\n *choice = odds;\n\n return -log(max(1.0 - u.y, 1e-12)) / chosen;\n}\n\n/**\n * Where a walk leaves the material: a white Lambert surface facing out, as Cycles leaves its random walk. The colour\n * was taken inside, scattering by scattering, so nothing tints the light again on the way out.\n */\nfn subsurfaceExit(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {\n var surface: Surface;\n // Turned to face against the reversed ray: outwards, the side the walk is leaving through.\n let out = interpolatedNormal(m, triangle, bary, -direction);\n\n surface.normal = out;\n surface.facing = out;\n surface.albedo = vec3<f32>(1.0);\n surface.flags = MATERIAL_LIT;\n surface.kind = KIND_LAMBERT;\n surface.opacity = 1.0;\n surface.alpha = 1.0;\n surface.blending = BLEND_NORMAL;\n surface.material = m;\n surface.roughness = 1.0;\n surface.ior = 1.5;\n surface.specularF90 = 1.0;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = 1.0;\n surface.gradientMap = NO_TEXTURE;\n\n return surface;\n}\n\n/** A point inside a volume where the light scatters: the medium's phase function in place of a surface's lobes. */\nfn volumePoint(direction: vec3<f32>, albedo: vec3<f32>, anisotropy: f32) -> Surface {\n var surface: Surface;\n\n surface.normal = -direction;\n surface.facing = -direction;\n surface.albedo = albedo;\n surface.flags = MATERIAL_LIT;\n surface.kind = KIND_VOLUME;\n surface.anisotropy = anisotropy;\n surface.opacity = 1.0;\n surface.alpha = 1.0;\n surface.blending = BLEND_NORMAL;\n surface.material = NO_MATERIAL;\n surface.roughness = 1.0;\n surface.ior = 1.0;\n surface.specularF90 = 1.0;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = 1.0;\n surface.gradientMap = NO_TEXTURE;\n\n return surface;\n}\n\n/**\n * The shading normal with the edges near it rounded, as Cycles' bevel node rounds them, by one probe a sample. A\n * point is drawn on a disk of the bevel radius around the shaded point, across an axis picked along the normal half\n * the time and along each tangent a quarter; a short ray along that axis through it finds the surface of the same\n * material there, and that surface's normal turns this one by as much as it differs from the normal here. Over the\n * samples a pixel takes, the normal blends from one face to the next within the radius of every edge, so a box reads\n * as a machined one. A probe that finds nothing, or a normal turned away from the view, leaves the normal as it was.\n */\nfn bevelNormal(m: u32, point: vec3<f32>, surface: Surface, view: vec3<f32>, radius: f32, u: vec2<f32>,\n v: vec2<f32>) -> vec3<f32> {\n let basis = orthonormalBasis(surface.facing);\n var axis = basis[2];\n var first = basis[0];\n var second = basis[1];\n\n if (v.x >= 0.5) {\n axis = select(basis[1], basis[0], v.x < 0.75);\n first = select(basis[2], basis[1], v.x < 0.75);\n second = select(basis[0], basis[2], v.x < 0.75);\n }\n\n let reach = radius * sqrt(u.x);\n let angle = 2.0 * PI * u.y;\n let across = point + first * (reach * cos(angle)) + second * (reach * sin(angle));\n // Along the axis one way or the other at random, from the far edge of the radius across it.\n let way = select(-1.0, 1.0, v.y < 0.5);\n let hit = traceScene(across - axis * (radius * way), axis * way, 2.0 * radius);\n\n if (hit.triangle == NO_HIT || materialIndexOf(hit.triangle) != m) {\n return surface.normal;\n }\n\n // The authored normal there, turned the way this side's own is: the facing normal is the authored one, or its reverse.\n let there = authoredNormal(hit.triangle, vec2<f32>(hit.u, hit.v)) * select(-1.0, 1.0, surface.front);\n let turned = normalize(surface.normal + there - surface.facing);\n\n return select(surface.normal, turned, dot(turned, view) > 0.0 && all(turned == turned));\n}\n\nstruct PathResult {\n radiance: vec3<f32>,\n alpha: f32,\n // How far along the camera ray the first surface it stopped at is, or 1e30 when it stopped at none.\n depth: f32,\n // The denoiser's two signals: what of the radiance came through the first surface's diffuse lobe (and what the camera\n // sees directly), the rest being specular, and what guides each at that surface.\n diffuse: vec3<f32>,\n diffuseAlbedo: vec3<f32>,\n specularAlbedo: vec3<f32>,\n normal: vec3<f32>,\n // Along the camera ray to the first surface, its roughness, and how far the specular continuation went from it.\n guideDistance: f32,\n roughness: f32,\n specularDistance: f32,\n // How much of the first surface scatters light inside, which the denoiser relaxes its tests over.\n subsurface: f32,\n};\n\n/** How many surfaces a path may pass through without scattering (cutouts, culled faces, glass) before it gives up. */\nconst MAX_STEPS: u32 = 64u;\n\n/** Scatterings one walk inside a material may take; the roulette ends most walks well before. */\nconst MAX_WALK: u32 = 128u;\n\nfn traceRadiance(\n rayOrigin: vec3<f32>, rayDirection: vec3<f32>, rayLength: f32, rng: ptr<function, PathSampler>) -> PathResult {\n var radiance = vec3<f32>(0.0);\n var throughput = vec3<f32>(1.0);\n var origin = rayOrigin;\n var direction = rayDirection;\n var limit = rayLength;\n // Solid angle density of the current direction; zero for the camera ray, which no light sampling competes with.\n var bsdfPdf = 0.0;\n // What the volume the ray is travelling through absorbs per unit of distance; zero outside any.\n var absorption = vec3<f32>(0.0);\n // Still on the camera's own line of sight, which transmission keeps a path on.\n var primary = true;\n // Still the camera ray itself, in a straight line: only this one is culled and clipped by the far plane.\n var straight = true;\n var alpha = 0.0;\n var bounce = 0u;\n // The vertex the path last scattered at took all of its direct light by sampling lights: a toon surface.\n var lightsOwnDirect = false;\n // Where the path last scattered and the normal the light tree weighed lights by there, for the heuristic's other half.\n var lastPosition = rayOrigin;\n var lastNormal = vec3<f32>(0.0);\n // That vertex's own environment map, which a ray escaping from it sees instead of the scene's.\n var escapeMap = NO_TEXTURE;\n var escapeIntensity = 1.0;\n // Which colour a dispersive path carries, once a dispersive volume has split it; all three before that.\n var channel = vec3<f32>(1.0);\n // Three's fog, laid over everything the camera sees through the first surface.\n var fogged = false;\n var fogAmount = 0.0;\n var fogStart = vec3<f32>(0.0);\n var fogThroughput = vec3<f32>(0.0);\n // The air between the camera and the first surface it sees: aerial perspective.\n var hazed = false;\n var hazeDistance = 0.0;\n var hazeStart = vec3<f32>(0.0);\n var hazeThroughput = vec3<f32>(0.0);\n var hazeDirection = vec3<f32>(0.0);\n // A built-in material's hooks after <opaque_fragment> edit the colour the camera sees of it, which is everything the\n // path gathers from that surface on.\n var afterMaterial = NO_MATERIAL;\n var afterTriangle = 0u;\n var afterBary = vec2<f32>(0.0);\n var afterDirection = vec3<f32>(0.0);\n var afterAlbedo = vec4<f32>(0.0);\n var afterStart = vec3<f32>(0.0);\n var afterThroughput = vec3<f32>(0.0);\n var depth = 1e30;\n // The denoiser's guides, taken at the first surface the camera ray stops at.\n var guided = false;\n var diffuseAlbedo = vec3<f32>(1.0);\n var specularAlbedoGuide = vec3<f32>(1.0);\n var guideNormal = vec3<f32>(0.0);\n var guideDistance = 1e6;\n var guideRoughness = 1.0;\n var guideSubsurface = 0.0;\n // Where the path first scattered, which decides which of the two signals the rest of it feeds.\n var split = false;\n var splitBase = vec3<f32>(0.0);\n var splitDirect = vec3<f32>(0.0);\n var splitDirectDiffuse = vec3<f32>(0.0);\n var splitDiffuse = true;\n var waitingForSpecular = false;\n var specularDistance = 1e6;\n // Inside a material that scatters light, between two scatterings: its extinction and single scattering albedo per\n // colour, its anisotropy, the odds each colour had when the current step's distance was drawn, and the steps so far.\n var inMedium = false;\n var sigma = vec3<f32>(0.0);\n var walkAlbedo = vec3<f32>(0.0);\n var walkG = 0.0;\n var walkOdds = vec3<f32>(0.0);\n var walkSteps = 0u;\n // Inside a volume: its density per unit of distance, what it keeps at each scattering and its anisotropy. A camera\n // inside one starts there.\n var inVolume = false;\n var volumeDensity = 0.0;\n var volumeAlbedo = vec3<f32>(0.0);\n var volumeG = 0.0;\n\n if (VOLUMES && frame.cameraVolume != NO_MATERIAL && frame.volumesEnabled != 0u) {\n inVolume = true;\n volumeDensity = materials[frame.cameraVolume].volume.w;\n volumeAlbedo = materials[frame.cameraVolume].volume.rgb;\n volumeG = materials[frame.cameraVolume].volumeAnisotropy;\n }\n\n for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {\n var hit = traceScene(origin, direction, limit);\n // A blade of grass in front of whatever the ray hit. A walk inside a material never meets one.\n var grass: GrassHit;\n\n if (GRASS && !(SUBSURFACE_WALK && inMedium)) {\n grass = traceGrass(origin, direction, hit.distance);\n\n if (grass.found) {\n hit.distance = grass.distance;\n hit.triangle = GRASS_TRIANGLE;\n hit.u = 0.0;\n hit.v = 0.0;\n }\n }\n\n // A step of the walk inside a material that ends in the medium: the light scatters there, keeping each colour's\n // share of it. It needs three numbers, which a plain hash gives for a fraction of what the sequence costs.\n if (SUBSURFACE_WALK && inMedium && hit.triangle == NO_HIT) {\n let travelled = exp(-sigma * limit);\n let density = dot(walkOdds, sigma * travelled);\n // A new direction, the next distance (a colour and how far), and the roulette's chance.\n let turn = hashSample2D(rng);\n let next = hashSample2D(rng);\n let chance = hashSample2D(rng).x;\n\n throughput = throughput * sigma * walkAlbedo * travelled / max(density, 1e-20);\n walkSteps = walkSteps + 1u;\n\n let survival = max3(throughput);\n\n if (walkSteps >= MAX_WALK || survival <= 0.0) {\n break;\n }\n\n if (survival < 0.2) {\n if (chance > survival) {\n break;\n }\n\n throughput = throughput / survival;\n }\n\n origin = origin + direction * limit;\n direction = sampleHenyeyGreenstein(direction, walkG, turn);\n limit = walkDistance(throughput, sigma, next, &walkOdds);\n continue;\n }\n\n // Every vertex draws the same dimensions in the same order, whatever the path then does with them: from the\n // sequence for the first vertices, from a hash past SEQUENCE_DEPTH bounces.\n let deep = bounce >= SEQUENCE_DEPTH;\n let coverage = pathSample2D(rng, deep);\n let pick = pathSample2D(rng, deep);\n let lightSample = pathSample2D(rng, deep);\n let lobe = pathSample2D(rng, deep);\n let bounceSample = pathSample2D(rng, deep);\n var mediumSample = vec2<f32>(0.0);\n\n if (VOLUMES) {\n mediumSample = pathSample2D(rng, deep);\n }\n\n // The walk left the material here, and the surface below is its way out rather than what the material shows.\n var exiting = false;\n\n if (SUBSURFACE_WALK && inMedium) {\n // The walk reached the surface before scattering again: the chance of travelling that far, per colour.\n let travelled = exp(-sigma * hit.distance);\n\n throughput = throughput * travelled / max(dot(walkOdds, travelled), 1e-20);\n inMedium = false;\n exiting = true;\n }\n\n // Inside a volume, the medium may stop the ray before whatever it hit. The distance is drawn now that the segment's\n // length is known, which a homogeneous medium allows; one grey density makes the weight of either outcome one.\n var scatterAt = -1.0;\n\n if (VOLUMES && inVolume && !exiting) {\n let free = -log(max(1.0 - mediumSample.x, 1e-12)) / max(volumeDensity, 1e-12);\n\n if (free < select(hit.distance, limit, hit.triangle == NO_HIT)) {\n scatterAt = free;\n }\n }\n\n let scattering = scatterAt >= 0.0;\n\n // The specular continuation's first hit: how far the reflection the denoiser filters actually is.\n if (waitingForSpecular) {\n specularDistance = select(min(hit.distance, 1e6), 1e6, hit.triangle == NO_HIT);\n waitingForSpecular = false;\n }\n\n if (hit.triangle == NO_HIT && !scattering) {\n if (primary) {\n // A transparent background leaves the pixel empty, which its alpha says.\n if (frame.background.w != 2.0) {\n alpha = 1.0;\n radiance = radiance + throughput * backgroundRadiance(direction);\n }\n } else {\n var sky = ambientRadiance(direction);\n\n if (escapeMap != NO_TEXTURE) {\n sky = sky + sampleEquirect(escapeMap, direction).rgb * escapeIntensity;\n } else {\n sky = sky + environmentRadiance(direction) * escapeWeight(direction, bsdfPdf, lightsOwnDirect, lastPosition);\n }\n\n let contribution = throughput * sky;\n\n radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);\n }\n\n break;\n }\n\n let travel = select(hit.distance, scatterAt, scattering);\n\n throughput = throughput * exp(-absorption * travel);\n\n let bary = vec2<f32>(hit.u, hit.v);\n let point = origin + direction * travel;\n var m = 0u;\n\n if (!scattering && !(GRASS && grass.found)) {\n m = materialIndexOf(hit.triangle);\n }\n\n var surface: Surface;\n\n if (SUBSURFACE_WALK && exiting) {\n surface = subsurfaceExit(m, hit.triangle, bary, direction);\n // Seen from outside, along its normal: the side the light leaves through.\n direction = -surface.normal;\n } else if (VOLUMES && scattering) {\n surface = volumePoint(direction, volumeAlbedo, volumeG);\n } else if (GRASS && grass.found) {\n surface = grassSurface(grass, direction);\n } else {\n\n // A volume's boundary is not a surface: the ray goes in or out, and the medium is what it meets.\n if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {\n if (dot(authoredNormal(hit.triangle, bary), direction) < 0.0) {\n if (frame.volumesEnabled != 0u) {\n inVolume = true;\n volumeDensity = materials[m].volume.w;\n volumeAlbedo = materials[m].volume.rgb;\n volumeG = materials[m].volumeAnisotropy;\n }\n } else {\n inVolume = false;\n }\n\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n // Three culls the side a material does not draw. Only the camera's rays are culled: a mesh still casts shadows and\n // holds light in from both sides.\n // A planar capture drops what lies behind its plane, for the camera's own ray.\n let clipped = any(frame.clipPlane != vec4<f32>(0.0)) && dot(frame.clipPlane.xyz, point) + frame.clipPlane.w < 0.0;\n\n if (straight && (clipped || isCulled(m, hit.triangle, direction))) {\n origin = point + direction * RAY_EPSILON;\n limit = limit - hit.distance;\n continue;\n }\n\n if (isClipped(m, point)\n || ((materials[m].flags & MATERIAL_WIREFRAME) != 0u && !onWireframe(hit.triangle, point))) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n let base = sampleMap(materials[m].map, hit.triangle, bary);\n\n // A cutout lets the ray through before anything else about the surface is read: the normal map, the other maps and\n // the lobes would only be thrown away. The draw and the comparison are the ones made below, so nothing changes.\n if (coverageFirst(m)) {\n let opacity = coverageOf(m, materialAlphaFrom(m, hit.triangle, bary, base));\n\n if (opacity < 1.0 && coverage.x >= opacity) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n }\n\n surface = resolveSurfaceFrom(hit.triangle, bary, direction, base);\n\n // Deep in the path, light reflects off a material that scatters inside as off a diffuse one of the same colour.\n if (SUBSURFACE_WALK && bounce >= frame.subsurfaceDepth) {\n surface.subsurface = 0.0;\n }\n\n if (BEVEL && frame.bevelEnabled != 0u && materials[m].bevel > 0.0) {\n let disk = hashSample2D(rng);\n let axis = hashSample2D(rng);\n\n surface.normal = bevelNormal(m, point, surface, -direction, materials[m].bevel, disk, axis);\n }\n }\n\n // Blending other than normal: the surface adds its light to, or filters, whatever is behind it, and the path goes on.\n if (surface.blending == BLEND_ADDITIVE || surface.blending == BLEND_MULTIPLY || surface.blending == BLEND_SUBTRACTIVE) {\n // Three blends these with the fragment's alpha whether or not the material is transparent.\n let strength = surface.opacity * clamp(surface.alpha, 0.0, 1.0);\n let color = select(surface.albedo, surface.emissive, (surface.flags & MATERIAL_LIT) == 0u);\n\n if (surface.blending == BLEND_ADDITIVE) {\n var added = surface.emissive;\n\n if ((surface.flags & MATERIAL_LIT) != 0u) {\n var unused: vec3<f32>;\n added = added + directLight(surface, point + surface.normal * RAY_EPSILON, -direction, pick.x, lightSample, rng,\n &unused, select(0.0, volumeDensity, inVolume));\n }\n\n radiance = radiance + throughput * added * strength;\n } else if (surface.blending == BLEND_MULTIPLY) {\n throughput = throughput * mix(vec3<f32>(1.0), color, strength);\n } else {\n throughput = throughput * (vec3<f32>(1.0) - color * strength);\n }\n\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n // A surface that only partly covers its triangle lets the ray straight through the rest of the time. The direction\n // does not change, so the density that found it still stands and the next hit is weighted against the same one.\n if (surface.opacity < 1.0 && coverage.x >= surface.opacity) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n // Three's ShadowMaterial shows its colour only where a light is blocked, and nothing elsewhere.\n if (surface.kind == KIND_SHADOW) {\n let shadow = shadowMaskAt(point + surface.facing * RAY_EPSILON, rng);\n\n if (coverage.y >= shadow * clamp(materials[m].color.w, 0.0, 1.0)) {\n origin = point + direction * RAY_EPSILON;\n limit = select(1e30, limit - hit.distance, straight);\n continue;\n }\n\n if (primary) {\n alpha = 1.0;\n }\n\n radiance = radiance + throughput * surface.albedo;\n break;\n }\n\n if (straight && depth >= 1e30) {\n depth = length(point - rayOrigin);\n }\n\n // A window the camera ray goes straight through is not what the camera sees: what lies behind it is, so the\n // denoiser takes its guides there, and the air its distance. Glass has next to no diffuse colour and one flat\n // normal, which would leave everything seen through it unfiltered. The window's own guides stand until then, for\n // the paths that reflect off it instead.\n let window = surface.transmission > 0.0 && surface.thickness <= 0.0;\n\n if (straight && !guided) {\n guided = !window;\n diffuseAlbedo = diffuseGuide(surface);\n specularAlbedoGuide = select(specularAlbedo(surface, -direction), vec3<f32>(1.0), (surface.flags & MATERIAL_LIT) == 0u);\n guideNormal = surface.normal;\n guideDistance = length(point - rayOrigin);\n guideRoughness = surface.roughness;\n guideSubsurface = select(0.0, surface.subsurface, SUBSURFACE_WALK);\n\n if (surface.kind == KIND_PHONG) {\n // Phong's exponent as the roughness of the GGX lobe that is about as wide.\n guideRoughness = pow(2.0 / (surface.shininess + 2.0), 0.25);\n }\n }\n\n if (primary) {\n alpha = 1.0;\n\n if (!scattering && !(GRASS && grass.found) && !fogged && afterMaterial == NO_MATERIAL\n && materials[m].afterProgram != NO_PROGRAM) {\n afterMaterial = m;\n afterTriangle = hit.triangle;\n afterBary = bary;\n afterDirection = direction;\n afterAlbedo = vec4<f32>(surface.albedo, surface.alpha);\n afterStart = radiance;\n afterThroughput = throughput;\n }\n\n if (!fogged) {\n let everySurface = frame.fogParams.w > 0.0;\n let kind = frame.fogColor.w;\n\n if (everySurface || (kind > 0.0 && (surface.flags & MATERIAL_FOG) != 0u)) {\n let depth = -(frame.worldToCamera * vec4<f32>(point, 1.0)).z;\n\n fogged = true;\n fogAmount = fogFactor(depth, select(kind, 1.0, everySurface));\n fogStart = radiance;\n fogThroughput = throughput;\n }\n }\n\n if (!hazed && !window && frame.atmosphereColor.w > 0.0) {\n hazed = true;\n // From the eye, where the camera ray only starts on the near plane; an orthographic view has no eye.\n hazeDistance = length(point - select(frame.cameraToWorld[3].xyz, rayOrigin, frame.cameraKind == 1u));\n hazeStart = radiance;\n hazeThroughput = throughput;\n hazeDirection = direction;\n }\n\n radiance = radiance + throughput * surface.lightMap;\n }\n\n if (any(surface.emissive > vec3<f32>(0.0))) {\n let contribution = throughput * surface.emissive * emissionWeight(hit, direction, bsdfPdf, lightsOwnDirect, lastPosition, lastNormal);\n\n // Seen by the camera or one bounce away this is direct light and stays exact; deeper it may be clamped.\n radiance = radiance + select(contribution, clampIndirect(contribution), bounce > 1u);\n }\n\n // Three does not light a basic, points, line or sprite material, so nothing carries on from one.\n // Past the last bounce a path still crosses a window straight on, which scatters nothing, to find the sky behind\n // it: cut there, the daylight a room gets through its glazing would go missing from every last bounce.\n let spent = bounce >= frame.bounces;\n\n if ((surface.flags & MATERIAL_LIT) == 0u || (spent && !window)) {\n break;\n }\n\n // Lambert and Phong blend their own environment map over the lit colour, which here is a share of the path.\n if (surface.legacyAmount > 0.0) {\n let amount = surface.legacyAmount;\n\n if (surface.combine == 1u) {\n radiance = radiance + throughput * surface.legacyEnv * amount;\n throughput = throughput * (1.0 - amount);\n } else if (surface.combine == 2u) {\n radiance = radiance + throughput * surface.legacyEnv * amount;\n } else {\n throughput = throughput * mix(vec3<f32>(1.0), surface.legacyEnv, amount);\n }\n }\n\n let view = -direction;\n var next = direction;\n var nextOrigin = point + direction * RAY_EPSILON;\n\n // Transmission replaces part of the diffuse layer, so that share of the time the surface is a dielectric instead.\n // Choosing it in proportion to its weight leaves both halves unweighted. The opaque half aims at a light; on a\n // window that lets only part of the light through, it does so on every path, weighed by its share, rather than on\n // the paths that chose it: a sunlit pane seen against a dim room would otherwise be lit on one path in five and\n // dark on the rest, which is the grain glass shows.\n let dielectric = surface.transmission > 0.0 && lobe.y < surface.transmission;\n let windowLight = window && !spent && surface.transmission < 1.0 && surface.subsurface <= 0.0;\n\n if (!dielectric || windowLight) {\n var chance = 0.0;\n\n if (!dielectric) {\n // Light that enters the material: a random walk inside it from here, in place of the share of the diffuse\n // layer that scatters inside. Chosen by chance, so neither branch is weighted against the other.\n // The window's opaque share is a surface like any other, which a spent path stops at.\n if (spent) {\n break;\n }\n\n if (SUBSURFACE_WALK) {\n chance = subsurfaceChance(surface, view);\n }\n\n if (SUBSURFACE_WALK && lobe.x < chance) {\n let medium = materials[surface.material].subsurface;\n\n throughput = throughput\n * ((1.0 - surface.metalness) * surface.subsurface * coatTransmittance(surface, view) / chance);\n sigma = vec3<f32>(1.0) / max(medium.rgb, vec3<f32>(1e-6));\n walkAlbedo = subsurfaceAlbedo(surface.albedo);\n walkG = materials[surface.material].subsurfaceAnisotropy;\n walkSteps = 0u;\n\n // Light that scattered inside is diffuse light, which is the signal it feeds.\n if (!split) {\n split = true;\n splitBase = radiance;\n splitDiffuse = true;\n }\n\n inMedium = true;\n primary = false;\n straight = false;\n bsdfPdf = 0.0;\n lightsOwnDirect = false;\n escapeMap = NO_TEXTURE;\n bounce = bounce + 1u;\n direction = cosineDirection(-surface.facing, bounceSample);\n origin = point - surface.facing * RAY_EPSILON;\n limit = walkDistance(throughput, sigma, pick, &walkOdds);\n continue;\n }\n\n throughput = throughput / (1.0 - chance);\n }\n\n let position = point + surface.normal * RAY_EPSILON;\n var directDiffuse: vec3<f32>;\n let lit = select(throughput, throughput * (1.0 - surface.transmission), windowLight);\n let direct = lit * directLight(surface, position, view, pick.x, lightSample, rng, &directDiffuse,\n select(0.0, volumeDensity, inVolume));\n let added = select(direct, clampIndirect(direct), bounce > 0u);\n let before = radiance;\n\n radiance = radiance + added;\n\n if (!dielectric) {\n let scattered = sampleBsdf(surface, view, (lobe.x - chance) / (1.0 - chance), bounceSample);\n\n // The first scatter splits the path: its direct light by lobe, and everything after it by the lobe it chose.\n if (!split) {\n split = true;\n splitBase = before;\n splitDirect = added;\n splitDirectDiffuse = lit * directDiffuse * select(1.0, luminanceOf(added) / max(luminanceOf(direct), 1e-20),\n bounce > 0u);\n splitDiffuse = !scattered.valid || scattered.diffuse;\n waitingForSpecular = scattered.valid && !scattered.diffuse;\n }\n\n if (!scattered.valid) {\n break;\n }\n\n throughput = throughput * scattered.weight;\n bsdfPdf = scattered.pdf;\n lastPosition = position;\n lastNormal = treeNormal(surface);\n lightsOwnDirect = surface.kind == KIND_TOON;\n escapeMap = surface.envMap;\n escapeIntensity = surface.envMapIntensity;\n next = scattered.direction;\n nextOrigin = position;\n\n // Through a translucent surface, the path carries on from its far side.\n if (GRASS && dot(next, surface.facing) < 0.0 && any(surface.translucent > vec3<f32>(0.0))) {\n nextOrigin = point - surface.facing * RAY_EPSILON;\n }\n primary = false;\n straight = false;\n bounce = bounce + 1u;\n }\n }\n\n if (dielectric) {\n var ior = surface.ior;\n\n // Three's dispersion: each colour refracts with its own index, spread around the material's. The path picks one\n // colour the first time it enters such a volume and carries only that one from then on.\n if (surface.dispersion > 0.0 && surface.thickness > 0.0) {\n if (all(channel == vec3<f32>(1.0))) {\n let which = min(u32(coverage.y * 3.0), 2u);\n\n channel = vec3<f32>(select(0.0, 1.0, which == 0u), select(0.0, 1.0, which == 1u), select(0.0, 1.0, which == 2u));\n throughput = throughput * channel * 3.0;\n }\n\n let spread = (ior - 1.0) * 0.025 * surface.dispersion;\n\n ior = dot(channel, vec3<f32>(ior - spread, ior, ior + spread));\n }\n\n let crossing = sampleDielectric(surface, view, ior, lobe.y / surface.transmission, bounceSample);\n\n // A reflection off the glass is a scattering, which a spent path no longer has.\n if (!crossing.valid || (spent && !crossing.straight)) {\n break;\n }\n\n // Bending or reflecting off glass is the specular signal.\n if (!split && !crossing.straight) {\n split = true;\n splitBase = radiance;\n splitDiffuse = false;\n waitingForSpecular = true;\n }\n\n throughput = throughput * crossing.weight;\n\n // Straight through a thin wall the direction stands, and with it the density that will weigh the next hit.\n if (!crossing.straight) {\n straight = false;\n next = crossing.direction;\n nextOrigin = point + surface.normal * select(RAY_EPSILON, -RAY_EPSILON, crossing.crossed);\n // A direction no light sampling could have produced: whatever the path finds next keeps its whole value.\n bsdfPdf = 0.0;\n lightsOwnDirect = false;\n escapeMap = surface.envMap;\n escapeIntensity = surface.envMapIntensity;\n\n if (crossing.crossed) {\n absorption = select(vec3<f32>(0.0), surface.absorption, surface.front);\n } else {\n primary = false;\n }\n }\n\n if (!primary) {\n bounce = bounce + 1u;\n }\n }\n\n // A path that carries almost nothing is ended by chance rather than by a cut, which keeps the estimate unbiased.\n let survival = max(throughput.r, max(throughput.g, throughput.b));\n\n if (survival <= 0.0) {\n break;\n }\n\n if (survival < frame.rouletteThreshold && bounce >= frame.rouletteStart) {\n if (pick.y > survival) {\n break;\n }\n\n throughput = throughput / survival;\n }\n\n origin = nextOrigin;\n limit = select(1e30, limit - hit.distance, straight);\n direction = next;\n }\n\n // The diffuse signal: everything before the first scatter, its diffuse direct light, and the rest if the diffuse lobe\n // carried the path on. A path that never scattered is all diffuse, which is what the camera sees directly.\n var diffuse = radiance;\n\n if (split) {\n let after = radiance - splitBase - splitDirect;\n diffuse = splitBase + splitDirectDiffuse + select(vec3<f32>(0.0), after, splitDiffuse);\n }\n\n if (afterMaterial != NO_MATERIAL && all(afterThroughput > vec3<f32>(0.0))) {\n var input = shaderInputAt(afterMaterial, afterTriangle, afterBary, afterDirection);\n input.data = materials[afterMaterial].afterData;\n input.diffuseColor = afterAlbedo;\n input.fragColor = vec4<f32>((radiance - afterStart) / afterThroughput, 1.0);\n\n let edited = runProgram(materials[afterMaterial].afterProgram, input);\n\n radiance = afterStart + afterThroughput * edited.color.rgb;\n // A hook rewrites the colour as a whole: there is no split left to keep.\n diffuse = radiance;\n }\n\n // Three lays its fog after the hooks, over what they made of the surface.\n if (fogged) {\n let fog = frame.fogColor.rgb * fogThroughput * fogAmount;\n\n radiance = fogStart + (radiance - fogStart) * (1.0 - fogAmount) + fog;\n // What came before the fogged surface is diffuse, so the fog's own colour joins that signal.\n diffuse = fogStart + (diffuse - fogStart) * (1.0 - fogAmount) + fog;\n }\n\n // The air takes its share of what lies behind it and adds the light it scatters towards the camera; the background,\n // which already is the sky, is left as it is.\n if (hazed) {\n let through = exp(-frame.atmosphereExtinction.rgb * hazeDistance);\n // The sun lights the air by the phase function of what fills it: Rayleigh's for clean air, a forward lobe for haze\n // (Henyey-Greenstein at 0.76), which is what makes the air glow towards the sun.\n let turn = dot(hazeDirection, frame.atmosphereSun.xyz);\n let phase = mix(3.0 / (16.0 * PI) * (1.0 + turn * turn), henyeyGreenstein(turn, 0.76), frame.atmosphereExtinction.w);\n let air = frame.atmosphereColor.rgb + frame.atmosphereSunColor.rgb * phase;\n let glow = air * hazeThroughput * (vec3<f32>(1.0) - through);\n\n radiance = hazeStart + (radiance - hazeStart) * through + glow;\n diffuse = hazeStart + (diffuse - hazeStart) * through + glow;\n }\n\n var result: PathResult;\n result.radiance = radiance;\n result.alpha = alpha;\n result.depth = depth;\n result.diffuse = diffuse;\n result.diffuseAlbedo = diffuseAlbedo;\n result.specularAlbedo = specularAlbedoGuide;\n result.normal = guideNormal;\n result.guideDistance = guideDistance;\n result.roughness = guideRoughness;\n result.specularDistance = specularDistance;\n result.subsurface = guideSubsurface;\n\n return result;\n}\n\n/** A point on the lens: a disk, or Three-free polygon bokeh shaped as the CPU renderer shapes it. */\nfn lensPoint(u: vec2<f32>) -> vec2<f32> {\n var radius = sqrt(u.x) * frame.lens.x;\n let angle = u.y * 2.0 * PI;\n let blades = frame.lens.z;\n\n if (blades >= 3.0) {\n let wedge = 2.0 * PI / blades;\n radius = radius * cos(PI / blades) / cos((angle - floor(angle / wedge) * wedge) - PI / blades);\n }\n\n return vec2<f32>(cos(angle), sin(angle)) * radius;\n}\n\n/**\n * Where a sample's camera ray starts, which way it points and how far it may go, for any camera: the ray is cast through\n * the inverse of the camera's own projection, from the near plane to the far one, which clip it as Three clips a\n * fragment. A lens moves the origin across its aperture and aims at the point in focus.\n */\nfn cameraRay(pixel: vec2<u32>, jitter: vec2<f32>, lens: vec2<f32>) -> array<vec4<f32>, 2> {\n let uv = (vec2<f32>(pixel) + jitter) / vec2<f32>(frame.resolution);\n var ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);\n\n // A lens's radial distortion: the pixel sees the scene at a radius scaled by the square of its own, which is one at\n // the corners. Positive reaches further out at the edges (barrel), negative less far (pincushion).\n if (frame.lensDistortion != 0.0) {\n let aspect = f32(frame.resolution.x) / f32(frame.resolution.y);\n let radius2 = (ndc.x * ndc.x * aspect * aspect + ndc.y * ndc.y) / (aspect * aspect + 1.0);\n\n ndc = ndc * (1.0 + frame.lensDistortion * radius2);\n }\n\n let ray = ndcRay(ndc);\n\n if (frame.lens.x <= 0.0 || frame.cameraKind == 1u) {\n return ray;\n }\n\n let position = frame.cameraToWorld[3].xyz;\n let forward = -normalize(frame.cameraToWorld[2].xyz);\n let focus = position + ray[1].xyz * (frame.lens.y / max(dot(ray[1].xyz, forward), 1e-6));\n let offset = lensPoint(lens);\n let origin = position + normalize(frame.cameraToWorld[0].xyz) * offset.x + normalize(frame.cameraToWorld[1].xyz) * offset.y;\n\n return array<vec4<f32>, 2>(vec4<f32>(origin, 0.0), vec4<f32>(normalize(focus - origin), 1e30));\n}\n\n/** The window depth GL would store for the first surface the camera ray stopped at: one where it stopped at none. */\nfn windowDepth(point: vec3<f32>, distance: f32) -> f32 {\n if (distance >= 1e30) {\n return 1.0;\n }\n\n let clip = frame.projection * (frame.worldToCamera * vec4<f32>(point, 1.0));\n\n return clamp(clip.z / clip.w * 0.5 + 0.5, 0.0, 1.0);\n}\n\n/** smoothstep as the CPU renderer writes it, which also holds when the two edges meet. */\nfn edge(low: f32, high: f32, value: f32) -> f32 {\n let t = select(select(0.0, 1.0, value >= high), clamp((value - low) / (high - low), 0.0, 1.0), high > low);\n\n return t * t * (3.0 - 2.0 * t);\n}\n\n/** How much of what lies at this point the fog lets through to the camera. */\nfn fogVisibility(point: vec3<f32>) -> f32 {\n let everySurface = frame.fogParams.w > 0.0;\n let kind = frame.fogColor.w;\n\n if (!everySurface && kind <= 0.0) {\n return 1.0;\n }\n\n let depth = -(frame.worldToCamera * vec4<f32>(point, 1.0)).z;\n\n return 1.0 - fogFactor(depth, select(kind, 1.0, everySurface));\n}\n\n/** The most volumetric lights one view ray marches through. */\nconst MAX_VOLUMETRIC: u32 = 8u;\n\n/**\n * Steps of one march. The CPU renderer marches 64 once per pixel; here every path marches, a few hundred times a\n * pixel, each at its own random offset, so a few steps a path add up to a finer integral than 64 once, at a fraction of\n * the shadow rays.\n */\nconst MARCH_STEPS: u32 = 8u;\n\n/**\n * The CPU renderer's volumetric lights (volumetricLight.js): one march across the union of every light's\n * reach along the view ray, Henyey-Greenstein scattering towards the camera, a shadow ray for a spot light that casts\n * one, and the same compression of the sum before it is added over the image.\n */\nfn volumetricGlow(origin: vec3<f32>, direction: vec3<f32>, distance: f32, jitter: f32) -> vec3<f32> {\n let count = min(frame.volumetricCount, MAX_VOLUMETRIC);\n let far = min(distance, 40.0);\n var tStart = far;\n var tEnd = 0.0;\n\n for (var k = 0u; k < count; k = k + 1u) {\n let at = frame.volumetricData + k * 20u;\n let position = extraVec4(at).xyz;\n let spot = extraFloat(at + 3u) > 0.5;\n let axis = extraVec4(at + 4u).xyz;\n let radius = extraFloat(at + 7u);\n let outer = extraFloat(at + 15u);\n let delta = origin - position;\n let b = dot(delta, direction);\n let disc = b * b - dot(delta, delta) + radius * radius;\n\n if (disc <= 0.0) {\n continue;\n }\n\n let start = max(0.0, -b - sqrt(disc));\n let end = min(far, -b + sqrt(disc));\n\n if (end <= start) {\n continue;\n }\n\n var cuts = array<f32, 4>(start, end, end, end);\n var cutCount = 2u;\n let cone = cos(outer);\n\n if (spot) {\n let ar = dot(axis, direction);\n let ac = dot(axis, delta);\n let cos2 = cone * cone;\n let qa = ar * ar - cos2;\n let qb = ar * ac - dot(direction, delta) * cos2;\n let qc = ac * ac - dot(delta, delta) * cos2;\n let discriminant = qb * qb - qa * qc;\n var roots = array<f32, 2>(-1.0, -1.0);\n\n if (abs(qa) < 1e-8) {\n if (abs(qb) > 1e-8) {\n roots[0] = -qc / (2.0 * qb);\n }\n } else if (discriminant >= 0.0) {\n roots[0] = (-qb - sqrt(discriminant)) / qa;\n roots[1] = (-qb + sqrt(discriminant)) / qa;\n }\n\n for (var r = 0u; r < 2u; r = r + 1u) {\n if (roots[r] > start && roots[r] < end) {\n cuts[cutCount] = roots[r];\n cutCount = cutCount + 1u;\n }\n }\n\n // Sort the few cuts in place.\n for (var i = 1u; i < cutCount; i = i + 1u) {\n var j = i;\n\n loop {\n if (j == 0u || cuts[j - 1u] <= cuts[j]) {\n break;\n }\n\n let swap = cuts[j];\n cuts[j] = cuts[j - 1u];\n cuts[j - 1u] = swap;\n j = j - 1u;\n }\n }\n }\n\n for (var segment = 1u; segment < cutCount; segment = segment + 1u) {\n let lo = cuts[segment - 1u];\n let hi = cuts[segment];\n\n if (spot && dot(normalize(delta + direction * ((lo + hi) * 0.5)), axis) < cone) {\n continue;\n }\n\n tStart = min(tStart, lo);\n tEnd = max(tEnd, hi);\n }\n }\n\n if (tEnd <= tStart) {\n return vec3<f32>(0.0);\n }\n\n let step = (tEnd - tStart) / f32(MARCH_STEPS);\n var transmittance: array<f32, 8>;\n\n for (var k = 0u; k < count; k = k + 1u) {\n transmittance[k] = exp(-extraFloat(frame.volumetricData + k * 20u + 11u) * tStart);\n }\n\n var result = vec3<f32>(0.0);\n\n for (var i = 0u; i < MARCH_STEPS; i = i + 1u) {\n let t = tStart + (f32(i) + jitter) * step;\n let p = origin + direction * t;\n let fogged = fogVisibility(p);\n\n if (fogged <= 0.0001) {\n break;\n }\n\n var lit = false;\n\n for (var k = 0u; k < count; k = k + 1u) {\n if (transmittance[k] <= 0.003) {\n continue;\n }\n\n lit = true;\n\n let at = frame.volumetricData + k * 20u;\n let position = extraVec4(at).xyz;\n let spot = extraFloat(at + 3u) > 0.5;\n let axis = extraVec4(at + 4u).xyz;\n let color = extraVec4(at + 8u).rgb;\n let density = extraFloat(at + 11u);\n let angle = extraFloat(at + 12u);\n let penumbra = extraFloat(at + 13u);\n let range = extraFloat(at + 14u);\n let outer = extraFloat(at + 15u);\n let spread = extraFloat(at + 16u);\n let anisotropy = extraFloat(at + 17u);\n let casts = extraFloat(at + 18u) > 0.5;\n let toLight = position - p;\n let dist = length(toLight);\n let toward = toLight / max(dist, 1e-20);\n var attenuation = exp(-dist / (select(10.0, range, range > 0.0) * 0.3));\n\n if (range > 0.0) {\n attenuation = attenuation * (1.0 - edge(range * 0.8, range, dist));\n }\n\n if (spot) {\n let along = -dot(toward, axis);\n let inner = cos(angle * (1.0 - penumbra));\n let core = edge(cos(angle), inner, along);\n let scattered = spread * pow(edge(cos(outer), inner, along), 2.0);\n\n attenuation = attenuation * select(core, max(core, scattered), spread > 0.0);\n }\n\n if (attenuation <= 0.0001) {\n continue;\n }\n\n // Shadowing follows Three's volumetric lights, which only have shadow maps for spots.\n if (spot && casts && frame.shadowsEnabled != 0u && traceAny(p, toward, dist * (1.0 - 1e-3)) != NO_HIT) {\n continue;\n }\n\n let cosine = dot(toward, direction);\n let g = anisotropy;\n let phase = select((1.0 - g * g) / pow(1.0 + g * g - 2.0 * g * cosine, 1.5), 1.0, g == 0.0);\n\n // The air between the camera and this point, exactly: a long step evaluated at its start would overstate it.\n result = result + color * (attenuation * density * step * exp(-density * t) * fogged * phase * 1.5);\n }\n\n if (!lit) {\n break;\n }\n\n for (var k = 0u; k < count; k = k + 1u) {\n transmittance[k] = transmittance[k] * exp(-extraFloat(frame.volumetricData + k * 20u + 11u) * step);\n }\n }\n\n return result / (vec3<f32>(1.0) + result);\n}\n\n/**\n * How far a pixel's estimate may still be from its mean: the standard error of its luminance, over the square root of\n * the luminance, which is roughly how visible that error is once the image is encoded for display.\n */\nfn pixelError(pixel: vec2<u32>) -> f32 {\n let index = pixel.y * frame.resolution.x + pixel.x;\n let sum = accumulation[index];\n\n if (sum.w < 2.0) {\n return 1e30;\n }\n\n let squares = accumulation[frame.pixelCount + index];\n let diffuse = accumulation[4u * frame.pixelCount + index].rgb;\n\n // Each of the denoiser's signals has its own variance; the pixel has converged when both have.\n return max(\n channelError(luminanceOf(diffuse) / sum.w, squares.z / sum.w, sum.w),\n channelError(luminanceOf(sum.rgb - diffuse) / sum.w, squares.w / sum.w, sum.w));\n}\n\nfn channelError(mean: f32, square: f32, samples: f32) -> f32 {\n let variance = max(square - mean * mean, 0.0);\n\n return sqrt(variance / samples) / sqrt(max(mean, 0.0) + 1e-3);\n}\n\n/**\n * Whether a pixel has converged, judged over its neighbourhood: a rare bright path lands in one pixel, and a pixel\n * judged alone would stop where its neighbours had not yet seen one and leave that area too dark.\n */\nfn converged(pixel: vec2<u32>) -> bool {\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let x = clamp(i32(pixel.x) + dx, 0, i32(frame.resolution.x) - 1);\n let y = clamp(i32(pixel.y) + dy, 0, i32(frame.resolution.y) - 1);\n\n if (pixelError(vec2<u32>(u32(x), u32(y))) > frame.adaptiveThreshold) {\n return false;\n }\n }\n }\n\n return true;\n}\n\n@compute @workgroup_size(8, 8, 1)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n if (id.x >= frame.bandSize.x || id.y >= frame.bandSize.y) {\n return;\n }\n\n let pixel = frame.bandOrigin + id.xy;\n\n if (pixel.x >= frame.resolution.x || pixel.y >= frame.resolution.y) {\n return;\n }\n\n let index = pixel.y * frame.resolution.x + pixel.x;\n\n // Adaptive sampling: a pixel whose neighbourhood has converged takes no more samples, which leaves the pass to\n // the pixels that still need them.\n if (frame.adaptiveMin > 0u && accumulation[index].w >= f32(frame.adaptiveMin) && converged(pixel)) {\n return;\n }\n\n var radiance = vec3<f32>(0.0);\n var alpha = 0.0;\n var depthSum = 0.0;\n var diffuseSquares = 0.0;\n var specularSquares = 0.0;\n var diffuseSum = vec3<f32>(0.0);\n var diffuseAlbedoSum = vec3<f32>(0.0);\n var specularAlbedoSum = vec3<f32>(0.0);\n var normalSum = vec3<f32>(0.0);\n var distanceSum = 0.0;\n var roughnessSum = 0.0;\n var specularDistanceSum = 0.0;\n var subsurfaceSum = 0.0;\n\n for (var s = 0u; s < frame.sampleCount; s = s + 1u) {\n var rng = makeSampler(pixel, frame.sampleIndex + s);\n let jitter = sample2D(&rng);\n let lens = sample2D(&rng);\n let ray = cameraRay(pixel, jitter, lens);\n let result = traceRadiance(ray[0].xyz, ray[1].xyz, ray[1].w, &rng);\n var sampleAlpha = result.alpha;\n\n var sampled = result.radiance;\n var sampledDiffuse = result.diffuse;\n\n if (frame.volumetricCount > 0u) {\n let glow = volumetricGlow(ray[0].xyz, ray[1].xyz, result.depth, sample2D(&rng).x);\n\n // As the CPU renderer composites it: added over the image, and raising the coverage just enough to hold it.\n sampled = sampled + glow;\n sampledDiffuse = sampledDiffuse + glow;\n sampleAlpha = max(sampleAlpha, max3(glow));\n }\n\n let sampledSpecular = sampled - sampledDiffuse;\n\n radiance = radiance + sampled;\n diffuseSum = diffuseSum + sampledDiffuse;\n diffuseSquares = diffuseSquares + luminanceOf(sampledDiffuse) * luminanceOf(sampledDiffuse);\n specularSquares = specularSquares + luminanceOf(sampledSpecular) * luminanceOf(sampledSpecular);\n diffuseAlbedoSum = diffuseAlbedoSum + result.diffuseAlbedo;\n specularAlbedoSum = specularAlbedoSum + result.specularAlbedo;\n normalSum = normalSum + result.normal;\n distanceSum = distanceSum + result.guideDistance;\n roughnessSum = roughnessSum + result.roughness;\n specularDistanceSum = specularDistanceSum + result.specularDistance;\n subsurfaceSum = subsurfaceSum + result.subsurface;\n\n alpha = alpha + sampleAlpha;\n depthSum = depthSum + windowDepth(ray[0].xyz + ray[1].xyz * min(result.depth, 1e20), result.depth);\n }\n\n let coverage = accumulation[frame.pixelCount + index];\n accumulation[index] = accumulation[index] + vec4<f32>(radiance, f32(frame.sampleCount));\n accumulation[frame.pixelCount + index] = vec4<f32>(\n coverage.x + alpha, coverage.y + depthSum, coverage.z + diffuseSquares, coverage.w + specularSquares);\n // The denoiser's signals and guides, summed like the radiance so they average over the pixel's samples.\n let pc = frame.pixelCount;\n accumulation[2u * pc + index] = accumulation[2u * pc + index] + vec4<f32>(diffuseAlbedoSum, roughnessSum);\n accumulation[3u * pc + index] = accumulation[3u * pc + index] + vec4<f32>(normalSum, distanceSum);\n accumulation[4u * pc + index] = accumulation[4u * pc + index] + vec4<f32>(diffuseSum, specularDistanceSum);\n accumulation[5u * pc + index] = accumulation[5u * pc + index] + vec4<f32>(specularAlbedoSum, subsurfaceSum);\n // This row still has work: every pixel that traced writes the same value, so the race is harmless.\n accumulation[6u * pc + pixel.y] = vec4<f32>(1.0);\n}\n";
|
|
14
14
|
//# sourceMappingURL=pathTrace.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathTrace.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"pathTrace.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,2oxDAs7C3B,CAAC"}
|
|
@@ -132,7 +132,8 @@ fn emissionWeight(hit: Hit, direction: vec3<f32>, bsdfPdf: f32, lightsOwnDirect:
|
|
|
132
132
|
* volume, a toon ramp read all round, a clearcoat that may face what its base turns from, and transmission.
|
|
133
133
|
*/
|
|
134
134
|
fn treeNormal(surface: Surface) -> vec3<f32> {
|
|
135
|
-
if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0
|
|
135
|
+
if (surface.kind == KIND_VOLUME || surface.kind == KIND_TOON || surface.clearcoat > 0.0 || surface.transmission > 0.0
|
|
136
|
+
|| (GRASS && any(surface.translucent > vec3<f32>(0.0)))) {
|
|
136
137
|
return vec3<f32>(0.0);
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -171,7 +172,14 @@ fn directLight(
|
|
|
171
172
|
let toon = surface.kind == KIND_TOON;
|
|
172
173
|
// Three's shadow map leaves a toon mesh's own far side unshadowed; tracing through the mesh would not.
|
|
173
174
|
let skip = select(NO_MATERIAL, surface.material, toon && dot(surface.normal, chosen.direction) < 0.1);
|
|
174
|
-
|
|
175
|
+
var shadowFrom = position;
|
|
176
|
+
|
|
177
|
+
// Light from behind a translucent surface is looked for from its far side, past the surface itself.
|
|
178
|
+
if (GRASS && any(surface.translucent > vec3<f32>(0.0)) && dot(chosen.direction, surface.facing) < 0.0) {
|
|
179
|
+
shadowFrom = position - (surface.normal + surface.facing) * RAY_EPSILON;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let through = lightTransmittance(shadowFrom, chosen, rng, skip, medium);
|
|
175
183
|
|
|
176
184
|
if (all(through <= vec3<f32>(0.0))) {
|
|
177
185
|
return vec3<f32>(0.0);
|
|
@@ -457,7 +465,20 @@ fn traceRadiance(
|
|
|
457
465
|
}
|
|
458
466
|
|
|
459
467
|
for (var step = 0u; step < MAX_STEPS + MAX_WALK; step = step + 1u) {
|
|
460
|
-
|
|
468
|
+
var hit = traceScene(origin, direction, limit);
|
|
469
|
+
// A blade of grass in front of whatever the ray hit. A walk inside a material never meets one.
|
|
470
|
+
var grass: GrassHit;
|
|
471
|
+
|
|
472
|
+
if (GRASS && !(SUBSURFACE_WALK && inMedium)) {
|
|
473
|
+
grass = traceGrass(origin, direction, hit.distance);
|
|
474
|
+
|
|
475
|
+
if (grass.found) {
|
|
476
|
+
hit.distance = grass.distance;
|
|
477
|
+
hit.triangle = GRASS_TRIANGLE;
|
|
478
|
+
hit.u = 0.0;
|
|
479
|
+
hit.v = 0.0;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
461
482
|
|
|
462
483
|
// A step of the walk inside a material that ends in the medium: the light scatters there, keeping each colour's
|
|
463
484
|
// share of it. It needs three numbers, which a plain hash gives for a fraction of what the sequence costs.
|
|
@@ -570,7 +591,7 @@ fn traceRadiance(
|
|
|
570
591
|
let point = origin + direction * travel;
|
|
571
592
|
var m = 0u;
|
|
572
593
|
|
|
573
|
-
if (!scattering) {
|
|
594
|
+
if (!scattering && !(GRASS && grass.found)) {
|
|
574
595
|
m = materialIndexOf(hit.triangle);
|
|
575
596
|
}
|
|
576
597
|
|
|
@@ -582,6 +603,8 @@ fn traceRadiance(
|
|
|
582
603
|
direction = -surface.normal;
|
|
583
604
|
} else if (VOLUMES && scattering) {
|
|
584
605
|
surface = volumePoint(direction, volumeAlbedo, volumeG);
|
|
606
|
+
} else if (GRASS && grass.found) {
|
|
607
|
+
surface = grassSurface(grass, direction);
|
|
585
608
|
} else {
|
|
586
609
|
|
|
587
610
|
// A volume's boundary is not a surface: the ray goes in or out, and the medium is what it meets.
|
|
@@ -730,7 +753,8 @@ fn traceRadiance(
|
|
|
730
753
|
if (primary) {
|
|
731
754
|
alpha = 1.0;
|
|
732
755
|
|
|
733
|
-
if (!scattering && !
|
|
756
|
+
if (!scattering && !(GRASS && grass.found) && !fogged && afterMaterial == NO_MATERIAL
|
|
757
|
+
&& materials[m].afterProgram != NO_PROGRAM) {
|
|
734
758
|
afterMaterial = m;
|
|
735
759
|
afterTriangle = hit.triangle;
|
|
736
760
|
afterBary = bary;
|
|
@@ -893,6 +917,11 @@ fn traceRadiance(
|
|
|
893
917
|
escapeIntensity = surface.envMapIntensity;
|
|
894
918
|
next = scattered.direction;
|
|
895
919
|
nextOrigin = position;
|
|
920
|
+
|
|
921
|
+
// Through a translucent surface, the path carries on from its far side.
|
|
922
|
+
if (GRASS && dot(next, surface.facing) < 0.0 && any(surface.translucent > vec3<f32>(0.0))) {
|
|
923
|
+
nextOrigin = point - surface.facing * RAY_EPSILON;
|
|
924
|
+
}
|
|
896
925
|
primary = false;
|
|
897
926
|
straight = false;
|
|
898
927
|
bounce = bounce + 1u;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathTrace.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC
|
|
1
|
+
{"version":3,"file":"pathTrace.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/pathTrace.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAs7CzC,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 // 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";
|
|
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 // What a translucent surface lets through to its far side, diffusely: grass blades. Zero for everything else.\n translucent: vec3<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,wxhCA6zBxB,CAAC"}
|
|
@@ -64,6 +64,8 @@ struct Surface {
|
|
|
64
64
|
material: u32,
|
|
65
65
|
// Share of the diffuse layer that scatters inside the material instead of at its surface.
|
|
66
66
|
subsurface: f32,
|
|
67
|
+
// What a translucent surface lets through to its far side, diffusely: grass blades. Zero for everything else.
|
|
68
|
+
translucent: vec3<f32>,
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6zBtC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinypoly/gpu-renderer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "WebGPU path tracer for Three.js scenes: BVH traversal, progressive accumulation and tone mapping in WGSL compute shaders.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"three",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"node": ">=20"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@tinypoly/cpu-renderer": "^0.3.
|
|
48
|
+
"@tinypoly/cpu-renderer": "^0.3.3",
|
|
49
49
|
"naga-wasm": "30.2.0",
|
|
50
50
|
"oidn-webgpu": "0.6.0"
|
|
51
51
|
},
|