playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
3 lines (2 loc) • 15.2 kB
TypeScript
declare const _default: "\n// Real-Time Polygonal-Light Shading with Linearly Transformed Cosines\n// by Eric Heitz, Jonathan Dupuy, Stephen Hill and David Neubelt\n// code: https://github.com/selfshadow/ltc_code/\n\nfn LTC_Uv(N: vec3f, V: vec3f, roughness: f32) -> vec2f {\n const LUT_SIZE: f32 = 64.0;\n const LUT_SCALE: f32 = (LUT_SIZE - 1.0) / LUT_SIZE;\n const LUT_BIAS: f32 = 0.5 / LUT_SIZE;\n let dotNV: f32 = saturate(dot( N, V ));\n // texture parameterized by sqrt( GGX alpha ) and sqrt( 1 - cos( theta ) )\n let uv: vec2f = vec2f( roughness, sqrt( 1.0 - dotNV ) );\n return uv * LUT_SCALE + LUT_BIAS;\n}\n\nfn LTC_ClippedSphereFormFactor( f: vec3f ) -> f32 {\n // Real-Time Area Lighting: a Journey from Research to Production (p.102)\n // An approximation of the form factor of a horizon-clipped rectangle.\n let l: f32 = length( f );\n return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\n\nfn LTC_EdgeVectorFormFactor( v1: vec3f, v2: vec3f ) -> vec3f {\n let x: f32 = dot( v1, v2 );\n let y: f32 = abs( x );\n // rational polynomial approximation to theta / sin( theta ) / 2PI\n let a: f32 = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n let b: f32 = 3.4175940 + ( 4.1616724 + y ) * y;\n let v: f32 = a / b;\n let inv_sqrt_term = inverseSqrt( max( 1.0 - x * x, 1e-7f ) );\n let theta_sintheta: f32 = select( (0.5 * inv_sqrt_term - v), v, x > 0.0 );\n return cross( v1, v2 ) * theta_sintheta;\n}\n\nstruct Coords {\n coord0: vec3f,\n coord1: vec3f,\n coord2: vec3f,\n coord3: vec3f,\n}\n\nfn LTC_EvaluateRect( N: vec3f, V: vec3f, P: vec3f, mInv: mat3x3f, rectCoords: Coords) -> f32 {\n // bail if point is on back side of plane of light\n // assumes ccw winding order of light vertices\n let v1: vec3f = rectCoords.coord1 - rectCoords.coord0;\n let v2: vec3f = rectCoords.coord3 - rectCoords.coord0;\n\n let lightNormal: vec3f = cross( v1, v2 );\n let factor: f32 = sign(-dot( lightNormal, P - rectCoords.coord0 ));\n\n // construct orthonormal basis around N\n let T1: vec3f = normalize( V - N * dot( V, N ) );\n let T2: vec3f = factor * cross( N, T1 ); // negated from paper; possibly due to a different handedness of world coordinate system\n // compute transform\n let mat: mat3x3f = mInv * transpose( mat3x3f( T1, T2, N ) );\n // transform rect\n var coords: array<vec3f, 4>;\n coords[0] = mat * ( rectCoords.coord0 - P );\n coords[1] = mat * ( rectCoords.coord1 - P );\n coords[2] = mat * ( rectCoords.coord2 - P );\n coords[3] = mat * ( rectCoords.coord3 - P );\n // project rect onto sphere\n coords[0] = normalize( coords[0] );\n coords[1] = normalize( coords[1] );\n coords[2] = normalize( coords[2] );\n coords[3] = normalize( coords[3] );\n // calculate vector form factor\n var vectorFormFactor: vec3f = vec3f( 0.0 );\n vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[0], coords[1] );\n vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[1], coords[2] );\n vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[2], coords[3] );\n vectorFormFactor = vectorFormFactor + LTC_EdgeVectorFormFactor( coords[3], coords[0] );\n // adjust for horizon clipping\n let result: f32 = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\n return result;\n}\n\nvar<private> dLTCCoords: Coords;\nfn getLTCLightCoords(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) -> Coords {\n var coords: Coords;\n coords.coord0 = lightPos + halfWidth - halfHeight;\n coords.coord1 = lightPos - halfWidth - halfHeight;\n coords.coord2 = lightPos - halfWidth + halfHeight;\n coords.coord3 = lightPos + halfWidth + halfHeight;\n return coords;\n}\n\nvar<private> dSphereRadius: f32;\nfn getSphereLightCoords(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) -> Coords {\n // used for simple sphere light falloff\n // also, the code only handles a spherical light, it cannot be non-uniformly scaled in world space, and so we enforce it here\n dSphereRadius = max(length(halfWidth), length(halfHeight));\n\n // Billboard the 2d light quad to reflection vector, as it's used for specular. This allows us to use disk math for the sphere.\n let f: vec3f = reflect(normalize(lightPos - uniform.view_position), vNormalW);\n let w: vec3f = normalize(cross(f, halfHeight));\n let h: vec3f = normalize(cross(f, w));\n\n return getLTCLightCoords(lightPos, w * dSphereRadius, h * dSphereRadius);\n}\n\n// used for LTC LUT texture lookup\nvar<private> dLTCUV: vec2f;\n#ifdef LIT_CLEARCOAT\n var<private> ccLTCUV: vec2f;\n#endif\n\nfn getLTCLightUV(gloss: f32, worldNormal: vec3f, viewDir: vec3f) -> vec2f {\n let roughness: f32 = max((1.0 - gloss) * (1.0 - gloss), 0.001);\n return LTC_Uv( worldNormal, viewDir, roughness );\n}\n\n// used for energy conservation and to modulate specular\nvar<private> dLTCSpecFres: vec3f;\n#ifdef LIT_CLEARCOAT\n var<private> ccLTCSpecFres: vec3f;\n#endif\n\nfn getLTCLightSpecFres(uv: vec2f, specularity: vec3f) -> vec3f {\n let t2: vec4f = textureSampleLevel(areaLightsLutTex2, areaLightsLutTex2Sampler, uv, 0.0);\n return specularity * t2.x + ( vec3f( 1.0 ) - specularity) * t2.y;\n}\n\nfn calcLTCLightValues(gloss: f32, worldNormal: vec3f, viewDir: vec3f, specularity: vec3f, clearcoatGloss: f32, clearcoatWorldNormal: vec3f, clearcoatSpecularity: f32) {\n dLTCUV = getLTCLightUV(gloss, worldNormal, viewDir);\n dLTCSpecFres = getLTCLightSpecFres(dLTCUV, specularity);\n\n #ifdef LIT_CLEARCOAT\n ccLTCUV = getLTCLightUV(clearcoatGloss, clearcoatWorldNormal, viewDir);\n ccLTCSpecFres = getLTCLightSpecFres(ccLTCUV, vec3f(clearcoatSpecularity));\n #endif\n}\n\nfn calcRectLightValues(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) {\n dLTCCoords = getLTCLightCoords(lightPos, halfWidth, halfHeight);\n}\nfn calcDiskLightValues(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) {\n calcRectLightValues(lightPos, halfWidth, halfHeight);\n}\nfn calcSphereLightValues(lightPos: vec3f, halfWidth: vec3f, halfHeight: vec3f) {\n dLTCCoords = getSphereLightCoords(lightPos, halfWidth, halfHeight);\n}\n\n// An extended version of the implementation from \"How to solve a cubic equation, revisited\"\n// http://momentsingraphics.de/?p=105\nfn SolveCubic(Coefficient_in: vec4f) -> vec3f {\n let pi: f32 = 3.14159;\n var Coefficient = Coefficient_in;\n\n // Normalize the polynomial\n Coefficient = vec4f(Coefficient.xyz / Coefficient.w, Coefficient.w);\n\n // Divide middle coefficients by three\n let new_yz: vec2f = Coefficient.yz / 3.0;\n Coefficient = vec4f(Coefficient.x, new_yz.x, new_yz.y, Coefficient.w);\n \n let A: f32 = Coefficient.w;\n let B: f32 = Coefficient.z;\n let C: f32 = Coefficient.y;\n let D: f32 = Coefficient.x;\n\n // Compute the Hessian and the discriminant\n let Delta: vec3f = vec3f(\n -Coefficient.z * Coefficient.z + Coefficient.y,\n -Coefficient.y * Coefficient.z + Coefficient.x,\n dot(vec2f(Coefficient.z, -Coefficient.y), Coefficient.xy)\n );\n\n let Discriminant: f32 = dot(vec2f(4.0 * Delta.x, -Delta.y), Delta.zy);\n\n var xlc: vec2f;\n var xsc: vec2f;\n\n // Algorithm A\n {\n let A_a: f32 = 1.0;\n let C_a: f32 = Delta.x;\n let D_a: f32 = -2.0 * B * Delta.x + Delta.y;\n\n // Take the cubic root of a normalized complex number\n let Theta: f32 = atan2(sqrt(Discriminant), -D_a) / 3.0;\n\n let sqrt_neg_Ca = sqrt(-C_a);\n let x_1a: f32 = 2.0 * sqrt_neg_Ca * cos(Theta);\n let x_3a: f32 = 2.0 * sqrt_neg_Ca * cos(Theta + (2.0 / 3.0) * pi);\n\n let xl: f32 = select(x_3a, x_1a, (x_1a + x_3a) > 2.0 * B);\n xlc = vec2f(xl - B, A);\n }\n\n // Algorithm D\n {\n let A_d: f32 = D;\n let C_d: f32 = Delta.z;\n let D_d: f32 = -D * Delta.y + 2.0 * C * Delta.z;\n\n // Take the cubic root of a normalized complex number\n let Theta: f32 = atan2(D * sqrt(Discriminant), -D_d) / 3.0;\n\n let sqrt_neg_Cd = sqrt(-C_d);\n let x_1d: f32 = 2.0 * sqrt_neg_Cd * cos(Theta);\n let x_3d: f32 = 2.0 * sqrt_neg_Cd * cos(Theta + (2.0 / 3.0) * pi);\n\n let xs: f32 = select(x_3d, x_1d, x_1d + x_3d < 2.0 * C);\n xsc = vec2f(-D, xs + C);\n }\n\n let E: f32 = xlc.y * xsc.y;\n let F: f32 = -xlc.x * xsc.y - xlc.y * xsc.x;\n let G: f32 = xlc.x * xsc.x;\n\n let xmc: vec2f = vec2f(C * F - B * G, -B * F + C * E);\n\n var Root: vec3f = vec3f(xsc.x / xsc.y, xmc.x / xmc.y, xlc.x / xlc.y);\n\n if (Root.x < Root.y && Root.x < Root.z) {\n Root = Root.yxz;\n } else if (Root.z < Root.x && Root.z < Root.y) {\n Root = Root.xzy;\n }\n return Root;\n}\n\nfn LTC_EvaluateDisk(N: vec3f, V: vec3f, P: vec3f, Minv: mat3x3f, points: Coords) -> f32 {\n // construct orthonormal basis around N\n let T1: vec3f = normalize(V - N * dot(V, N));\n let T2: vec3f = cross(N, T1);\n\n // rotate area light in (T1, T2, N) basis\n let R: mat3x3f = transpose( mat3x3f( T1, T2, N ) );\n // polygon (allocate 5 vertices for clipping\n var L_: array<vec3f, 3>;\n L_[0] = R * ( points.coord0 - P );\n L_[1] = R * ( points.coord1 - P );\n L_[2] = R * ( points.coord2 - P );\n\n // init ellipse\n let C: vec3f = 0.5 * (L_[0] + L_[2]);\n var V1: vec3f = 0.5 * (L_[1] - L_[2]);\n var V2: vec3f = 0.5 * (L_[1] - L_[0]);\n\n let C_Minv: vec3f = Minv * C;\n let V1_Minv: vec3f = Minv * V1;\n let V2_Minv: vec3f = Minv * V2;\n\n // compute eigenvectors of ellipse\n var a: f32;\n var b: f32;\n let d11: f32 = dot(V1_Minv, V1_Minv);\n let d22: f32 = dot(V2_Minv, V2_Minv);\n let d12: f32 = dot(V1_Minv, V2_Minv);\n if (abs(d12) / sqrt(d11 * d22) > 0.0001) {\n let tr: f32 = d11 + d22;\n let det_inner: f32 = -d12 * d12 + d11 * d22;\n let det: f32 = sqrt(det_inner);\n let u: f32 = 0.5 * sqrt(tr - 2.0 * det);\n let v: f32 = 0.5 * sqrt(tr + 2.0 * det);\n let e_max: f32 = (u + v) * (u + v);\n let e_min: f32 = (u - v) * (u - v);\n\n var V1_: vec3f;\n var V2_: vec3f;\n\n if (d11 > d22) {\n V1_ = d12 * V1_Minv + (e_max - d11) * V2_Minv;\n V2_ = d12 * V1_Minv + (e_min - d11) * V2_Minv;\n } else {\n V1_ = d12*V2_Minv + (e_max - d22)*V1_Minv;\n V2_ = d12*V2_Minv + (e_min - d22)*V1_Minv;\n }\n\n a = 1.0 / e_max;\n b = 1.0 / e_min;\n V1 = normalize(V1_);\n V2 = normalize(V2_);\n } else {\n a = 1.0 / dot(V1_Minv, V1_Minv);\n b = 1.0 / dot(V2_Minv, V2_Minv);\n V1 = V1_Minv * sqrt(a);\n V2 = V2_Minv * sqrt(b);\n }\n\n var V3: vec3f = normalize(cross(V1, V2));\n if (dot(C_Minv, V3) < 0.0) {\n V3 = V3 * -1.0;\n }\n\n let L: f32 = dot(V3, C_Minv);\n let x0: f32 = dot(V1, C_Minv) / L;\n let y0: f32 = dot(V2, C_Minv) / L;\n\n let E1: f32 = inverseSqrt(a);\n let E2: f32 = inverseSqrt(b);\n\n let a_scaled = a * L * L;\n let b_scaled = b * L * L;\n\n let c0: f32 = a_scaled * b_scaled;\n let c1: f32 = a_scaled * b_scaled * (1.0 + x0 * x0 + y0 * y0) - a_scaled - b_scaled;\n let c2: f32 = 1.0 - a_scaled * (1.0 + x0 * x0) - b_scaled * (1.0 + y0 * y0);\n let c3: f32 = 1.0;\n\n let roots: vec3f = SolveCubic(vec4f(c0, c1, c2, c3));\n let e1: f32 = roots.x;\n let e2: f32 = roots.y;\n let e3: f32 = roots.z;\n\n var avgDir: vec3f = vec3f(a_scaled * x0 / (a_scaled - e2), b_scaled * y0 / (b_scaled - e2), 1.0);\n\n let rotate: mat3x3f = mat3x3f(V1, V2, V3);\n\n avgDir = rotate * avgDir;\n avgDir = normalize(avgDir);\n\n let L1: f32 = sqrt(-e2 / e3);\n let L2: f32 = sqrt(-e2 / e1);\n\n let formFactor: f32 = max(0.0, L1 * L2 * inverseSqrt((1.0 + L1 * L1) * (1.0 + L2 * L2)));\n\n const LUT_SIZE_disk: f32 = 64.0;\n const LUT_SCALE_disk: f32 = ( LUT_SIZE_disk - 1.0 ) / LUT_SIZE_disk;\n const LUT_BIAS_disk: f32 = 0.5 / LUT_SIZE_disk;\n\n // use tabulated horizon-clipped sphere\n var uv: vec2f = vec2f(avgDir.z * 0.5 + 0.5, formFactor);\n uv = uv * LUT_SCALE_disk + LUT_BIAS_disk;\n\n let scale: f32 = textureSampleLevel(areaLightsLutTex2, areaLightsLutTex2Sampler, uv, 0.0).w;\n\n return formFactor * scale;\n}\n\n// LTC_EvaluateDisk in some rare cases genereates NaN values in a or b, just before 'float c0 = a * b;'\n// Get rid of those Nan values before they propagate further, as in case of bloom / DOF blurs they\n// propagate to large areas. I didn't find the actual reason where those come from, so that is still TODO.\n// Note that only disk/sphere lights are causing it, so only handle those.\nfn FixNan(value: f32) -> f32 {\n // use value != value check for NaN as isnan() is not available in WGSL\n return select(value, 0.0, value != value);\n}\n\nfn getRectLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDir: vec3f, lightDirNorm: vec3f) -> f32 {\n let identityMat = mat3x3f(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0));\n return LTC_EvaluateRect( worldNormal, viewDir, vPositionW, identityMat, dLTCCoords );\n}\n\nfn getDiskLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDir: vec3f, lightDirNorm: vec3f) -> f32 {\n let identityMat = mat3x3f(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0));\n return FixNan(LTC_EvaluateDisk( worldNormal, viewDir, vPositionW, identityMat, dLTCCoords ));\n}\n\nfn getSphereLightDiffuse(worldNormal: vec3f, viewDir: vec3f, lightDir: vec3f, lightDirNorm: vec3f) -> f32 {\n // NB: this could be improved further with distance based wrap lighting\n let falloff: f32 = dSphereRadius / (dot(lightDir, lightDir) + dSphereRadius);\n return FixNan(getLightDiffuse(worldNormal, viewDir, lightDirNorm) * falloff);\n}\n\nfn getLTCLightInvMat(uv: vec2f) -> mat3x3f {\n let t1: vec4f = textureSampleLevel(areaLightsLutTex1, areaLightsLutTex1Sampler, uv, 0.0);\n\n return mat3x3f(\n vec3f( t1.x, 0.0, t1.y ),\n vec3f( 0.0, 1.0, 0.0 ),\n vec3f( t1.z, 0.0, t1.w )\n );\n}\n\nfn calcRectLightSpecular(worldNormal: vec3f, viewDir: vec3f, uv: vec2f) -> f32 {\n let mInv: mat3x3f = getLTCLightInvMat(uv);\n return LTC_EvaluateRect( worldNormal, viewDir, vPositionW, mInv, dLTCCoords );\n}\n\nfn getRectLightSpecular(worldNormal: vec3f, viewDir: vec3f) -> f32 {\n return calcRectLightSpecular(worldNormal, viewDir, dLTCUV);\n}\n\nfn calcDiskLightSpecular(worldNormal: vec3f, viewDir: vec3f, uv: vec2f) -> f32 {\n let mInv: mat3x3f = getLTCLightInvMat(uv);\n return LTC_EvaluateDisk( worldNormal, viewDir, vPositionW, mInv, dLTCCoords );\n}\n\nfn getDiskLightSpecular(worldNormal: vec3f, viewDir: vec3f) -> f32 {\n return calcDiskLightSpecular(worldNormal, viewDir, dLTCUV);\n}\n\nfn getSphereLightSpecular(worldNormal: vec3f, viewDir: vec3f) -> f32 {\n return calcDiskLightSpecular(worldNormal, viewDir, dLTCUV);\n}\n";
export default _default;