@tinypoly/gpu-renderer 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/ARCHITECTURE.md +22 -7
  2. package/README.md +1 -1
  3. package/dist/bvh.d.ts +11 -6
  4. package/dist/bvh.d.ts.map +1 -1
  5. package/dist/bvh.js +64 -11
  6. package/dist/bvh.js.map +1 -1
  7. package/dist/denoise.d.ts +7 -2
  8. package/dist/denoise.d.ts.map +1 -1
  9. package/dist/denoise.js +61 -21
  10. package/dist/denoise.js.map +1 -1
  11. package/dist/dispatchScheduler.d.ts +11 -4
  12. package/dist/dispatchScheduler.d.ts.map +1 -1
  13. package/dist/dispatchScheduler.js +14 -7
  14. package/dist/dispatchScheduler.js.map +1 -1
  15. package/dist/frameLayout.d.ts.map +1 -1
  16. package/dist/frameLayout.js +3 -0
  17. package/dist/frameLayout.js.map +1 -1
  18. package/dist/imageResolve.d.ts +26 -0
  19. package/dist/imageResolve.d.ts.map +1 -0
  20. package/dist/imageResolve.js +114 -0
  21. package/dist/imageResolve.js.map +1 -0
  22. package/dist/lights.d.ts +9 -1
  23. package/dist/lights.d.ts.map +1 -1
  24. package/dist/lights.js +9 -0
  25. package/dist/lights.js.map +1 -1
  26. package/dist/renderer.d.ts +1 -0
  27. package/dist/renderer.d.ts.map +1 -1
  28. package/dist/renderer.js +97 -62
  29. package/dist/renderer.js.map +1 -1
  30. package/dist/sceneUpload.d.ts +14 -1
  31. package/dist/sceneUpload.d.ts.map +1 -1
  32. package/dist/sceneUpload.js +21 -3
  33. package/dist/sceneUpload.js.map +1 -1
  34. package/dist/texturePack.d.ts +16 -4
  35. package/dist/texturePack.d.ts.map +1 -1
  36. package/dist/texturePack.js +61 -8
  37. package/dist/texturePack.js.map +1 -1
  38. package/dist/wgsl/bvh.wgsl.d.ts +8 -2
  39. package/dist/wgsl/bvh.wgsl.d.ts.map +1 -1
  40. package/dist/wgsl/bvh.wgsl.js +167 -96
  41. package/dist/wgsl/bvh.wgsl.js.map +1 -1
  42. package/dist/wgsl/common.wgsl.d.ts.map +1 -1
  43. package/dist/wgsl/common.wgsl.js +20 -7
  44. package/dist/wgsl/common.wgsl.js.map +1 -1
  45. package/dist/wgsl/lights.wgsl.d.ts +1 -1
  46. package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
  47. package/dist/wgsl/lights.wgsl.js +3 -2
  48. package/dist/wgsl/lights.wgsl.js.map +1 -1
  49. package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
  50. package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
  51. package/dist/wgsl/pathTrace.wgsl.js +15 -1
  52. package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
  53. package/dist/wgsl/random.wgsl.d.ts +1 -1
  54. package/dist/wgsl/random.wgsl.d.ts.map +1 -1
  55. package/dist/wgsl/random.wgsl.js +9 -17
  56. package/dist/wgsl/random.wgsl.js.map +1 -1
  57. package/dist/wgsl/surface.wgsl.d.ts +1 -1
  58. package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
  59. package/dist/wgsl/surface.wgsl.js +26 -3
  60. package/dist/wgsl/surface.wgsl.js.map +1 -1
  61. package/dist/wgsl/texture.wgsl.d.ts +1 -1
  62. package/dist/wgsl/texture.wgsl.d.ts.map +1 -1
  63. package/dist/wgsl/texture.wgsl.js +10 -11
  64. package/dist/wgsl/texture.wgsl.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/bvh.ts +81 -12
  67. package/src/denoise.ts +66 -21
  68. package/src/dispatchScheduler.ts +16 -7
  69. package/src/frameLayout.ts +3 -0
  70. package/src/imageResolve.ts +136 -0
  71. package/src/lights.ts +19 -1
  72. package/src/renderer.ts +123 -77
  73. package/src/sceneUpload.ts +32 -4
  74. package/src/texturePack.ts +80 -13
  75. package/src/wgsl/bvh.wgsl.ts +167 -96
  76. package/src/wgsl/common.wgsl.ts +20 -7
  77. package/src/wgsl/lights.wgsl.ts +3 -2
  78. package/src/wgsl/pathTrace.wgsl.ts +15 -1
  79. package/src/wgsl/random.wgsl.ts +9 -17
  80. package/src/wgsl/surface.wgsl.ts +26 -3
  81. package/src/wgsl/texture.wgsl.ts +10 -11
@@ -6,5 +6,5 @@
6
6
  * CPU renderer follows too: roughness from green, metalness from blue, the alpha map from green, clearcoat from red,
7
7
  * its roughness from green, sheen roughness from alpha, and so on.
8
8
  */
