#ifndef UTS_AREA_LIGHT_INLCUDE #define UTS_AREA_LIGHT_INLCUDE // The output is *not* normalized by the factor of 1/TWO_PI (this is done by the PolygonFormFactor function). real3 UTS_ComputeEdgeFactor(real3 V1, real3 V2) { real V1oV2 = dot(V1, V2); real3 V1xV2 = cross(V1, V2); // Plane normal (tangent to the unit sphere) real sqLen = saturate(1 - V1oV2 * V1oV2); // length(V1xV2) = abs(sin(angle)) real rcpLen = rsqrt(max(FLT_EPS, sqLen)); // Make sure it is finite #if 0 real y = rcpLen * acos(V1oV2); #else // Let y[x_] = ArcCos[x] / Sqrt[1 - x^2]. // Range reduction: since ArcCos[-x] == Pi - ArcCos[x], we only need to consider x on [0, 1]. real x = abs(V1oV2); // Limit[y[x], x -> 1] == 1, // Limit[y[x], x -> 0] == Pi/2. // The approximation is exact at the endpoints of [0, 1]. // Max. abs. error on [0, 1] is 1.33e-6 at x = 0.0036. // Max. rel. error on [0, 1] is 8.66e-7 at x = 0.0037. real y = HALF_PI + x * (-0.99991 + x * (0.783393 + x * (-0.649178 + x * (0.510589 + x * (-0.326137 + x * (0.137528 + x * -0.0270813)))))); if (V1oV2 < 0) { y = rcpLen * PI - y; } #endif return V1xV2 * y; } // Input: 3-5 vertices in the coordinate frame centered at the shaded point. // Output: signed vector irradiance. // No horizon clipping is performed. real3 UTS_PolygonFormFactor(real4x3 L, real3 L4, uint n, bool isDiffuse) { // The length cannot be zero since we have already checked // that the light has a non-zero effective area, // and thus its plane cannot pass through the origin. L[0] = normalize(L[0]); L[1] = normalize(L[1]); L[2] = normalize(L[2]); switch (n) { case 3: L[3] = L[0]; break; case 4: L[3] = normalize(L[3]); L4 = L[0]; break; case 5: L[3] = normalize(L[3]); L4 = normalize(L4); break; } // If the magnitudes of a pair of edge factors are // nearly the same, catastrophic cancellation may occur: // https://en.wikipedia.org/wiki/Catastrophic_cancellation // For the same reason, the value of the cross product of two // nearly collinear vectors is prone to large errors. // Therefore, the algorithm is inherently numerically unstable // for area lights that shrink to a line (or a point) after // projection onto the unit sphere. real3 F = UTS_ComputeEdgeFactor(L[0], L[1]); F += UTS_ComputeEdgeFactor(L[1], L[2]); F += UTS_ComputeEdgeFactor(L[2], L[3]); if (n >= 4) F += UTS_ComputeEdgeFactor(L[3], L4); if (n == 5) F += UTS_ComputeEdgeFactor(L4, L[0]); return lerp(INV_TWO_PI * F, HALF_PI * F, isDiffuse); // The output may be projected onto the tangent plane (F.z) to yield signed irradiance. } // See "Real-Time Area Lighting: a Journey from Research to Production", slide 102. // Turns out, despite the authors claiming that this function "calculates an approximation of // the clipped sphere form factor", that is simply not true. // First of all, above horizon, the function should then just return 'F.z', which it does not. // Secondly, if we use the correct function called DiffuseSphereLightIrradiance(), it results // in severe light leaking if the light is placed vertically behind the camera. // So this function is clearly a hack designed to work around these problems. real UTS_PolygonIrradianceFromVectorFormFactor(float3 F, bool isDiffuse) { float l = length(F); float z = lerp(F.z , INV_TWO_PI * F.z, isDiffuse); return max(0, (l * l + z) / (l + 1)); } // Expects non-normalized vertex positions. // Output: F is the signed vector irradiance. real UTS_PolygonIrradiance(real4x3 L, bool isDiffuse, out real3 F) { //APPROXIMATE_POLY_LIGHT_AS_SPHERE_LIGHT F = UTS_PolygonFormFactor(L, float3(0,0,1), 4, isDiffuse); return UTS_PolygonIrradianceFromVectorFormFactor(F, isDiffuse); // Accounts for the horizon. } // This function assumes that inputs are well-behaved, e.i. // that the line does not pass through the origin and // that the light is (at least partially) above the surface. float UTS_Diffuse_Line(float3 C, float3 A, float hl) { // Solve C.z + h * A.z = 0. float h = -C.z * rcp(A.z); // May be Inf, but never NaN // Clip the line segment against the z-plane if necessary. float h2 = (A.z >= 0) ? max( hl, h) : min( hl, h); // P2 = C + h2 * A float h1 = (A.z >= 0) ? max(-hl, h) : min(-hl, h); // P1 = C + h1 * A // Normalize the tangent. float as = dot(A, A); // |A|^2 float ar = rsqrt(as); // 1/|A| float a = as * ar; // |A| float3 T = A * ar; // A/|A| // Orthogonal 2D coordinates: // P(n, t) = n * N + t * T. float tc = dot(T, C); // C = n * N + tc * T float3 P0 = C - tc * T; // P(n, 0) = n * N float ns = dot(P0, P0); // |P0|^2 float nr = rsqrt(ns); // 1/|P0| float n = ns * nr; // |P0| float Nz = P0.z * nr; // N.z = P0.z/|P0| // P(n, t) - C = P0 + t * T - P0 - tc * T // = (t - tc) * T = h * A = (h * a) * T. float t2 = tc + h2 * a; // P2.t float t1 = tc + h1 * a; // P1.t float s2 = ns + t2 * t2; // |P2|^2 float s1 = ns + t1 * t1; // |P1|^2 float mr = rsqrt(s1 * s2); // 1/(|P1|*|P2|) float r2 = s1 * (mr * mr); // 1/|P2|^2 float r1 = s2 * (mr * mr); // 1/|P1|^2 // I = (i1 + i2 + i3) / Pi. // i1 = N.z * (P2.t / |P2|^2 - P1.t / |P1|^2). // i2 = -T.z * (P2.n / |P2|^2 - P1.n / |P1|^2). // i3 = N.z * ArcCos[Dot[P1, P2] / (|P1| * |P2|)] / |P0|. float i12 = (Nz * t2 - (T.z * n)) * r2 - (Nz * t1 - (T.z * n)) * r1; // Guard against numerical errors. float dt = min(1, (ns + t1 * t2) * mr); float i3 = acos(dt) * (Nz * nr); // angle * cos(θ) / r^2 //return T.z * n ; // Guard against numerical errors. return INV_PI * max(0, i12 + i3); } float4 UTS_EvaluateLTC_Area(bool isRectLight, float3 center, float3 right, float3 up, float halfLength, float halfHeight, float3x3 invM, float perceptualRoughness, bool isDiffuse, int cookieMode, float4 cookieScaleOffset) { float3 ortho = cross(center, right); float orthoSq = dot(ortho, ortho); // Check whether the light is in a vertical orientation. bool quit = (orthoSq == 0); // Check whether the light is entirely below the surface. // We must test twice, since a linear transformation // may bring the light above the surface (a side-effect). quit = quit || (center.z + halfLength * abs(right.z) + halfHeight * abs(up.z) <= 0); float4 ltcValue = float4(1, 1, 1, 0); if (quit && !isDiffuse) { return ltcValue; } // Perform a sparse matrix multiplication. float3 C = mul(invM, center); float3 A = mul(invM, right); float3 B = mul(invM, up); // Check whether the light is entirely below the surface. // We must test twice, since a linear transformation // may bring the light below the surface (as expected). if (C.z + halfLength * abs(A.z) + halfHeight * abs(B.z) <= 0 && !isDiffuse) { return ltcValue; } if (isRectLight) { float4x3 lightVerts; lightVerts[0] = C - halfLength * A - halfHeight * B; // LL lightVerts[1] = lightVerts[0] + (2 * halfHeight) * B; // UL lightVerts[2] = lightVerts[1] + (2 * halfLength) * A; // UR lightVerts[3] = lightVerts[2] - (2 * halfHeight) * B; // LR // Polygon irradiance in the transformed configuration. float3 fromFactor; ltcValue.a = UTS_PolygonIrradiance(lightVerts, isDiffuse, fromFactor); if (cookieMode != COOKIEMODE_NONE) { ltcValue.rgb = SampleAreaLightCookie(cookieScaleOffset, lightVerts, fromFactor, perceptualRoughness); } } else // Line light { float w = ComputeLineWidthFactor(invM, ortho, orthoSq); ltcValue.a = UTS_Diffuse_Line(C, A, halfLength) * w; } return ltcValue; } #endif