123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- #ifndef UNITY_AREA_LIGHTING_INCLUDED
- #define UNITY_AREA_LIGHTING_INCLUDED
-
- #define APPROXIMATE_POLY_LIGHT_AS_SPHERE_LIGHT
- #define APPROXIMATE_SPHERE_LIGHT_NUMERICALLY
-
-
- // 'sinSqSigma' is the sine^2 of the half-angle subtended by the sphere (aperture) as seen from the shaded point.
- // 'cosOmega' is the cosine of the angle between the normal and the direction to the center of the light.
- // This function performs horizon clipping.
- real DiffuseSphereLightIrradiance(real sinSqSigma, real cosOmega)
- {
- #ifdef APPROXIMATE_SPHERE_LIGHT_NUMERICALLY
- real x = sinSqSigma;
- real y = cosOmega;
-
- // Use a numerical fit found in Mathematica. Mean absolute error: 0.00476944.
- // You can use the following Mathematica code to reproduce our results:
- // t = Flatten[Table[{x, y, f[x, y]}, {x, 0, 0.999999, 0.001}, {y, -0.999999, 0.999999, 0.002}], 1]
- // m = NonlinearModelFit[t, x * (y + e) * (0.5 + (y - e) * (a + b * x + c * x^2 + d * x^3)), {a, b, c, d, e}, {x, y}]
- return saturate(x * (0.9245867471551246 + y) * (0.5 + (-0.9245867471551246 + y) * (0.5359050373687144 + x * (-1.0054221851257754 + x * (1.8199061187417047 - x * 1.3172081704209504)))));
- #else
- #if 0 // Ref: Area Light Sources for Real-Time Graphics, page 4 (1996).
- real sinSqOmega = saturate(1 - cosOmega * cosOmega);
- real cosSqSigma = saturate(1 - sinSqSigma);
- real sinSqGamma = saturate(cosSqSigma / sinSqOmega);
- real cosSqGamma = saturate(1 - sinSqGamma);
-
- real sinSigma = sqrt(sinSqSigma);
- real sinGamma = sqrt(sinSqGamma);
- real cosGamma = sqrt(cosSqGamma);
-
- real sigma = asin(sinSigma);
- real omega = acos(cosOmega);
- real gamma = asin(sinGamma);
-
- if (omega >= HALF_PI + sigma)
- {
- // Full horizon occlusion (case #4).
- return 0;
- }
-
- real e = sinSqSigma * cosOmega;
-
- UNITY_BRANCH
- if (omega < HALF_PI - sigma)
- {
- // No horizon occlusion (case #1).
- return e;
- }
- else
- {
- real g = (-2 * sqrt(sinSqOmega * cosSqSigma) + sinGamma) * cosGamma + (HALF_PI - gamma);
- real h = cosOmega * (cosGamma * sqrt(saturate(sinSqSigma - cosSqGamma)) + sinSqSigma * asin(saturate(cosGamma / sinSigma)));
-
- if (omega < HALF_PI)
- {
- // Partial horizon occlusion (case #2).
- return saturate(e + INV_PI * (g - h));
- }
- else
- {
- // Partial horizon occlusion (case #3).
- return saturate(INV_PI * (g + h));
- }
- }
- #else // Ref: Moving Frostbite to Physically Based Rendering, page 47 (2015, optimized).
- real cosSqOmega = cosOmega * cosOmega; // y^2
-
- UNITY_BRANCH
- if (cosSqOmega > sinSqSigma) // (y^2)>x
- {
- return saturate(sinSqSigma * cosOmega); // Clip[x*y,{0,1}]
- }
- else
- {
- real cotSqSigma = rcp(sinSqSigma) - 1; // 1/x-1
- real tanSqSigma = rcp(cotSqSigma); // x/(1-x)
- real sinSqOmega = 1 - cosSqOmega; // 1-y^2
-
- real w = sinSqOmega * tanSqSigma; // (1-y^2)*(x/(1-x))
- real x = -cosOmega * rsqrt(w); // -y*Sqrt[(1/x-1)/(1-y^2)]
- real y = sqrt(sinSqOmega * tanSqSigma - cosSqOmega); // Sqrt[(1-y^2)*(x/(1-x))-y^2]
- real z = y * cotSqSigma; // Sqrt[(1-y^2)*(x/(1-x))-y^2]*(1/x-1)
-
- real a = cosOmega * acos(x) - z; // y*ArcCos[-y*Sqrt[(1/x-1)/(1-y^2)]]-Sqrt[(1-y^2)*(x/(1-x))-y^2]*(1/x-1)
- real b = atan(y); // ArcTan[Sqrt[(1-y^2)*(x/(1-x))-y^2]]
-
- return saturate(INV_PI * (a * sinSqSigma + b));
- }
- #endif
- #endif
- }
-
- // The output is *not* normalized by the factor of 1/TWO_PI (this is done by the PolygonFormFactor function).
- real3 ComputeEdgeFactor(real3 V1, real3 V2)
- {
- real subtendedAngle;
-
- 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_MIN, 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 PolygonFormFactor(real4x3 L, real3 L4, uint n)
- {
- // 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 = ComputeEdgeFactor(L[0], L[1]);
- F += ComputeEdgeFactor(L[1], L[2]);
- F += ComputeEdgeFactor(L[2], L[3]);
- if (n >= 4)
- F += ComputeEdgeFactor(L[3], L4);
- if (n == 5)
- F += ComputeEdgeFactor(L4, L[0]);
-
- return INV_TWO_PI * F; // 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 PolygonIrradianceFromVectorFormFactor(float3 F)
- {
- #if 1
- float l = length(F);
- return max(0, (l * l + F.z) / (l + 1));
- #else
- real sff = saturate(dot(F, F));
- real sinSqAperture = sqrt(sff);
- real cosElevationAngle = F.z * rsqrt(sff);
-
- return DiffuseSphereLightIrradiance(sinSqAperture, cosElevationAngle);
- #endif
- }
-
- // Expects non-normalized vertex positions.
- // Output: F is the signed vector irradiance.
- real PolygonIrradiance(real4x3 L, out real3 F)
- {
- #ifdef APPROXIMATE_POLY_LIGHT_AS_SPHERE_LIGHT
- F = PolygonFormFactor(L, real3(0,0,1), 4); // Before horizon clipping.
-
- return PolygonIrradianceFromVectorFormFactor(F); // Accounts for the horizon.
- #else
- // 1. ClipQuadToHorizon
-
- // detect clipping config
- uint config = 0;
- if (L[0].z > 0) config += 1;
- if (L[1].z > 0) config += 2;
- if (L[2].z > 0) config += 4;
- if (L[3].z > 0) config += 8;
-
- // The fifth vertex for cases when clipping cuts off one corner.
- // Due to a compiler bug, copying L into a vector array with 5 rows
- // messes something up, so we need to stick with the matrix + the L4 vertex.
- real3 L4 = L[3];
-
- // This switch is surprisingly fast. Tried replacing it with a lookup array of vertices.
- // Even though that replaced the switch with just some indexing and no branches, it became
- // way, way slower - mem fetch stalls?
-
- // clip
- uint n = 0;
- switch (config)
- {
- case 0: // clip all
- break;
-
- case 1: // V1 clip V2 V3 V4
- n = 3;
- L[1] = -L[1].z * L[0] + L[0].z * L[1];
- L[2] = -L[3].z * L[0] + L[0].z * L[3];
- break;
-
- case 2: // V2 clip V1 V3 V4
- n = 3;
- L[0] = -L[0].z * L[1] + L[1].z * L[0];
- L[2] = -L[2].z * L[1] + L[1].z * L[2];
- break;
-
- case 3: // V1 V2 clip V3 V4
- n = 4;
- L[2] = -L[2].z * L[1] + L[1].z * L[2];
- L[3] = -L[3].z * L[0] + L[0].z * L[3];
- break;
-
- case 4: // V3 clip V1 V2 V4
- n = 3;
- L[0] = -L[3].z * L[2] + L[2].z * L[3];
- L[1] = -L[1].z * L[2] + L[2].z * L[1];
- break;
-
- case 5: // V1 V3 clip V2 V4: impossible
- break;
-
- case 6: // V2 V3 clip V1 V4
- n = 4;
- L[0] = -L[0].z * L[1] + L[1].z * L[0];
- L[3] = -L[3].z * L[2] + L[2].z * L[3];
- break;
-
- case 7: // V1 V2 V3 clip V4
- n = 5;
- L4 = -L[3].z * L[0] + L[0].z * L[3];
- L[3] = -L[3].z * L[2] + L[2].z * L[3];
- break;
-
- case 8: // V4 clip V1 V2 V3
- n = 3;
- L[0] = -L[0].z * L[3] + L[3].z * L[0];
- L[1] = -L[2].z * L[3] + L[3].z * L[2];
- L[2] = L[3];
- break;
-
- case 9: // V1 V4 clip V2 V3
- n = 4;
- L[1] = -L[1].z * L[0] + L[0].z * L[1];
- L[2] = -L[2].z * L[3] + L[3].z * L[2];
- break;
-
- case 10: // V2 V4 clip V1 V3: impossible
- break;
-
- case 11: // V1 V2 V4 clip V3
- n = 5;
- L[3] = -L[2].z * L[3] + L[3].z * L[2];
- L[2] = -L[2].z * L[1] + L[1].z * L[2];
- break;
-
- case 12: // V3 V4 clip V1 V2
- n = 4;
- L[1] = -L[1].z * L[2] + L[2].z * L[1];
- L[0] = -L[0].z * L[3] + L[3].z * L[0];
- break;
-
- case 13: // V1 V3 V4 clip V2
- n = 5;
- L[3] = L[2];
- L[2] = -L[1].z * L[2] + L[2].z * L[1];
- L[1] = -L[1].z * L[0] + L[0].z * L[1];
- break;
-
- case 14: // V2 V3 V4 clip V1
- n = 5;
- L4 = -L[0].z * L[3] + L[3].z * L[0];
- L[0] = -L[0].z * L[1] + L[1].z * L[0];
- break;
-
- case 15: // V1 V2 V3 V4
- n = 4;
- break;
- }
-
- if (n == 0) return 0;
-
- // 2. Integrate
- F = PolygonFormFactor(L, L4, n); // After the horizon clipping.
-
- // 3. Compute irradiance
- return max(0, F.z);
- #endif
- }
-
- // 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 I_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
-
- // Guard against numerical errors.
- return INV_PI * max(0, i12 + i3);
- }
-
- // Computes 1 / length(mul(transpose(inverse(invM)), normalize(ortho))).
- float ComputeLineWidthFactor(float3x3 invM, float3 ortho, float orthoSq)
- {
- // transpose(inverse(invM)) = 1 / determinant(invM) * cofactor(invM).
- // Take into account the sparsity of the matrix:
- // {{a,0,b},
- // {0,c,0},
- // {d,0,1}}
- float a = invM[0][0];
- float b = invM[0][2];
- float c = invM[1][1];
- float d = invM[2][0];
-
- float det = c * (a - b * d);
- float3 X = float3(c * (ortho.x - d * ortho.z),
- (ortho.y * (a - b * d)),
- c * (-b * ortho.x + a * ortho.z)); // mul(cof, ortho)
-
- // 1 / length(1/s * X) = abs(s) / length(X).
- return abs(det) * rsqrt(dot(X, X) * orthoSq) * orthoSq; // rsqrt(x^2) * x^2 = x
- }
-
- float I_ltc_line(float3x3 invM, float3 center, float3 axis, float halfLength)
- {
- float3 ortho = cross(center, axis);
- float orthoSq = dot(ortho, ortho);
-
- // Check whether the line passes through the origin.
- 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(axis.z) <= 0);
-
- // Transform into the diffuse configuration.
- // This is a sparse matrix multiplication.
- // Pay attention to the multiplication order
- // (in case your matrices are transposed).
- float3 C = mul(invM, center);
- float3 A = mul(invM, axis);
-
- // 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).
- quit = quit || (C.z + halfLength * abs(A.z) <= 0);
-
- float result = 0;
-
- if (!quit)
- {
- float w = ComputeLineWidthFactor(invM, ortho, orthoSq);
-
- result = I_diffuse_line(C, A, halfLength) * w;
- }
-
- return result;
- }
-
- #endif // UNITY_AREA_LIGHTING_INCLUDED
|