9
- export declare const SURFACE_WGSL = "\nstruct Surface {\n // The shading normal, maps applied, facing the ray.\n normal: vec3<f32>,\n albedo: vec3<f32>,\n // What the surface radiates by itself: its emission, or the whole colour of a material Three does not light.\n emissive: vec3<f32>,\n // The dielectric's reflectance at normal incidence and at grazing angles, before metalness turns it to the albedo.\n specularColor: vec3<f32>,\n specularF90: f32,\n roughness: f32,\n metalness: f32,\n // How much of the surface actually covers this point: a cutout, a transparency or a discarded fragment.\n opacity: f32,\n // Three's diffuseColor.a, which blending other than normal weighs the surface by.\n alpha: f32,\n flags: u32,\n kind: u32,\n // Share of the diffuse layer that lets light through, with metal already taken out of it.\n transmission: f32,\n ior: f32,\n thickness: f32,\n dispersion: f32,\n // What a unit of distance inside the volume absorbs, per channel: zero for a volume that never darkens.\n absorption: vec3<f32>,\n // The interpolated normal facing the ray, before any map: thin walls, shadow rays and the clearcoat read this one.\n facing: vec3<f32>,\n // Whether the ray arrived from the side the normals point to, which is the outside of a closed volume.\n front: bool,\n clearcoat: f32,\n clearcoatRoughness: f32,\n clearcoatNormal: vec3<f32>,\n sheenColor: vec3<f32>,\n sheenRoughness: f32,\n iridescence: f32,\n iridescenceFresnel: vec3<f32>,\n // Anisotropic GGX: its strength, the roughness along the tangent, and the tangent frame it stretches along.\n anisotropy: f32,\n alphaT: f32,\n anisotropyT: vec3<f32>,\n anisotropyB: vec3<f32>,\n shininess: f32,\n // Phong's specular map, which scales its highlight.\n specularStrength: f32,\n gradientMap: u32,\n // The material's own environment map, which the paths leaving it see instead of the scene's.\n envMap: u32,\n envMapIntensity: f32,\n // Basic, Lambert and Phong blend their environment map over the lit colour: what the map shows here, and how much.\n legacyEnv: vec3<f32>,\n legacyAmount: f32,\n combine: u32,\n // Light a light map bakes into this point, which only the camera sees: the path traces the rest itself.\n lightMap: vec3<f32>,\n blending: u32,\n material: u32,\n};\n\n/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */\nfn triangleBarycentric(u: vec2<f32>) -> vec2<f32> {\n let root = sqrt(u.x);\n\n return vec2<f32>(1.0 - root, u.y * root);\n}\n\nfn trianglePointAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n return triangleVertex(triangle, 0u) * (1.0 - bary.x - bary.y)\n + triangleVertex(triangle, 1u) * bary.x\n + triangleVertex(triangle, 2u) * bary.y;\n}\n\nfn triangleUv(triangle: u32, bary: vec2<f32>, channel: u32) -> vec2<f32> {\n return attributeUv(triangle, 0u, channel) * (1.0 - bary.x - bary.y)\n + attributeUv(triangle, 1u, channel) * bary.x\n + attributeUv(triangle, 2u, channel) * bary.y;\n}\n\nfn triangleColor(triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n return attributeColor(triangle, 0u) * (1.0 - bary.x - bary.y)\n + attributeColor(triangle, 1u) * bary.x\n + attributeColor(triangle, 2u) * bary.y;\n}\n\n/** A map slot, sampled through the UV set the texture asks for. Missing maps read as one, so a multiply is always safe. */\nfn sampleMap(index: u32, triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n if (index == NO_TEXTURE) {\n return vec4<f32>(1.0);\n }\n\n return sampleTextureAt(index, triangleUv(triangle, bary, textureInfo(index).channel));\n}\n\n/** Whether the ray sees the side Three calls the front: the winding, which packing keeps counter-clockwise. */\nfn isFrontFace(triangle: u32, direction: vec3<f32>) -> bool {\n return dot(triangleNormal(triangle), direction) < 0.0;\n}\n\n/** Three culls the side a material does not draw. */\nfn isCulled(material: u32, triangle: u32, direction: vec3<f32>) -> bool {\n let side = materials[material].side;\n let front = isFrontFace(triangle, direction);\n\n return (side == SIDE_FRONT && !front) || (side == SIDE_BACK && front);\n}\n\n/** What the triangle radiates at this point, its emissive map included so direct and indirect light agree on it. */\nfn emissionAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n let map = sampleMap(materials[m].emissiveMap, triangle, bary);\n\n return materials[m].emissive.rgb * materials[m].emissive.w * map.rgb;\n}\n\n/** Three's material clipping planes: a point on the far side of one (or of all, with clipIntersection) is cut away. */\nfn isClipped(m: u32, point: vec3<f32>) -> bool {\n let count = materials[m].clipCount;\n\n if (count == 0u) {\n return false;\n }\n\n let intersection = (materials[m].flags & MATERIAL_CLIP_INTERSECTION) != 0u;\n var every = true;\n\n for (var i = 0u; i < count; i = i + 1u) {\n let plane = extraVec4(materials[m].clipStart + i * 4u);\n let outside = dot(plane.xyz, point) + plane.w < 0.0;\n\n if (outside && !intersection) {\n return true;\n }\n\n every = every && outside;\n }\n\n return intersection && every;\n}\n\n/** How large one pixel is at this point, in world units: what a one pixel wide line covers. */\nfn pixelFootprint(point: vec3<f32>) -> f32 {\n let scale = 2.0 / (frame.projection[1][1] * f32(frame.resolution.y));\n\n if (frame.cameraKind == 1u) {\n return scale;\n }\n\n return scale * max(-(frame.worldToCamera * vec4<f32>(point, 1.0)).z, 1e-6);\n}\n\nfn edgeDistance(point: vec3<f32>, a: vec3<f32>, b: vec3<f32>) -> f32 {\n let edge = b - a;\n\n return length(cross(point - a, edge)) / max(length(edge), 1e-20);\n}\n\n/** A wireframe draws only the edges of each triangle, one pixel wide as WebGL draws every line. */\nfn onWireframe(triangle: u32, point: vec3<f32>) -> bool {\n let a = triangleVertex(triangle, 0u);\n let b = triangleVertex(triangle, 1u);\n let c = triangleVertex(triangle, 2u);\n let nearest = min(edgeDistance(point, a, b), min(edgeDistance(point, b, c), edgeDistance(point, c, a)));\n\n return nearest <= 0.5 * pixelFootprint(point);\n}\n\n/** Three's diffuseColor.a: opacity, the colour map's alpha, the alpha map's green and the vertex alpha. */\nfn materialAlpha(m: u32, triangle: u32, bary: vec2<f32>) -> f32 {\n var alpha = materials[m].color.w * sampleMap(materials[m].map, triangle, bary).a\n * 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/** 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 let flags = materials[m].flags;\n let kind = materials[m].kind;\n let base = sampleMap(materials[m].map, triangle, bary);\n let facing = interpolatedNormal(m, triangle, bary, direction);\n let view = -direction;\n var surface: Surface;\n\n surface.material = m;\n surface.flags = flags;\n surface.kind = kind;\n surface.blending = materials[m].blending;\n surface.facing = facing;\n surface.front = dot(authoredNormal(triangle, bary), direction) < 0.0;\n surface.normal = mappedNormal(m, triangle, bary, facing);\n\n // A normal map may tilt past the horizon, where the surface would be lit from behind itself.\n if (dot(surface.normal, view) <= 0.0) {\n surface.normal = facing;\n }\n\n var albedo = materials[m].color.rgb * base.rgb;\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((flags & MATERIAL_VERTEX_COLORS) != 0u) {\n let color = triangleColor(triangle, bary);\n albedo = albedo * color.rgb;\n alpha = alpha * color.a;\n }\n\n var roughness = materials[m].specular.w * sampleMap(materials[m].roughnessMap, triangle, bary).g;\n var metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n var emissive = emissionAt(triangle, bary);\n\n surface.opacity = 1.0;\n\n // A built-in material's hooks run on its values before lighting, in Three's view space; a shader material's output\n // is the colour itself.\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(albedo, alpha);\n input.normal = toView(surface.normal);\n input.geometryNormal = toView(facing);\n input.roughness = roughness;\n input.metalness = metalness;\n input.emissive = emissive;\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n surface.opacity = 0.0;\n }\n\n if ((flags & MATERIAL_PATCHED) != 0u) {\n albedo = output.diffuseColor.rgb;\n alpha = output.diffuseColor.a;\n roughness = output.roughness;\n metalness = output.metalness;\n emissive = output.emissive;\n let normal = toWorld(output.normal);\n\n if (all(normal == normal)) {\n surface.normal = select(normal, -normal, dot(normal, view) < 0.0);\n }\n } else {\n albedo = output.color.rgb;\n alpha = output.color.a;\n }\n }\n\n surface.opacity = surface.opacity * coverageOf(m, alpha);\n surface.alpha = alpha;\n surface.albedo = albedo;\n surface.roughness = clamp(roughness, 0.0525, 1.0);\n surface.metalness = clamp(metalness, 0.0, 1.0);\n surface.emissive = emissive;\n surface.transmission = transmissionAt(m, triangle, bary, surface.metalness);\n surface.ior = max(materials[m].ior, 1.0);\n surface.thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n surface.dispersion = materials[m].dispersion;\n surface.absorption = absorptionOf(m);\n surface.shininess = materials[m].shininess;\n surface.specularStrength = sampleMap(materials[m].specularMap, triangle, bary).r;\n surface.gradientMap = materials[m].gradientMap;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = materials[m].envMapIntensity;\n surface.legacyAmount = 0.0;\n surface.legacyEnv = vec3<f32>(0.0);\n surface.combine = materials[m].combine;\n surface.lightMap = vec3<f32>(0.0);\n surface.clearcoat = 0.0;\n surface.iridescence = 0.0;\n surface.anisotropy = 0.0;\n surface.sheenColor = vec3<f32>(0.0);\n surface.specularColor = materials[m].specular.rgb;\n surface.specularF90 = 1.0;\n\n if ((flags & MATERIAL_PBR) != 0u) {\n if ((flags & MATERIAL_SPECULAR) != 0u) {\n // A physical material: the index of refraction, the specular colour and intensity and their maps.\n let intensity = materials[m].specularIntensity * sampleMap(materials[m].specularIntensityMap, triangle, bary).a;\n let color = materials[m].specular.rgb * sampleMap(materials[m].specularColorMap, triangle, bary).rgb;\n\n surface.specularColor = min(materials[m].dielectric * color, vec3<f32>(1.0)) * intensity;\n surface.specularF90 = mix(intensity, 1.0, surface.metalness);\n } else {\n // Three's standard material has no index of refraction and reflects 0.04.\n surface.specularColor = vec3<f32>(0.04);\n }\n\n surface.envMap = materials[m].envMap;\n\n if (materials[m].clearcoat > 0.0) {\n surface.clearcoat = clamp(materials[m].clearcoat * sampleMap(materials[m].clearcoatMap, triangle, bary).r, 0.0, 1.0);\n surface.clearcoatRoughness = clamp(\n materials[m].clearcoatRoughness * sampleMap(materials[m].clearcoatRoughnessMap, triangle, bary).g, 0.0525, 1.0);\n // The coat ignores the base's normal map: it has its own, over the interpolated normal.\n surface.clearcoatNormal = tangentNormal(materials[m].clearcoatNormalMap, triangle, bary, facing,\n vec2<f32>(materials[m].clearcoatNormalScaleX, materials[m].clearcoatNormalScaleY));\n\n if (dot(surface.clearcoatNormal, view) <= 0.0) {\n surface.clearcoatNormal = facing;\n }\n }\n\n if (any(materials[m].sheen.rgb > vec3<f32>(0.0))) {\n let map = sampleMap(materials[m].sheenRoughnessMap, triangle, bary).a;\n surface.sheenColor = materials[m].sheen.rgb * sampleMap(materials[m].sheenColorMap, triangle, bary).rgb;\n surface.sheenRoughness = clamp(materials[m].sheen.w, 0.0001, 1.0) * map;\n }\n\n if (materials[m].iridescence > 0.0) {\n var thickness = materials[m].iridescenceThicknessMax;\n\n if (materials[m].iridescenceThicknessMap != NO_TEXTURE) {\n thickness = mix(materials[m].iridescenceThicknessMin, materials[m].iridescenceThicknessMax,\n sampleMap(materials[m].iridescenceThicknessMap, triangle, bary).g);\n }\n\n if (thickness > 0.0) {\n surface.iridescence = clamp(\n materials[m].iridescence * sampleMap(materials[m].iridescenceMap, triangle, bary).r, 0.0, 1.0);\n\n let nv = clamp(dot(surface.normal, view), 0.0, 1.0);\n surface.iridescenceFresnel = mix(\n evalIridescence(materials[m].iridescenceIor, nv, thickness, surface.specularColor),\n evalIridescence(materials[m].iridescenceIor, nv, thickness, albedo),\n surface.metalness);\n }\n }\n\n if (materials[m].anisotropy != 0.0) {\n let rotation = materials[m].anisotropyRotation;\n var vector = vec2<f32>(cos(rotation), sin(rotation)) * materials[m].anisotropy;\n let channel = select(0u, textureInfo(materials[m].anisotropyMap).channel, materials[m].anisotropyMap != NO_TEXTURE);\n let tangents = tangentFrame(triangle, channel, surface.normal);\n\n if (materials[m].anisotropyMap != NO_TEXTURE) {\n let polar = sampleMap(materials[m].anisotropyMap, triangle, bary).rgb;\n let turn = mat2x2<f32>(vector.x, vector.y, -vector.y, vector.x);\n vector = turn * normalize(2.0 * polar.rg - vec2<f32>(1.0)) * polar.b;\n }\n\n let strength = length(vector);\n let along = select(vector / max(strength, 1e-20), vec2<f32>(1.0, 0.0), strength == 0.0);\n\n surface.anisotropy = clamp(strength, 0.0, 1.0);\n surface.alphaT = mix(surface.roughness * surface.roughness, 1.0, surface.anisotropy * surface.anisotropy);\n surface.anisotropyT = tangents.tangent * along.x + tangents.bitangent * along.y;\n surface.anisotropyB = tangents.bitangent * along.x - tangents.tangent * along.y;\n }\n } else if (materials[m].envMap != NO_TEXTURE\n && (kind == KIND_BASIC || kind == KIND_LAMBERT || kind == KIND_PHONG)) {\n // Basic, Lambert and Phong blend their own map over the lit colour, mirrored or bent the way the map says.\n let info = textureInfo(materials[m].envMap);\n let bent = refract(direction, surface.normal, materials[m].refractionRatio);\n let lookup = select(reflect(direction, surface.normal), bent, (info.flags & 16u) != 0u && any(bent != vec3<f32>(0.0)));\n\n surface.legacyEnv = sampleEquirect(materials[m].envMap, lookup).rgb * materials[m].envMapIntensity;\n surface.legacyAmount = clamp(materials[m].reflectivity * surface.specularStrength, 0.0, 1.0);\n }\n\n if (materials[m].lightMap != NO_TEXTURE) {\n let baked = sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity;\n surface.lightMap = baked * albedo * (1.0 - surface.metalness) / PI;\n }\n\n // A material Three does not light reads as its own colour, which in a path tracer is the same as emitting it.\n if ((flags & MATERIAL_LIT) == 0u) {\n var color = albedo;\n\n if (kind == KIND_NORMAL) {\n color = toView(surface.normal) * 0.5 + 0.5;\n } else if (kind == KIND_DEPTH) {\n let clip = frame.projection * (frame.worldToCamera * vec4<f32>(trianglePointAt(triangle, bary), 1.0));\n color = vec3<f32>(1.0 - (clip.z / clip.w * 0.5 + 0.5));\n } else if (kind == KIND_MATCAP) {\n let point = trianglePointAt(triangle, bary);\n let viewDirection = normalize(-(frame.worldToCamera * vec4<f32>(point, 1.0)).xyz);\n let x = normalize(vec3<f32>(viewDirection.z, 0.0, -viewDirection.x));\n let y = cross(viewDirection, x);\n let normal = toView(surface.normal);\n let uv = vec2<f32>(dot(x, normal), dot(y, normal)) * 0.495 + 0.5;\n var matcap = vec3<f32>(mix(0.2, 0.8, uv.y));\n\n if (materials[m].matcap != NO_TEXTURE) {\n matcap = sampleTextureRaw(materials[m].matcap, uv).rgb;\n }\n\n color = albedo * matcap;\n } else if (kind == KIND_BASIC && materials[m].lightMap != NO_TEXTURE) {\n // A basic material with a light map shows the map's light instead of full brightness.\n color = albedo * sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity / PI;\n }\n\n if (surface.legacyAmount > 0.0) {\n let amount = surface.legacyAmount;\n\n if (surface.combine == 1u) {\n color = mix(color, surface.legacyEnv, amount);\n } else if (surface.combine == 2u) {\n color = color + surface.legacyEnv * amount;\n } else {\n color = mix(color, color * surface.legacyEnv, amount);\n }\n\n surface.legacyAmount = 0.0;\n }\n\n surface.emissive = surface.emissive + color;\n surface.lightMap = vec3<f32>(0.0);\n }\n\n return surface;\n}\n";
9
+ export declare const SURFACE_WGSL = "\nstruct Surface {\n // The shading normal, maps applied, facing the ray.\n normal: vec3<f32>,\n albedo: vec3<f32>,\n // What the surface radiates by itself: its emission, or the whole colour of a material Three does not light.\n emissive: vec3<f32>,\n // The dielectric's reflectance at normal incidence and at grazing angles, before metalness turns it to the albedo.\n specularColor: vec3<f32>,\n specularF90: f32,\n roughness: f32,\n metalness: f32,\n // How much of the surface actually covers this point: a cutout, a transparency or a discarded fragment.\n opacity: f32,\n // Three's diffuseColor.a, which blending other than normal weighs the surface by.\n alpha: f32,\n flags: u32,\n kind: u32,\n // Share of the diffuse layer that lets light through, with metal already taken out of it.\n transmission: f32,\n ior: f32,\n thickness: f32,\n dispersion: f32,\n // What a unit of distance inside the volume absorbs, per channel: zero for a volume that never darkens.\n absorption: vec3<f32>,\n // The interpolated normal facing the ray, before any map: thin walls, shadow rays and the clearcoat read this one.\n facing: vec3<f32>,\n // Whether the ray arrived from the side the normals point to, which is the outside of a closed volume.\n front: bool,\n clearcoat: f32,\n clearcoatRoughness: f32,\n clearcoatNormal: vec3<f32>,\n sheenColor: vec3<f32>,\n sheenRoughness: f32,\n iridescence: f32,\n iridescenceFresnel: vec3<f32>,\n // Anisotropic GGX: its strength, the roughness along the tangent, and the tangent frame it stretches along.\n anisotropy: f32,\n alphaT: f32,\n anisotropyT: vec3<f32>,\n anisotropyB: vec3<f32>,\n shininess: f32,\n // Phong's specular map, which scales its highlight.\n specularStrength: f32,\n gradientMap: u32,\n // The material's own environment map, which the paths leaving it see instead of the scene's.\n envMap: u32,\n envMapIntensity: f32,\n // Basic, Lambert and Phong blend their environment map over the lit colour: what the map shows here, and how much.\n legacyEnv: vec3<f32>,\n legacyAmount: f32,\n combine: u32,\n // Light a light map bakes into this point, which only the camera sees: the path traces the rest itself.\n lightMap: vec3<f32>,\n blending: u32,\n material: u32,\n};\n\n/** Barycentric weights of the two trailing vertices, for a point drawn uniformly over a triangle's area. */\nfn triangleBarycentric(u: vec2<f32>) -> vec2<f32> {\n let root = sqrt(u.x);\n\n return vec2<f32>(1.0 - root, u.y * root);\n}\n\nfn trianglePointAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n return triangleVertex(triangle, 0u) * (1.0 - bary.x - bary.y)\n + triangleVertex(triangle, 1u) * bary.x\n + triangleVertex(triangle, 2u) * bary.y;\n}\n\nfn triangleUv(triangle: u32, bary: vec2<f32>, channel: u32) -> vec2<f32> {\n return attributeUv(triangle, 0u, channel) * (1.0 - bary.x - bary.y)\n + attributeUv(triangle, 1u, channel) * bary.x\n + attributeUv(triangle, 2u, channel) * bary.y;\n}\n\nfn triangleColor(triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n return attributeColor(triangle, 0u) * (1.0 - bary.x - bary.y)\n + attributeColor(triangle, 1u) * bary.x\n + attributeColor(triangle, 2u) * bary.y;\n}\n\n/** A map slot, sampled through the UV set the texture asks for. Missing maps read as one, so a multiply is always safe. */\nfn sampleMap(index: u32, triangle: u32, bary: vec2<f32>) -> vec4<f32> {\n if (index == NO_TEXTURE) {\n return vec4<f32>(1.0);\n }\n\n return sampleTextureAt(index, triangleUv(triangle, bary, textureInfo(index).channel));\n}\n\n/** Whether the ray sees the side Three calls the front: the winding, which packing keeps counter-clockwise. */\nfn isFrontFace(triangle: u32, direction: vec3<f32>) -> bool {\n return dot(triangleNormal(triangle), direction) < 0.0;\n}\n\n/** Three culls the side a material does not draw. */\nfn isCulled(material: u32, triangle: u32, direction: vec3<f32>) -> bool {\n let side = materials[material].side;\n let front = isFrontFace(triangle, direction);\n\n return (side == SIDE_FRONT && !front) || (side == SIDE_BACK && front);\n}\n\n/** What the triangle radiates at this point, its emissive map included so direct and indirect light agree on it. */\nfn emissionAt(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n let map = sampleMap(materials[m].emissiveMap, triangle, bary);\n\n return materials[m].emissive.rgb * materials[m].emissive.w * map.rgb;\n}\n\n/** Three's material clipping planes: a point on the far side of one (or of all, with clipIntersection) is cut away. */\nfn isClipped(m: u32, point: vec3<f32>) -> bool {\n let count = materials[m].clipCount;\n\n if (count == 0u) {\n return false;\n }\n\n let intersection = (materials[m].flags & MATERIAL_CLIP_INTERSECTION) != 0u;\n var every = true;\n\n for (var i = 0u; i < count; i = i + 1u) {\n let plane = extraVec4(materials[m].clipStart + i * 4u);\n let outside = dot(plane.xyz, point) + plane.w < 0.0;\n\n if (outside && !intersection) {\n return true;\n }\n\n every = every && outside;\n }\n\n return intersection && every;\n}\n\n/** How large one pixel is at this point, in world units: what a one pixel wide line covers. */\nfn pixelFootprint(point: vec3<f32>) -> f32 {\n let scale = 2.0 / (frame.projection[1][1] * f32(frame.resolution.y));\n\n if (frame.cameraKind == 1u) {\n return scale;\n }\n\n return scale * max(-(frame.worldToCamera * vec4<f32>(point, 1.0)).z, 1e-6);\n}\n\nfn edgeDistance(point: vec3<f32>, a: vec3<f32>, b: vec3<f32>) -> f32 {\n let edge = b - a;\n\n return length(cross(point - a, edge)) / max(length(edge), 1e-20);\n}\n\n/** A wireframe draws only the edges of each triangle, one pixel wide as WebGL draws every line. */\nfn onWireframe(triangle: u32, point: vec3<f32>) -> bool {\n let a = triangleVertex(triangle, 0u);\n let b = triangleVertex(triangle, 1u);\n let c = triangleVertex(triangle, 2u);\n let nearest = min(edgeDistance(point, a, b), min(edgeDistance(point, b, c), edgeDistance(point, c, a)));\n\n return nearest <= 0.5 * pixelFootprint(point);\n}\n\n/** Three's diffuseColor.a: opacity, the colour map's alpha, the alpha map's green and the vertex alpha. */\nfn materialAlpha(m: u32, triangle: u32, bary: vec2<f32>) -> f32 {\n return materialAlphaFrom(m, triangle, bary, sampleMap(materials[m].map, triangle, bary));\n}\n\n/** The same, from the colour map's texel at this point. Multiplied in the order resolveSurface multiplies it. */\nfn materialAlphaFrom(m: u32, triangle: u32, bary: vec2<f32>, base: vec4<f32>) -> f32 {\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n alpha = alpha * triangleColor(triangle, bary).a;\n }\n\n return alpha;\n}\n\n/**\n * How much of the surface is there, from its alpha the way Three's chunks use it: alphaTest cuts, alphaHash keeps the\n * share, and only a transparent material with normal blending lets the rest through; an opaque one sets alpha to one.\n */\nfn coverageOf(m: u32, alpha: f32) -> f32 {\n if (materials[m].alphaTest > 0.0 && alpha < materials[m].alphaTest) {\n return 0.0;\n }\n\n var coverage = 1.0;\n\n if ((materials[m].flags & MATERIAL_ALPHA_HASH) != 0u) {\n coverage = clamp(alpha, 0.0, 1.0);\n }\n\n if ((materials[m].flags & MATERIAL_TRANSPARENT) != 0u && materials[m].blending == BLEND_NORMAL) {\n coverage = coverage * clamp(alpha, 0.0, 1.0);\n }\n\n return coverage;\n}\n\n/** Barycentrics on the triangle's plane of a ray, extrapolated past its edges: where a neighbouring pixel lands. */\nfn planeBarycentric(triangle: u32, origin: vec3<f32>, direction: vec3<f32>, fallback: vec2<f32>) -> vec2<f32> {\n let a = triangleVertex(triangle, 0u);\n let e1 = triangleVertex(triangle, 1u) - a;\n let e2 = triangleVertex(triangle, 2u) - a;\n let n = cross(e1, e2);\n let denominator = dot(n, direction);\n\n if (abs(denominator) < 1e-20) {\n return fallback;\n }\n\n let p = origin + direction * (dot(n, a - origin) / denominator) - a;\n let d00 = dot(e1, e1);\n let d01 = dot(e1, e2);\n let d11 = dot(e2, e2);\n let d20 = dot(p, e1);\n let d21 = dot(p, e2);\n let det = d00 * d11 - d01 * d01;\n\n if (abs(det) < 1e-30) {\n return fallback;\n }\n\n return vec2<f32>((d11 * d20 - d01 * d21) / det, (d00 * d21 - d01 * d20) / det);\n}\n\n/** The camera ray through a point given in normalized device coordinates: origin on the near plane, and its length. */\nfn ndcRay(ndc: vec2<f32>) -> array<vec4<f32>, 2> {\n let nearView = frame.inverseProjection * vec4<f32>(ndc, -1.0, 1.0);\n let farView = frame.inverseProjection * vec4<f32>(ndc, 1.0, 1.0);\n let near = (frame.cameraToWorld * vec4<f32>(nearView.xyz / nearView.w, 1.0)).xyz;\n let far = (frame.cameraToWorld * vec4<f32>(farView.xyz / farView.w, 1.0)).xyz;\n let span = far - near;\n let extent = max(length(span), 1e-20);\n\n return array<vec4<f32>, 2>(vec4<f32>(near, 0.0), vec4<f32>(span / extent, extent));\n}\n\n/** What a fragment shader knows about the point it runs at, in GL's own terms. */\nfn shaderInputAt(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> ShaderInput {\n var input: ShaderInput;\n let point = trianglePointAt(triangle, bary);\n let view = frame.worldToCamera * vec4<f32>(point, 1.0);\n let clip = frame.projection * view;\n let w = select(clip.w, 1e-20, abs(clip.w) < 1e-20);\n let ndc = clip.xyz / w;\n let size = vec2<f32>(frame.resolution);\n let pixel = (ndc.xy * 0.5 + 0.5) * size;\n let right = ndcRay(vec2<f32>(ndc.x + 2.0 / size.x, ndc.y));\n let above = ndcRay(vec2<f32>(ndc.x, ndc.y + 2.0 / size.y));\n\n input.data = materials[m].shaderData;\n input.material = m;\n input.triangle = triangle;\n input.frontFacing = isFrontFace(triangle, direction);\n input.bary = bary;\n input.baryDx = planeBarycentric(triangle, right[0].xyz, right[1].xyz, bary);\n input.baryDy = planeBarycentric(triangle, above[0].xyz, above[1].xyz, bary);\n input.fragCoord = vec4<f32>(pixel, ndc.z * 0.5 + 0.5, 1.0 / w);\n let uv = triangleUv(triangle, bary, 0u);\n input.uv = uv;\n input.fragColor = vec4<f32>(0.0, 0.0, 0.0, 1.0);\n // A point's quad carries gl_PointCoord in its first UV set, with v running up as a texture's does.\n input.pointCoord = vec2<f32>(uv.x, 1.0 - uv.y);\n input.direction = direction;\n input.diffuseColor = vec4<f32>(1.0);\n input.normal = vec3<f32>(0.0, 0.0, 1.0);\n input.geometryNormal = vec3<f32>(0.0, 0.0, 1.0);\n input.roughness = 1.0;\n input.metalness = 0.0;\n input.emissive = vec3<f32>(0.0);\n input.viewPosition = -view.xyz;\n\n return input;\n}\n\n/** World to view space for a direction, which is the space Three's shaders keep their normals in. */\nfn toView(direction: vec3<f32>) -> vec3<f32> {\n return normalize((frame.worldToCamera * vec4<f32>(direction, 0.0)).xyz);\n}\n\nfn toWorld(direction: vec3<f32>) -> vec3<f32> {\n return normalize((frame.cameraToWorld * vec4<f32>(direction, 0.0)).xyz);\n}\n\n/**\n * How much of the surface covers this point for a shadow ray: the same alpha, cut, clipping and wireframe the camera\n * sees, and a shader's own discard. Blending other than normal is handled by the caller.\n */\nfn coverageAt(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> f32 {\n let m = materialIndexOf(triangle);\n let flags = materials[m].flags;\n\n if (materials[m].clipCount > 0u || (flags & MATERIAL_WIREFRAME) != 0u) {\n let point = trianglePointAt(triangle, bary);\n\n if (isClipped(m, point)) {\n return 0.0;\n }\n\n if ((flags & MATERIAL_WIREFRAME) != 0u && !onWireframe(triangle, point)) {\n return 0.0;\n }\n }\n\n var alpha = materialAlpha(m, triangle, bary);\n\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(materials[m].color.rgb, alpha);\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n return 0.0;\n }\n\n alpha = select(output.color.a, output.diffuseColor.a, (flags & MATERIAL_PATCHED) != 0u);\n }\n\n return coverageOf(m, alpha);\n}\n\n/** A tangent frame from the triangle's own UVs, which is what Three's tangents amount to for a flat triangle. */\nstruct TangentFrame {\n tangent: vec3<f32>,\n bitangent: vec3<f32>,\n valid: bool,\n};\n\nfn tangentFrame(triangle: u32, channel: u32, normal: vec3<f32>) -> TangentFrame {\n var tangents: TangentFrame;\n tangents.valid = false;\n\n let p0 = triangleVertex(triangle, 0u);\n let edge1 = triangleVertex(triangle, 1u) - p0;\n let edge2 = triangleVertex(triangle, 2u) - p0;\n let uv0 = attributeUv(triangle, 0u, channel);\n let delta1 = attributeUv(triangle, 1u, channel) - uv0;\n let delta2 = attributeUv(triangle, 2u, channel) - uv0;\n let determinant = delta1.x * delta2.y - delta2.x * delta1.y;\n\n // A triangle with no UV area has no tangent frame to offer.\n if (abs(determinant) < 1e-16) {\n let basis = orthonormalBasis(normal);\n tangents.tangent = basis[0];\n tangents.bitangent = basis[1];\n\n return tangents;\n }\n\n let raw = (edge1 * delta2.y - edge2 * delta1.y) / determinant;\n // Made perpendicular to the shading normal, so a map tilts the interpolated normal rather than the flat one.\n let tangent = normalize(raw - normal * dot(normal, raw));\n\n if (!all(tangent == tangent)) {\n let basis = orthonormalBasis(normal);\n tangents.tangent = basis[0];\n tangents.bitangent = basis[1];\n\n return tangents;\n }\n\n tangents.tangent = tangent;\n tangents.bitangent = cross(normal, tangent) * select(-1.0, 1.0, determinant >= 0.0);\n tangents.valid = true;\n\n return tangents;\n}\n\n/** A tangent space normal map over a normal, scaled the way Three's normalScale scales it. */\nfn tangentNormal(index: u32, triangle: u32, bary: vec2<f32>, normal: vec3<f32>, scale: vec2<f32>) -> vec3<f32> {\n if (index == NO_TEXTURE) {\n return normal;\n }\n\n let channel = textureInfo(index).channel;\n let tangents = tangentFrame(triangle, channel, normal);\n\n if (!tangents.valid) {\n return normal;\n }\n\n let texel = sampleTextureAt(index, triangleUv(triangle, bary, channel)).xyz * 2.0 - 1.0;\n let mapped = normalize(\n tangents.tangent * (texel.x * scale.x)\n + tangents.bitangent * (texel.y * scale.y)\n + normal * texel.z);\n\n return select(mapped, normal, !all(mapped == mapped));\n}\n\n/** The normal the material asks for: an object space or tangent space normal map, or else a bump map. */\nfn mappedNormal(m: u32, triangle: u32, bary: vec2<f32>, normal: vec3<f32>) -> vec3<f32> {\n let normalMap = materials[m].normalMap;\n\n if (normalMap != NO_TEXTURE && (materials[m].flags & MATERIAL_OBJECT_NORMAL_MAP) != 0u) {\n let texel = sampleMap(normalMap, triangle, bary).xyz * 2.0 - 1.0;\n let world = normalize(vec3<f32>(\n dot(materials[m].normalRow0.xyz, texel),\n dot(materials[m].normalRow1.xyz, texel),\n dot(materials[m].normalRow2.xyz, texel)));\n\n // Seen from behind, a double sided surface turns its map around with it.\n return select(world, -world, dot(world, normal) < 0.0);\n }\n\n if (normalMap != NO_TEXTURE) {\n return tangentNormal(normalMap, triangle, bary, normal, vec2<f32>(materials[m].normalScaleX, materials[m].normalScaleY));\n }\n\n let bumpMap = materials[m].bumpMap;\n\n if (bumpMap != NO_TEXTURE) {\n // The CPU renderer's bump: the height's change over one texel, along the tangent frame of the map's UVs.\n let info = textureInfo(bumpMap);\n let channel = info.channel;\n let tangents = tangentFrame(triangle, channel, normal);\n let uv = triangleUv(triangle, bary, channel);\n let du = 1.0 / f32(max(info.size.x, 1u));\n let dv = 1.0 / f32(max(info.size.y, 1u));\n let scale = materials[m].bumpScale;\n let height = sampleTextureAt(bumpMap, uv).x;\n let dx = (sampleTextureAt(bumpMap, uv + vec2<f32>(du, 0.0)).x - height) * scale;\n let dy = (sampleTextureAt(bumpMap, uv + vec2<f32>(0.0, dv)).x - height) * scale;\n let bumped = normalize(normal - (tangents.tangent * dx + tangents.bitangent * dy));\n\n return select(bumped, normal, !all(bumped == bumped));\n }\n\n return normal;\n}\n\n/** The interpolated vertex normal as the mesh authored it, pointing out of a closed volume. */\nfn authoredNormal(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let base = triangle * 9u;\n let n0 = vec3<f32>(attributes[base], attributes[base + 1u], attributes[base + 2u]);\n let n1 = vec3<f32>(attributes[base + 3u], attributes[base + 4u], attributes[base + 5u]);\n let n2 = vec3<f32>(attributes[base + 6u], attributes[base + 7u], attributes[base + 8u]);\n\n return normalize(n0 * (1.0 - bary.x - bary.y) + n1 * bary.x + n2 * bary.y);\n}\n\n/** The interpolated vertex normal, or the face's for flat shading, turned to face the ray. */\nfn interpolatedNormal(m: u32, triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n var normal = authoredNormal(triangle, bary);\n\n if ((materials[m].flags & MATERIAL_FLAT) != 0u || !all(normal == normal)) {\n normal = triangleNormal(triangle);\n }\n\n return select(normal, -normal, dot(normal, direction) > 0.0);\n}\n\n/** Three's transmission at this point: the factor, its map's red channel, and none of it on metal. */\nfn transmissionAt(m: u32, triangle: u32, bary: vec2<f32>, metalness: f32) -> f32 {\n if (materials[m].transmission <= 0.0) {\n return 0.0;\n }\n\n let map = sampleMap(materials[m].transmissionMap, triangle, bary).r;\n\n return clamp(materials[m].transmission * map, 0.0, 1.0) * (1.0 - metalness);\n}\n\n/** Beer-Lambert: the colour reached at the attenuation distance fixes how fast each channel fades. */\nfn absorptionOf(m: u32) -> vec3<f32> {\n let attenuation = materials[m].attenuation;\n\n if (attenuation.w <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n return -log(max(attenuation.rgb, vec3<f32>(1e-6))) / attenuation.w;\n}\n\n/** Three's evalSensitivity: the XYZ response of a thin film, Fourier-fitted, in linear sRGB. */\nfn filmSensitivity(opd: f32, shift: vec3<f32>) -> vec3<f32> {\n let phase = 2.0 * PI * opd * 1.0e-9;\n let value = vec3<f32>(5.4856e-13, 4.4201e-13, 5.2481e-13);\n let position = vec3<f32>(1.6810e+06, 1.7953e+06, 2.2084e+06);\n let variance = vec3<f32>(4.3278e+09, 9.3046e+09, 6.6121e+09);\n var xyz = value * sqrt(2.0 * PI * variance) * cos(position * phase + shift) * exp(-phase * phase * variance);\n\n xyz.x = xyz.x + 9.7470e-14 * sqrt(2.0 * PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift.x)\n * exp(-4.5282e+09 * phase * phase);\n xyz = xyz / 1.0685e-7;\n\n return mat3x3<f32>(\n vec3<f32>(3.2404542, -0.9692660, 0.0556434),\n vec3<f32>(-1.5371385, 1.8760108, -0.2040259),\n vec3<f32>(-0.4985314, 0.0415560, 1.0572252)) * xyz;\n}\n\nfn schlick3(f0: vec3<f32>, f90: f32, cosine: f32) -> vec3<f32> {\n let t = 1.0 - cosine;\n let k = t * t * t * t * t;\n\n return f0 + (vec3<f32>(f90) - f0) * k;\n}\n\n/** Three's evalIridescence (Belcour and Barla 2017): the Fresnel of a thin film over a base of reflectance baseF0. */\nfn evalIridescence(filmIor: f32, cosTheta1: f32, thickness: f32, baseF0: vec3<f32>) -> vec3<f32> {\n let ior = mix(1.0, filmIor, smoothstep(0.0, 0.03, thickness));\n let sinTheta2Sq = (1.0 / ior) * (1.0 / ior) * (1.0 - cosTheta1 * cosTheta1);\n let cosTheta2Sq = 1.0 - sinTheta2Sq;\n\n if (cosTheta2Sq < 0.0) {\n return vec3<f32>(1.0);\n }\n\n let cosTheta2 = sqrt(cosTheta2Sq);\n let r0 = ((ior - 1.0) / (ior + 1.0)) * ((ior - 1.0) / (ior + 1.0));\n let r12 = r0 + (1.0 - r0) * pow(1.0 - cosTheta1, 5.0);\n let t121 = 1.0 - r12;\n let phi12 = select(0.0, PI, ior < 1.0);\n let phi21 = PI - phi12;\n let root = sqrt(clamp(baseF0, vec3<f32>(0.0), vec3<f32>(0.9999)));\n let baseIor = (vec3<f32>(1.0) + root) / (vec3<f32>(1.0) - root);\n let r1 = ((baseIor - vec3<f32>(ior)) / (baseIor + vec3<f32>(ior))) * ((baseIor - vec3<f32>(ior)) / (baseIor + vec3<f32>(ior)));\n let r23 = schlick3(r1, 1.0, cosTheta2);\n let phi23 = select(vec3<f32>(0.0), vec3<f32>(PI), baseIor < vec3<f32>(ior));\n let opd = 2.0 * ior * thickness * cosTheta2;\n let phi = vec3<f32>(phi21) + phi23;\n let r123 = clamp(r12 * r23, vec3<f32>(1e-5), vec3<f32>(0.9999));\n let r123Root = sqrt(r123);\n let rs = t121 * t121 * r23 / (vec3<f32>(1.0) - r123);\n var intensity = vec3<f32>(r12) + rs;\n var cm = rs - vec3<f32>(t121);\n\n for (var m = 1; m <= 2; m = m + 1) {\n cm = cm * r123Root;\n intensity = intensity + cm * 2.0 * filmSensitivity(f32(m) * opd, f32(m) * phi);\n }\n\n return max(intensity, vec3<f32>(0.0));\n}\n\n/** Three's toon ramp: the gradient map across dot(n, l) from -1 to 1, or a single step at 0.7 without one. */\nfn toonRamp(gradientMap: u32, cosine: f32) -> f32 {\n let coordinate = cosine * 0.5 + 0.5;\n\n if (gradientMap == NO_TEXTURE) {\n return select(0.7, 1.0, coordinate >= 0.7);\n }\n\n return sampleTextureRaw(gradientMap, vec2<f32>(coordinate, 0.0)).r;\n}\n\n/**\n * Whether a material's coverage can be told from its maps alone, before the rest of the surface is read: no shader that\n * might rewrite the alpha or discard, normal blending, and something that makes it partial at all.\n */\nfn coverageFirst(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return materials[m].program == NO_PROGRAM\n && (blending == BLEND_NORMAL || blending == BLEND_NONE)\n && (materials[m].alphaTest > 0.0 || (flags & (MATERIAL_ALPHA_HASH | MATERIAL_TRANSPARENT)) != 0u);\n}\n\n/** Everything the lobes need about one point of one triangle. */\nfn resolveSurface(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {\n let m = materialIndexOf(triangle);\n\n return resolveSurfaceFrom(triangle, bary, direction, sampleMap(materials[m].map, triangle, bary));\n}\n\n/** The same, from the colour map's texel at this point, for a caller that already read it. */\nfn resolveSurfaceFrom(triangle: u32, bary: vec2<f32>, direction: vec3<f32>, base: vec4<f32>) -> Surface {\n let m = materialIndexOf(triangle);\n let flags = materials[m].flags;\n let kind = materials[m].kind;\n let facing = interpolatedNormal(m, triangle, bary, direction);\n let view = -direction;\n var surface: Surface;\n\n surface.material = m;\n surface.flags = flags;\n surface.kind = kind;\n surface.blending = materials[m].blending;\n surface.facing = facing;\n surface.front = dot(authoredNormal(triangle, bary), direction) < 0.0;\n surface.normal = mappedNormal(m, triangle, bary, facing);\n\n // A normal map may tilt past the horizon, where the surface would be lit from behind itself.\n if (dot(surface.normal, view) <= 0.0) {\n surface.normal = facing;\n }\n\n var albedo = materials[m].color.rgb * base.rgb;\n var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;\n\n if ((flags & MATERIAL_VERTEX_COLORS) != 0u) {\n let color = triangleColor(triangle, bary);\n albedo = albedo * color.rgb;\n alpha = alpha * color.a;\n }\n\n var roughness = materials[m].specular.w * sampleMap(materials[m].roughnessMap, triangle, bary).g;\n var metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n var emissive = emissionAt(triangle, bary);\n\n surface.opacity = 1.0;\n\n // A built-in material's hooks run on its values before lighting, in Three's view space; a shader material's output\n // is the colour itself.\n if (materials[m].program != NO_PROGRAM) {\n var input = shaderInputAt(m, triangle, bary, direction);\n input.diffuseColor = vec4<f32>(albedo, alpha);\n input.normal = toView(surface.normal);\n input.geometryNormal = toView(facing);\n input.roughness = roughness;\n input.metalness = metalness;\n input.emissive = emissive;\n let output = runProgram(materials[m].program, input);\n\n if (output.discarded) {\n surface.opacity = 0.0;\n }\n\n if ((flags & MATERIAL_PATCHED) != 0u) {\n albedo = output.diffuseColor.rgb;\n alpha = output.diffuseColor.a;\n roughness = output.roughness;\n metalness = output.metalness;\n emissive = output.emissive;\n let normal = toWorld(output.normal);\n\n if (all(normal == normal)) {\n surface.normal = select(normal, -normal, dot(normal, view) < 0.0);\n }\n } else {\n albedo = output.color.rgb;\n alpha = output.color.a;\n }\n }\n\n surface.opacity = surface.opacity * coverageOf(m, alpha);\n surface.alpha = alpha;\n surface.albedo = albedo;\n surface.roughness = clamp(roughness, 0.0525, 1.0);\n surface.metalness = clamp(metalness, 0.0, 1.0);\n surface.emissive = emissive;\n surface.transmission = transmissionAt(m, triangle, bary, surface.metalness);\n surface.ior = max(materials[m].ior, 1.0);\n surface.thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n surface.dispersion = materials[m].dispersion;\n surface.absorption = absorptionOf(m);\n surface.shininess = materials[m].shininess;\n surface.specularStrength = sampleMap(materials[m].specularMap, triangle, bary).r;\n surface.gradientMap = materials[m].gradientMap;\n surface.envMap = NO_TEXTURE;\n surface.envMapIntensity = materials[m].envMapIntensity;\n surface.legacyAmount = 0.0;\n surface.legacyEnv = vec3<f32>(0.0);\n surface.combine = materials[m].combine;\n surface.lightMap = vec3<f32>(0.0);\n surface.clearcoat = 0.0;\n surface.iridescence = 0.0;\n surface.anisotropy = 0.0;\n surface.sheenColor = vec3<f32>(0.0);\n surface.specularColor = materials[m].specular.rgb;\n surface.specularF90 = 1.0;\n\n if ((flags & MATERIAL_PBR) != 0u) {\n if ((flags & MATERIAL_SPECULAR) != 0u) {\n // A physical material: the index of refraction, the specular colour and intensity and their maps.\n let intensity = materials[m].specularIntensity * sampleMap(materials[m].specularIntensityMap, triangle, bary).a;\n let color = materials[m].specular.rgb * sampleMap(materials[m].specularColorMap, triangle, bary).rgb;\n\n surface.specularColor = min(materials[m].dielectric * color, vec3<f32>(1.0)) * intensity;\n surface.specularF90 = mix(intensity, 1.0, surface.metalness);\n } else {\n // Three's standard material has no index of refraction and reflects 0.04.\n surface.specularColor = vec3<f32>(0.04);\n }\n\n surface.envMap = materials[m].envMap;\n\n if (materials[m].clearcoat > 0.0) {\n surface.clearcoat = clamp(materials[m].clearcoat * sampleMap(materials[m].clearcoatMap, triangle, bary).r, 0.0, 1.0);\n surface.clearcoatRoughness = clamp(\n materials[m].clearcoatRoughness * sampleMap(materials[m].clearcoatRoughnessMap, triangle, bary).g, 0.0525, 1.0);\n // The coat ignores the base's normal map: it has its own, over the interpolated normal.\n surface.clearcoatNormal = tangentNormal(materials[m].clearcoatNormalMap, triangle, bary, facing,\n vec2<f32>(materials[m].clearcoatNormalScaleX, materials[m].clearcoatNormalScaleY));\n\n if (dot(surface.clearcoatNormal, view) <= 0.0) {\n surface.clearcoatNormal = facing;\n }\n }\n\n if (any(materials[m].sheen.rgb > vec3<f32>(0.0))) {\n let map = sampleMap(materials[m].sheenRoughnessMap, triangle, bary).a;\n surface.sheenColor = materials[m].sheen.rgb * sampleMap(materials[m].sheenColorMap, triangle, bary).rgb;\n surface.sheenRoughness = clamp(materials[m].sheen.w, 0.0001, 1.0) * map;\n }\n\n if (materials[m].iridescence > 0.0) {\n var thickness = materials[m].iridescenceThicknessMax;\n\n if (materials[m].iridescenceThicknessMap != NO_TEXTURE) {\n thickness = mix(materials[m].iridescenceThicknessMin, materials[m].iridescenceThicknessMax,\n sampleMap(materials[m].iridescenceThicknessMap, triangle, bary).g);\n }\n\n if (thickness > 0.0) {\n surface.iridescence = clamp(\n materials[m].iridescence * sampleMap(materials[m].iridescenceMap, triangle, bary).r, 0.0, 1.0);\n\n let nv = clamp(dot(surface.normal, view), 0.0, 1.0);\n surface.iridescenceFresnel = mix(\n evalIridescence(materials[m].iridescenceIor, nv, thickness, surface.specularColor),\n evalIridescence(materials[m].iridescenceIor, nv, thickness, albedo),\n surface.metalness);\n }\n }\n\n if (materials[m].anisotropy != 0.0) {\n let rotation = materials[m].anisotropyRotation;\n var vector = vec2<f32>(cos(rotation), sin(rotation)) * materials[m].anisotropy;\n let channel = select(0u, textureInfo(materials[m].anisotropyMap).channel, materials[m].anisotropyMap != NO_TEXTURE);\n let tangents = tangentFrame(triangle, channel, surface.normal);\n\n if (materials[m].anisotropyMap != NO_TEXTURE) {\n let polar = sampleMap(materials[m].anisotropyMap, triangle, bary).rgb;\n let turn = mat2x2<f32>(vector.x, vector.y, -vector.y, vector.x);\n vector = turn * normalize(2.0 * polar.rg - vec2<f32>(1.0)) * polar.b;\n }\n\n let strength = length(vector);\n let along = select(vector / max(strength, 1e-20), vec2<f32>(1.0, 0.0), strength == 0.0);\n\n surface.anisotropy = clamp(strength, 0.0, 1.0);\n surface.alphaT = mix(surface.roughness * surface.roughness, 1.0, surface.anisotropy * surface.anisotropy);\n surface.anisotropyT = tangents.tangent * along.x + tangents.bitangent * along.y;\n surface.anisotropyB = tangents.bitangent * along.x - tangents.tangent * along.y;\n }\n } else if (materials[m].envMap != NO_TEXTURE\n && (kind == KIND_BASIC || kind == KIND_LAMBERT || kind == KIND_PHONG)) {\n // Basic, Lambert and Phong blend their own map over the lit colour, mirrored or bent the way the map says.\n let info = textureInfo(materials[m].envMap);\n let bent = refract(direction, surface.normal, materials[m].refractionRatio);\n let lookup = select(reflect(direction, surface.normal), bent, (info.flags & 16u) != 0u && any(bent != vec3<f32>(0.0)));\n\n surface.legacyEnv = sampleEquirect(materials[m].envMap, lookup).rgb * materials[m].envMapIntensity;\n surface.legacyAmount = clamp(materials[m].reflectivity * surface.specularStrength, 0.0, 1.0);\n }\n\n if (materials[m].lightMap != NO_TEXTURE) {\n let baked = sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity;\n surface.lightMap = baked * albedo * (1.0 - surface.metalness) / PI;\n }\n\n // A material Three does not light reads as its own colour, which in a path tracer is the same as emitting it.\n if ((flags & MATERIAL_LIT) == 0u) {\n var color = albedo;\n\n if (kind == KIND_NORMAL) {\n color = toView(surface.normal) * 0.5 + 0.5;\n } else if (kind == KIND_DEPTH) {\n let clip = frame.projection * (frame.worldToCamera * vec4<f32>(trianglePointAt(triangle, bary), 1.0));\n color = vec3<f32>(1.0 - (clip.z / clip.w * 0.5 + 0.5));\n } else if (kind == KIND_MATCAP) {\n let point = trianglePointAt(triangle, bary);\n let viewDirection = normalize(-(frame.worldToCamera * vec4<f32>(point, 1.0)).xyz);\n let x = normalize(vec3<f32>(viewDirection.z, 0.0, -viewDirection.x));\n let y = cross(viewDirection, x);\n let normal = toView(surface.normal);\n let uv = vec2<f32>(dot(x, normal), dot(y, normal)) * 0.495 + 0.5;\n var matcap = vec3<f32>(mix(0.2, 0.8, uv.y));\n\n if (materials[m].matcap != NO_TEXTURE) {\n matcap = sampleTextureRaw(materials[m].matcap, uv).rgb;\n }\n\n color = albedo * matcap;\n } else if (kind == KIND_BASIC && materials[m].lightMap != NO_TEXTURE) {\n // A basic material with a light map shows the map's light instead of full brightness.\n color = albedo * sampleMap(materials[m].lightMap, triangle, bary).rgb * materials[m].lightMapIntensity / PI;\n }\n\n if (surface.legacyAmount > 0.0) {\n let amount = surface.legacyAmount;\n\n if (surface.combine == 1u) {\n color = mix(color, surface.legacyEnv, amount);\n } else if (surface.combine == 2u) {\n color = color + surface.legacyEnv * amount;\n } else {\n color = mix(color, color * surface.legacyEnv, amount);\n }\n\n surface.legacyAmount = 0.0;\n }\n\n surface.emissive = surface.emissive + color;\n surface.lightMap = vec3<f32>(0.0);\n }\n\n return surface;\n}\n";
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,2u9BAgxBxB,CAAC"}
1
+ {"version":3,"file":"surface.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/surface.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,i0/BAuyBxB,CAAC"}
@@ -173,8 +173,12 @@ fn onWireframe(triangle: u32, point: vec3<f32>) -> bool {
173
173
 
174
174
  /** Three's diffuseColor.a: opacity, the colour map's alpha, the alpha map's green and the vertex alpha. */
175
175
  fn materialAlpha(m: u32, triangle: u32, bary: vec2<f32>) -> f32 {
176
- var alpha = materials[m].color.w * sampleMap(materials[m].map, triangle, bary).a
177
- * sampleMap(materials[m].alphaMap, triangle, bary).g;
176
+ return materialAlphaFrom(m, triangle, bary, sampleMap(materials[m].map, triangle, bary));
177
+ }
178
+
179
+ /** The same, from the colour map's texel at this point. Multiplied in the order resolveSurface multiplies it. */
180
+ fn materialAlphaFrom(m: u32, triangle: u32, bary: vec2<f32>, base: vec4<f32>) -> f32 {
181
+ var alpha = materials[m].color.w * base.a * sampleMap(materials[m].alphaMap, triangle, bary).g;
178
182
 
179
183
  if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {
180
184
  alpha = alpha * triangleColor(triangle, bary).a;
@@ -554,12 +558,31 @@ fn toonRamp(gradientMap: u32, cosine: f32) -> f32 {
554
558
  return sampleTextureRaw(gradientMap, vec2<f32>(coordinate, 0.0)).r;
555
559
  }
556
560
 
561
+ /**
562
+ * Whether a material's coverage can be told from its maps alone, before the rest of the surface is read: no shader that
563
+ * might rewrite the alpha or discard, normal blending, and something that makes it partial at all.
564
+ */
565
+ fn coverageFirst(m: u32) -> bool {
566
+ let flags = materials[m].flags;
567
+ let blending = materials[m].blending;
568
+
569
+ return materials[m].program == NO_PROGRAM
570
+ && (blending == BLEND_NORMAL || blending == BLEND_NONE)
571
+ && (materials[m].alphaTest > 0.0 || (flags & (MATERIAL_ALPHA_HASH | MATERIAL_TRANSPARENT)) != 0u);
572
+ }
573
+
557
574
  /** Everything the lobes need about one point of one triangle. */
558
575
  fn resolveSurface(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> Surface {
559
576
  let m = materialIndexOf(triangle);
577
+
578
+ return resolveSurfaceFrom(triangle, bary, direction, sampleMap(materials[m].map, triangle, bary));
579
+ }
580
+
581
+ /** The same, from the colour map's texel at this point, for a caller that already read it. */
582
+ fn resolveSurfaceFrom(triangle: u32, bary: vec2<f32>, direction: vec3<f32>, base: vec4<f32>) -> Surface {
583
+ let m = materialIndexOf(triangle);
560
584
  let flags = materials[m].flags;
561
585
  let kind = materials[m].kind;
562
- let base = sampleMap(materials[m].map, triangle, bary);
563
586
  let facing = interpolatedNormal(m, triangle, bary, direction);
564
587
  let view = -direction;
565
588
  var surface: Surface;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgxBtC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuyBtC,CAAC"}
@@ -7,5 +7,5 @@
7
7
  * renderer already draws many jittered samples through every pixel, which averages that detail properly instead of
8
8
  * approximating it. Levels would buy memory locality, not correctness, so they can wait.
9
9
  */
10
- export declare const TEXTURE_WGSL = "\nconst TEXTURE_FLOAT: u32 = 1u;\nconst TEXTURE_SRGB: u32 = 2u;\nconst TEXTURE_NEAREST: u32 = 4u;\nconst TEXTURE_FLIP_Y: u32 = 8u;\n\n// Three's own wrap constants. Anything else clamps, as it does in the CPU renderer.\nconst WRAP_REPEAT: u32 = 1000u;\nconst WRAP_MIRROR: u32 = 1002u;\n\n/** Brings a texel index inside the texture the way the wrap mode says to. */\nfn wrapIndex(index: i32, size: i32, mode: u32) -> i32 {\n if (mode == WRAP_REPEAT) {\n return ((index % size) + size) % size;\n }\n\n if (mode == WRAP_MIRROR) {\n let period = size * 2;\n let folded = ((index % period) + period) % period;\n\n return select(period - 1 - folded, folded, folded < size);\n }\n\n return clamp(index, 0, size - 1);\n}\n\nfn srgbChannelToLinear(value: f32) -> f32 {\n return select(pow((value + 0.055) / 1.055, 2.4), value / 12.92, value <= 0.04045);\n}\n\n/** One texel, in linear light. Alpha is never encoded, as in Three. */\nfn readTexel(info: TextureInfo, x: i32, y: i32) -> vec4<f32> {\n let index = u32(y) * info.size.x + u32(x);\n\n if ((info.flags & TEXTURE_FLOAT) != 0u) {\n let at = info.offset + index * 4u;\n\n return vec4<f32>(\n bitcast<f32>(textures[at]),\n bitcast<f32>(textures[at + 1u]),\n bitcast<f32>(textures[at + 2u]),\n bitcast<f32>(textures[at + 3u]));\n }\n\n let word = textures[info.offset + index];\n let raw = vec4<f32>(\n f32(word & 0xffu),\n f32((word >> 8u) & 0xffu),\n f32((word >> 16u) & 0xffu),\n f32((word >> 24u) & 0xffu)) / 255.0;\n\n if ((info.flags & TEXTURE_SRGB) != 0u) {\n return vec4<f32>(\n srgbChannelToLinear(raw.r), srgbChannelToLinear(raw.g), srgbChannelToLinear(raw.b), raw.a);\n }\n\n return raw;\n}\n\n/** One texture of the table, sampled with its own transform, wrap modes and filter. */\nfn sampleTextureAt(index: u32, uv: vec2<f32>) -> vec4<f32> {\n let info = textureInfo(index);\n // The affine transform Three keeps on the texture, one row at a time.\n let transformed = vec2<f32>(\n dot(info.rowU.xyz, vec3<f32>(uv, 1.0)),\n dot(info.rowV.xyz, vec3<f32>(uv, 1.0)));\n\n return sampleTextureInfo(info, transformed);\n}\n\n/**\n * A texture read the way GLSL's texture2D reads it: at the coordinates given, with no transform. Three applies a\n * texture's matrix in its own materials' shaders, so a shader of the scene's, an environment lookup and a light's\n * projected map do not get it.\n */\nfn sampleTextureRaw(index: u32, uv: vec2<f32>) -> vec4<f32> {\n return sampleTextureInfo(textureInfo(index), uv);\n}\n\n/** Three's equirectUv: longitude across, and latitude up from the bottom of the image, as GL reads it. */\nfn equirectUv(direction: vec3<f32>) -> vec2<f32> {\n let d = normalize(direction);\n\n return vec2<f32>(atan2(d.z, d.x) / (2.0 * PI) + 0.5, asin(clamp(d.y, -1.0, 1.0)) / PI + 0.5);\n}\n\n/** An environment map of the scene's: an equirectangular image, or a cube map the snapshot already unwrapped into one. */\nfn sampleEquirect(index: u32, direction: vec3<f32>) -> vec4<f32> {\n return sampleTextureRaw(index, equirectUv(direction));\n}\n\nfn sampleTextureInfo(info: TextureInfo, transformed: vec2<f32>) -> vec4<f32> {\n if (!all(transformed == transformed) || info.size.x == 0u || info.size.y == 0u) {\n return vec4<f32>(0.0);\n }\n\n let flipped = select(transformed.y, 1.0 - transformed.y, (info.flags & TEXTURE_FLIP_Y) != 0u);\n let size = vec2<i32>(info.size);\n // The half texel offset that puts a sample in the middle of a texel, as on a graphics pipeline.\n let centred = vec2<f32>(transformed.x, flipped) * vec2<f32>(info.size) - 0.5;\n\n if ((info.flags & TEXTURE_NEAREST) != 0u) {\n let at = vec2<i32>(floor(centred + 0.5));\n\n return readTexel(info, wrapIndex(at.x, size.x, info.wrapS), wrapIndex(at.y, size.y, info.wrapT));\n }\n\n let low = floor(centred);\n let fraction = centred - low;\n let base = vec2<i32>(low);\n let x0 = wrapIndex(base.x, size.x, info.wrapS);\n let x1 = wrapIndex(base.x + 1, size.x, info.wrapS);\n let y0 = wrapIndex(base.y, size.y, info.wrapT);\n let y1 = wrapIndex(base.y + 1, size.y, info.wrapT);\n\n // The four corners are blended in linear light, which is why the decode happens per texel above.\n return mix(\n mix(readTexel(info, x0, y0), readTexel(info, x1, y0), fraction.x),\n mix(readTexel(info, x0, y1), readTexel(info, x1, y1), fraction.x),\n fraction.y);\n}\n";
10
+ export declare const TEXTURE_WGSL = "\nconst TEXTURE_FLOAT: u32 = 1u;\nconst TEXTURE_SRGB: u32 = 2u;\nconst TEXTURE_NEAREST: u32 = 4u;\nconst TEXTURE_FLIP_Y: u32 = 8u;\n\n// Three's own wrap constants. Anything else clamps, as it does in the CPU renderer.\nconst WRAP_REPEAT: u32 = 1000u;\nconst WRAP_MIRROR: u32 = 1002u;\n\n/** Brings a texel index inside the texture the way the wrap mode says to. */\nfn wrapIndex(index: i32, size: i32, mode: u32) -> i32 {\n if (mode == WRAP_REPEAT) {\n return ((index % size) + size) % size;\n }\n\n if (mode == WRAP_MIRROR) {\n let period = size * 2;\n let folded = ((index % period) + period) % period;\n\n return select(period - 1 - folded, folded, folded < size);\n }\n\n return clamp(index, 0, size - 1);\n}\n\n/** One texel, in linear light. Alpha is never encoded, as in Three. */\nfn readTexel(info: TextureInfo, x: i32, y: i32) -> vec4<f32> {\n let index = u32(y) * info.size.x + u32(x);\n\n if ((info.flags & TEXTURE_FLOAT) != 0u) {\n let at = info.offset + index * 4u;\n\n return vec4<f32>(\n bitcast<f32>(textures[at]),\n bitcast<f32>(textures[at + 1u]),\n bitcast<f32>(textures[at + 2u]),\n bitcast<f32>(textures[at + 3u]));\n }\n\n let word = textures[info.offset + index];\n\n // The decode is a table of the 256 codes rather than a power per channel and per texel: the same values, read.\n if ((info.flags & TEXTURE_SRGB) != 0u) {\n return vec4<f32>(\n bitcast<f32>(textures[SRGB_TABLE + (word & 0xffu)]),\n bitcast<f32>(textures[SRGB_TABLE + ((word >> 8u) & 0xffu)]),\n bitcast<f32>(textures[SRGB_TABLE + ((word >> 16u) & 0xffu)]),\n f32((word >> 24u) & 0xffu) / 255.0);\n }\n\n return vec4<f32>(\n f32(word & 0xffu),\n f32((word >> 8u) & 0xffu),\n f32((word >> 16u) & 0xffu),\n f32((word >> 24u) & 0xffu)) / 255.0;\n}\n\n/** One texture of the table, sampled with its own transform, wrap modes and filter. */\nfn sampleTextureAt(index: u32, uv: vec2<f32>) -> vec4<f32> {\n let info = textureInfo(index);\n // The affine transform Three keeps on the texture, one row at a time.\n let transformed = vec2<f32>(\n dot(info.rowU.xyz, vec3<f32>(uv, 1.0)),\n dot(info.rowV.xyz, vec3<f32>(uv, 1.0)));\n\n return sampleTextureInfo(info, transformed);\n}\n\n/**\n * A texture read the way GLSL's texture2D reads it: at the coordinates given, with no transform. Three applies a\n * texture's matrix in its own materials' shaders, so a shader of the scene's, an environment lookup and a light's\n * projected map do not get it.\n */\nfn sampleTextureRaw(index: u32, uv: vec2<f32>) -> vec4<f32> {\n return sampleTextureInfo(textureInfo(index), uv);\n}\n\n/** Three's equirectUv: longitude across, and latitude up from the bottom of the image, as GL reads it. */\nfn equirectUv(direction: vec3<f32>) -> vec2<f32> {\n let d = normalize(direction);\n\n return vec2<f32>(atan2(d.z, d.x) / (2.0 * PI) + 0.5, asin(clamp(d.y, -1.0, 1.0)) / PI + 0.5);\n}\n\n/** An environment map of the scene's: an equirectangular image, or a cube map the snapshot already unwrapped into one. */\nfn sampleEquirect(index: u32, direction: vec3<f32>) -> vec4<f32> {\n return sampleTextureRaw(index, equirectUv(direction));\n}\n\nfn sampleTextureInfo(info: TextureInfo, transformed: vec2<f32>) -> vec4<f32> {\n if (!all(transformed == transformed) || info.size.x == 0u || info.size.y == 0u) {\n return vec4<f32>(0.0);\n }\n\n let flipped = select(transformed.y, 1.0 - transformed.y, (info.flags & TEXTURE_FLIP_Y) != 0u);\n let size = vec2<i32>(info.size);\n // The half texel offset that puts a sample in the middle of a texel, as on a graphics pipeline.\n let centred = vec2<f32>(transformed.x, flipped) * vec2<f32>(info.size) - 0.5;\n\n if ((info.flags & TEXTURE_NEAREST) != 0u) {\n let at = vec2<i32>(floor(centred + 0.5));\n\n return readTexel(info, wrapIndex(at.x, size.x, info.wrapS), wrapIndex(at.y, size.y, info.wrapT));\n }\n\n let low = floor(centred);\n let fraction = centred - low;\n let base = vec2<i32>(low);\n let x0 = wrapIndex(base.x, size.x, info.wrapS);\n let x1 = wrapIndex(base.x + 1, size.x, info.wrapS);\n let y0 = wrapIndex(base.y, size.y, info.wrapT);\n let y1 = wrapIndex(base.y + 1, size.y, info.wrapT);\n\n // The four corners are blended in linear light, which is why the decode happens per texel above.\n return mix(\n mix(readTexel(info, x0, y0), readTexel(info, x1, y0), fraction.x),\n mix(readTexel(info, x0, y1), readTexel(info, x1, y1), fraction.x),\n fraction.y);\n}\n";
11
11
  //# sourceMappingURL=texture.wgsl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"texture.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/texture.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,s0IAyHxB,CAAC"}
1
+ {"version":3,"file":"texture.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/texture.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,66IAwHxB,CAAC"}
@@ -33,10 +33,6 @@ fn wrapIndex(index: i32, size: i32, mode: u32) -> i32 {
33
33
  return clamp(index, 0, size - 1);
34
34
  }
35
35
 
36
- fn srgbChannelToLinear(value: f32) -> f32 {
37
- return select(pow((value + 0.055) / 1.055, 2.4), value / 12.92, value <= 0.04045);
38
- }
39
-
40
36
  /** One texel, in linear light. Alpha is never encoded, as in Three. */
41
37
  fn readTexel(info: TextureInfo, x: i32, y: i32) -> vec4<f32> {
42
38
  let index = u32(y) * info.size.x + u32(x);
@@ -52,18 +48,21 @@ fn readTexel(info: TextureInfo, x: i32, y: i32) -> vec4<f32> {
52
48
  }
53
49
 
54
50
  let word = textures[info.offset + index];
55
- let raw = vec4<f32>(
56
- f32(word & 0xffu),
57
- f32((word >> 8u) & 0xffu),
58
- f32((word >> 16u) & 0xffu),
59
- f32((word >> 24u) & 0xffu)) / 255.0;
60
51
 
52
+ // The decode is a table of the 256 codes rather than a power per channel and per texel: the same values, read.
61
53
  if ((info.flags & TEXTURE_SRGB) != 0u) {
62
54
  return vec4<f32>(
63
- srgbChannelToLinear(raw.r), srgbChannelToLinear(raw.g), srgbChannelToLinear(raw.b), raw.a);
55
+ bitcast<f32>(textures[SRGB_TABLE + (word & 0xffu)]),
56
+ bitcast<f32>(textures[SRGB_TABLE + ((word >> 8u) & 0xffu)]),
57
+ bitcast<f32>(textures[SRGB_TABLE + ((word >> 16u) & 0xffu)]),
58
+ f32((word >> 24u) & 0xffu) / 255.0);
64
59
  }
65
60
 
66
- return raw;
61
+ return vec4<f32>(
62
+ f32(word & 0xffu),
63
+ f32((word >> 8u) & 0xffu),
64
+ f32((word >> 16u) & 0xffu),
65
+ f32((word >> 24u) & 0xffu)) / 255.0;
67
66
  }
68
67
 
69
68
  /** One texture of the table, sampled with its own transform, wrap modes and filter. */
@@ -1 +1 @@
1
- {"version":3,"file":"texture.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/texture.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyHtC,CAAC"}
1
+ {"version":3,"file":"texture.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/texture.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwHtC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinypoly/gpu-renderer",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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",
package/src/bvh.ts CHANGED
@@ -1,15 +1,20 @@
1
1
  /**
2
- * Binned SAH bounding volume hierarchy, built straight into the layout the shader reads: eight words per node, the
3
- * first four a minimum corner and a left child or leaf start, the last four a maximum corner and a right child or
4
- * triangle count, matching `struct BvhNode` in `wgsl/bvh.wgsl.ts`.
2
+ * Binned SAH bounding volume hierarchy. It is built as a binary tree of boxes, eight words a node, and then laid out
3
+ * the way the shader reads it: each node holds the boxes of its two children, so one read tells the traversal both
4
+ * what it may enter and where to go, and a leaf is tested without being read at all. That is sixteen words a node,
5
+ * matching `struct BvhNode` in `wgsl/common.wgsl.ts`: per child a minimum corner and a link, then a maximum corner and
6
+ * a triangle count. A link of zero or more is the node to go to; a negative one is a leaf, `-1 - start` into `order`.
5
7
  *
6
8
  * The bounds are computed from the same `Float32Array` the shader traverses, so every corner is already a float and
7
9
  * needs no widening. Building from double precision positions, as the CPU renderer does, would need each minimum
8
10
  * rounded down and each maximum rounded up before they reach the GPU.
9
11
  */
10
12
 
11
- /** Words per node in the packed buffer. */
12
- export const NODE_WORDS = 8;
13
+ /** Words per node in the packed buffer: both children's corners, links and counts. */
14
+ export const NODE_WORDS = 16;
15
+
16
+ /** Words per node of the binary tree the builder works on: its own corners, and its children or its leaf. */
17
+ const BINARY_WORDS = 8;
13
18
 
14
19
  /** Triangles a leaf may hold. */
15
20
  const MAX_LEAF_SIZE = 4;
@@ -27,7 +32,10 @@ const TRAVERSAL_COST = 0.125;
27
32
  export const MAX_BVH_DEPTH = 32;
28
33
 
29
34
  export interface PackedBvh {
30
- /** `nodeCount * NODE_WORDS` words, read as floats for the corners and as signed integers for the links. */
35
+ /**
36
+ * `nodeCount * NODE_WORDS` words, read as floats for the corners and as signed integers for the links. Node zero
37
+ * holds the root's two children, or the root itself beside an empty child when the whole scene is one leaf.
38
+ */
31
39
  nodes: ArrayBuffer;
32
40
  nodeCount: number;
33
41
  /** Triangle indices in leaf order: a leaf covers `order[start] .. order[start + count - 1]`. */
@@ -66,15 +74,15 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
66
74
 
67
75
  const { min, max, centroid } = triangleBounds(positions, triangleCount);
68
76
  // The heuristic may split down to one triangle per leaf, so the pool starts at the common case and grows.
69
- let nodes = new ArrayBuffer(Math.max(4, Math.ceil(triangleCount / 2)) * NODE_WORDS * 4);
77
+ let nodes = new ArrayBuffer(Math.max(4, Math.ceil(triangleCount / 2)) * BINARY_WORDS * 4);
70
78
  let floats = new Float32Array(nodes);
71
79
  let ints = new Int32Array(nodes);
72
80
 
73
81
  const reserve = (needed: number) => {
74
- if (needed * NODE_WORDS * 4 <= nodes.byteLength) return;
82
+ if (needed * BINARY_WORDS * 4 <= nodes.byteLength) return;
75
83
 
76
- const grown = new ArrayBuffer(Math.max(needed, Math.ceil(nodes.byteLength / (NODE_WORDS * 4) * 1.6))
77
- * NODE_WORDS * 4);
84
+ const grown = new ArrayBuffer(Math.max(needed, Math.ceil(nodes.byteLength / (BINARY_WORDS * 4) * 1.6))
85
+ * BINARY_WORDS * 4);
78
86
 
79
87
  new Float32Array(grown).set(floats);
80
88
  nodes = grown;
@@ -132,7 +140,7 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
132
140
  if (value > cMaxZ) cMaxZ = value;
133
141
  }
134
142
 
135
- const word = node * NODE_WORDS;
143
+ const word = node * BINARY_WORDS;
136
144
 
137
145
  floats[word] = minX;
138
146
  floats[word + 1] = minY;
@@ -180,7 +188,68 @@ export function buildBvh(positions: Float32Array, triangleCount: number): Packed
180
188
  top += 2;
181
189
  }
182
190
 
183
- return { nodes: nodes.slice(0, nodeCount * NODE_WORDS * 4), nodeCount, order, maxDepth };
191
+ return { ...widen(floats, ints, nodeCount), order, maxDepth };
192
+ }
193
+
194
+ /**
195
+ * The binary tree laid out for the shader: one node per interior node of it, holding both children's boxes. Interior
196
+ * nodes are numbered depth first, left before right, so a node and the child it leads to tend to share a cache line.
197
+ */
198
+ function widen(floats: Float32Array, ints: Int32Array, binaryCount: number): { nodes: ArrayBuffer; nodeCount: number } {
199
+ const leaf = (node: number) => ints[node * BINARY_WORDS + 3] < 0;
200
+ const rootLeaf = leaf(0);
201
+ // A full binary tree has one fewer interior node than leaves; a single leaf still needs a node to stand in.
202
+ const nodeCount = rootLeaf ? 1 : (binaryCount - 1) / 2;
203
+ const nodes = new ArrayBuffer(nodeCount * NODE_WORDS * 4);
204
+ const outFloats = new Float32Array(nodes);
205
+ const outInts = new Int32Array(nodes);
206
+ const index = new Int32Array(binaryCount);
207
+
208
+ const writeChild = (word: number, child: number) => {
209
+ const from = child * BINARY_WORDS;
210
+
211
+ for (let axis = 0; axis < 3; axis++) {
212
+ outFloats[word + axis] = floats[from + axis];
213
+ outFloats[word + 4 + axis] = floats[from + 4 + axis];
214
+ }
215
+
216
+ outInts[word + 3] = leaf(child) ? ints[from + 3] : index[child];
217
+ outInts[word + 7] = leaf(child) ? ints[from + 7] : 0;
218
+ };
219
+
220
+ if (rootLeaf) {
221
+ writeChild(0, 0);
222
+
223
+ // A box at the far corner of the float range, which no ray reaches, around a leaf of nothing.
224
+ for (let axis = 0; axis < 3; axis++) outFloats[8 + axis] = outFloats[12 + axis] = 3.4028234663852886e38;
225
+
226
+ outInts[11] = -1;
227
+ outInts[15] = 0;
228
+
229
+ return { nodes, nodeCount };
230
+ }
231
+
232
+ const interior: number[] = [];
233
+ const pending = [0];
234
+
235
+ while (pending.length) {
236
+ const node = pending.pop() as number;
237
+
238
+ index[node] = interior.length;
239
+ interior.push(node);
240
+
241
+ const left = ints[node * BINARY_WORDS + 3], right = ints[node * BINARY_WORDS + 7];
242
+
243
+ if (!leaf(right)) pending.push(right);
244
+ if (!leaf(left)) pending.push(left);
245
+ }
246
+
247
+ interior.forEach((node, at) => {
248
+ writeChild(at * NODE_WORDS, ints[node * BINARY_WORDS + 3]);
249
+ writeChild(at * NODE_WORDS + 8, ints[node * BINARY_WORDS + 7]);
250
+ });
251
+
252
+ return { nodes, nodeCount };
184
253
  }
185
254
 
186
255
  /** Surface area, the cost the split heuristic weighs each side by. Empty bounds cost nothing. */
package/src/denoise.ts CHANGED
@@ -86,6 +86,10 @@ struct Params {
86
86
  @group(0) @binding(3) var<storage, read> specularIn: array<vec4<f32>>;
87
87
  @group(0) @binding(4) var<storage, read_write> diffuseOut: array<vec4<f32>>;
88
88
  @group(0) @binding(5) var<storage, read_write> specularOut: array<vec4<f32>>;
89
+ // The guides each tap compares, worked out once per pixel by prepare: the normal and the depth, then the roughness, or
90
+ // -1 where no sample has landed. The same numbers the accumulation gives, two reads a tap instead of four.
91
+ @group(0) @binding(6) var<storage, read_write> guides: array<vec4<f32>>;
92
+ @group(0) @binding(7) var<storage, read_write> roughnesses: array<f32>;
89
93
 
90
94
  fn luminance(color: vec3<f32>) -> f32 {
91
95
  return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
@@ -153,10 +157,15 @@ fn prepare(@builtin(global_invocation_id) id: vec3<u32>) {
153
157
  if (count <= 0.0) {
154
158
  diffuseOut[index] = vec4<f32>(0.0);
155
159
  specularOut[index] = vec4<f32>(0.0);
160
+ guides[index] = vec4<f32>(0.0);
161
+ roughnesses[index] = -1.0;
156
162
 
157
163
  return;
158
164
  }
159
165
 
166
+ guides[index] = vec4<f32>(guideNormal(index), guideDepth(index));
167
+ roughnesses[index] = guideRoughness(index);
168
+
160
169
  let squares = accumulation[params.count + index];
161
170
  let diffuse = diffuseMean(index);
162
171
  let specular = specularMean(index);
@@ -269,9 +278,10 @@ fn atrous(@builtin(global_invocation_id) id: vec3<u32>) {
269
278
  return;
270
279
  }
271
280
 
272
- let normal = guideNormal(index);
273
- let depth = guideDepth(index);
274
- let roughness = guideRoughness(index);
281
+ let guide = guides[index];
282
+ let normal = guide.xyz;
283
+ let depth = guide.w;
284
+ let roughness = roughnesses[index];
275
285
  let diffuseSigma = params.edge * sqrt(max(localVariance(pixel, false), 0.0)) + 1e-6;
276
286
  let specularSigma = params.edge * sqrt(max(localVariance(pixel, true), 0.0)) + 1e-6;
277
287
  let radius = specularRadius(index, depth);
@@ -299,19 +309,23 @@ fn atrous(@builtin(global_invocation_id) id: vec3<u32>) {
299
309
  var ws = base;
300
310
 
301
311
  if (dx != 0 || dy != 0) {
302
- if (accumulation[other].w <= 0.0) {
312
+ let otherRoughness = roughnesses[other];
313
+
314
+ // No sample has landed there.
315
+ if (otherRoughness < 0.0) {
303
316
  continue;
304
317
  }
305
318
 
319
+ let otherGuide = guides[other];
306
320
  let offset = length(vec2<f32>(f32(dx * step), f32(dy * step)));
307
- let alignment = max(dot(normal, guideNormal(other)), 0.0);
308
- let depthWeight = exp(-abs(depth - guideDepth(other)) / (0.02 * depth * offset + 1e-3));
321
+ let alignment = max(dot(normal, otherGuide.xyz), 0.0);
322
+ let depthWeight = exp(-abs(depth - otherGuide.w) / (0.02 * depth * offset + 1e-3));
309
323
 
310
324
  wd = wd * pow(alignment, 128.0) * depthWeight
311
325
  * exp(-abs(luminance(d.rgb) - luminance(diffuseCenter.rgb)) / diffuseSigma);
312
326
 
313
327
  ws = ws * pow(alignment, specularPower) * depthWeight
314
- * exp(-abs(guideRoughness(other) - roughness) * 10.0)
328
+ * exp(-abs(otherRoughness - roughness) * 10.0)
315
329
  * exp(-abs(luminance(s.rgb) - luminance(specularCenter.rgb)) / specularSigma)
316
330
  * select(0.0, exp(-offset * offset / (radius * radius)), radius >= 0.5);
317
331
  }
@@ -381,8 +395,21 @@ export class DenoisePass {
381
395
  private readonly finish: GPUComputePipeline;
382
396
  /** One uniform per stage, so a whole denoise is a single submit. */
383
397
  private readonly params: GPUBuffer[];
384
- /** Two ping-pong pairs, diffuse and specular, and the output. */
398
+ /** Two ping-pong pairs, diffuse and specular, the output, then the guides and the roughness. */
385
399
  private buffers: GPUBuffer[] = [];
400
+ /** Bind groups by the buffers they bind, and a number for each buffer seen, which is what the key is made of. */
401
+ private readonly groups = new Map<string, GPUBindGroup>();
402
+ private readonly ids = new WeakMap<GPUBuffer, number>();
403
+ private nextId = 0;
404
+
405
+ private idOf(buffer: GPUBuffer): number {
406
+ let id = this.ids.get(buffer);
407
+
408
+ if (id === undefined) this.ids.set(buffer, id = this.nextId++);
409
+
410
+ return id;
411
+ }
412
+
386
413
  private width = 0;
387
414
  private height = 0;
388
415
 
@@ -402,6 +429,8 @@ export class DenoisePass {
402
429
  { binding: 3, visibility, buffer: storage("read-only-storage") },
403
430
  { binding: 4, visibility, buffer: storage("storage") },
404
431
  { binding: 5, visibility, buffer: storage("storage") },
432
+ { binding: 6, visibility, buffer: storage("storage") },
433
+ { binding: 7, visibility, buffer: storage("storage") },
405
434
  ],
406
435
  });
407
436
 
@@ -423,9 +452,12 @@ export class DenoisePass {
423
452
 
424
453
  const size = Math.max(16, width * height * 16);
425
454
 
426
- this.buffers = Array.from({ length: 5 }, () =>
455
+ this.buffers = Array.from({ length: 6 }, () =>
427
456
  this.device.createBuffer({ size, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC }));
457
+ this.buffers.push(
458
+ this.device.createBuffer({ size: Math.max(16, width * height * 4), usage: GPUBufferUsage.STORAGE }));
428
459
  this.output = this.buffers[4];
460
+ this.groups.clear();
429
461
  this.width = width;
430
462
  this.height = height;
431
463
  }
@@ -460,22 +492,34 @@ export class DenoisePass {
460
492
  this.resize(width, height);
461
493
  this.writeParams(width, height, settings, view);
462
494
 
463
- const [diffuseA, diffuseB, specularA, specularB, output] = this.buffers;
495
+ const [diffuseA, diffuseB, specularA, specularB, output, guides, roughnesses] = this.buffers;
464
496
  const groups = [Math.ceil(width / 8), Math.ceil(height / 8)] as const;
465
497
 
466
498
  const run = (pipeline: GPUComputePipeline, params: GPUBuffer,
467
499
  sources: [GPUBuffer, GPUBuffer], targets: [GPUBuffer, GPUBuffer]) => {
468
- const group = this.device.createBindGroup({
469
- layout: this.layout,
470
- entries: [
471
- { binding: 0, resource: { buffer: params } },
472
- { binding: 1, resource: { buffer: accumulation } },
473
- { binding: 2, resource: { buffer: sources[0] } },
474
- { binding: 3, resource: { buffer: sources[1] } },
475
- { binding: 4, resource: { buffer: targets[0] } },
476
- { binding: 5, resource: { buffer: targets[1] } },
477
- ],
478
- });
500
+ // The same few groups serve every denoise of an image size: they are made once and kept.
501
+ const key = [params, accumulation, ...sources, ...targets].map(buffer => this.idOf(buffer)).join();
502
+ let group = this.groups.get(key);
503
+
504
+ if (!group) {
505
+ group = this.device.createBindGroup({
506
+ layout: this.layout,
507
+ entries: [
508
+ { binding: 0, resource: { buffer: params } },
509
+ { binding: 1, resource: { buffer: accumulation } },
510
+ { binding: 2, resource: { buffer: sources[0] } },
511
+ { binding: 3, resource: { buffer: sources[1] } },
512
+ { binding: 4, resource: { buffer: targets[0] } },
513
+ { binding: 5, resource: { buffer: targets[1] } },
514
+ { binding: 6, resource: { buffer: guides } },
515
+ { binding: 7, resource: { buffer: roughnesses } },
516
+ ],
517
+ });
518
+ // A new accumulation buffer comes with a new size, which clears these; the cap is only a guard.
519
+ if (this.groups.size > 64) this.groups.clear();
520
+
521
+ this.groups.set(key, group);
522
+ }
479
523
 
480
524
  const pass = encoder.beginComputePass({ label: "denoise" });
481
525
 
@@ -509,6 +553,7 @@ export class DenoisePass {
509
553
  for (const buffer of [...this.buffers, ...this.params]) buffer.destroy();
510
554
 
511
555
  this.buffers = [];
556
+ this.groups.clear();
512
557
  this.output = null;
513
558
  }
514
559
  }