From 4c12e9c5abbbba52f99ea49c431e7390c2d08776 Mon Sep 17 00:00:00 2001 From: Ken Pruiksma Date: Wed, 7 Jul 2021 15:37:49 -0500 Subject: [PATCH] Refactoring the LTC quad and polygon lights (#1881) * Refactoring the LTC quad and polygon lights to clip the light to the normal hemisphere before applying the LTC matrix. This prevents specular light leaking from behind the surface. Diffuse and specular contribution are now calculated at the same time to avoid clipping the polygon twice. Added a cheaper diffuse integration function that's accurate enough for diffuse. Also added a commented out alternate specular integration for platforms with poor acos() accuracy. Signed-off-by: Ken Pruiksma * PR review feedback - typo and some saturate() protection Signed-off-by: Ken Pruiksma * Making branch more explicit from review feedback. Signed-off-by: Ken Pruiksma --- .../Atom/Features/PBR/Lights/Ltc.azsli | 249 ++++++++++++++---- .../Features/PBR/Lights/PolygonLight.azsli | 17 +- .../Atom/Features/PBR/Lights/QuadLight.azsli | 14 +- 3 files changed, 207 insertions(+), 73 deletions(-) diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ltc.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ltc.azsli index 292f8a8a22..8042758db8 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ltc.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ltc.azsli @@ -80,14 +80,48 @@ float3x3 BuildViewAlignedOrthonormalBasis(in float3 normal, in float3 dirToView) // xy plane in positive z space. float IntegrateEdge(float3 v1, float3 v2) { + // This alternate version may work better for platforms where acos() precision is low. + /* + float x = dot(v1, v2); + float y = abs(x); + + float a = 5.42031 + (3.12829 + 0.0902326 * y) * y; + float b = 3.45068 + (4.18814 + y) * y; + float theta_sinTheta = a / b; + + if (x < 0.0) + { + theta_sinTheta = PI * rsqrt(saturate(1.0 - x * x)) - theta_sinTheta; + } + + float3 u = cross(v1, v2); + return theta_sinTheta * u.z; + */ + float cosTheta = dot(v1, v2); - cosTheta = clamp(cosTheta, -0.9999, 0.9999); + float theta = acos(cosTheta); // calculate 1.0 / sin(theta) - float invSinTheta = rsqrt(1.0 - cosTheta * cosTheta); + float invSinTheta = rsqrt(saturate(1.0 - cosTheta * cosTheta)); - float theta = acos(cosTheta); - return cross(v1, v2).z * theta * invSinTheta; + return cross(v1, v2).z * ((theta > 0.001) ? theta * invSinTheta : 1.0); +} + +// Cheaper version of above which is good enough for diffuse +float IntegrateEdgeDiffuse(float3 v1, float3 v2) +{ + float cosTheta = dot(v1, v2); + float theta_sinTheta = 0.0; + if (cosTheta > 0.0) + { + float absCosTheta = abs(cosTheta); + theta_sinTheta = 1.5708 + (-0.879406 + 0.308609 * absCosTheta) * absCosTheta; + } + else + { + theta_sinTheta = PI * rsqrt(1.0 - cosTheta * cosTheta) - theta_sinTheta; + } + return theta_sinTheta * cross(v1, v2).z; } // Returns the unnormalized z plane intersection point between pointAboveHorizon and pointBelowHorizon. @@ -189,42 +223,90 @@ void ClipQuadToHorizon(inout float3 p[5], out int vertexCount) } } -// Takes 4 points (p) from a quad plus a 5th dummy point, rotates them into the space of the normal, then transforms -// the points by the LTC matrix provided. The points are then clipped to the normal's hemisphere. The number of points -// after the clip is returned in vertexCount. It's possible for the resulting clipped quad to be a triangle (when 3 -// points are below the horizon), or a pentagon (when one point is below the horizon), or be a regular 4 point quad. -void LtcClipAndNormalizeQuad(in float3 normal, in float3 dirToView, float3x3 ltcMat, inout float3 p[5], out int vertexCount) +// Applies the LTC matrix to the clipped points of a quad. +void ApplyLtcMatrixToQuad(in float3x3 ltcMat, inout float3 p[5], in int vertexCount) { - // Rotate ltc matrix - ltcMat = mul(ltcMat, BuildViewAlignedOrthonormalBasis(normal, dirToView)); - // Transform points into ltc space - p[0] = mul(ltcMat, p[0].xyz); - p[1] = mul(ltcMat, p[1].xyz); - p[2] = mul(ltcMat, p[2].xyz); - p[3] = mul(ltcMat, p[3].xyz); + p[0] = mul(ltcMat, p[0]); + p[1] = mul(ltcMat, p[1]); + p[2] = mul(ltcMat, p[2]); - ClipQuadToHorizon(p, vertexCount); - - // visibility check - if (vertexCount == 0) + if (vertexCount > 3) { - return; + p[3] = mul(ltcMat, p[3]); } + if (vertexCount > 4) + { + p[4] = mul(ltcMat, p[4]); + } +} - // project onto sphere +// Projects the clipped points of a quad onto the sphere. +void NormalizeQuadPoints(inout float3 p[5], in int vertexCount) +{ + // project quad points onto a sphere. p[0] = normalize(p[0]); p[1] = normalize(p[1]); p[2] = normalize(p[2]); - p[3] = normalize(p[3]); - p[4] = normalize(p[4]); + + if (vertexCount > 3) + { + p[3] = normalize(p[3]); + } + if (vertexCount > 4) + { + p[4] = normalize(p[4]); + } +} + +// Transforms the 4 points of a quad into the hemisphere of the normal +void TransformQuadToOrthonormalBasis(in float3 normal, in float3 dirToView, inout float3 p[4]) +{ + float3x3 orthoNormalBasis = BuildViewAlignedOrthonormalBasis(normal, dirToView); + + // Transform points into orthonormal space + p[0] = mul(orthoNormalBasis, p[0]); + p[1] = mul(orthoNormalBasis, p[1]); + p[2] = mul(orthoNormalBasis, p[2]); + p[3] = mul(orthoNormalBasis, p[3]); } -float IntegrateQuad(in float3 v[5], in float vertexCount, in bool doubleSided) +// Integrates the edges of a quad for lambertian diffuse contribution. +float IntegrateQuadDiffuse(in float3 v[5], in float vertexCount, in bool doubleSided) { - // Integrate float sum = 0.0; + NormalizeQuadPoints(v, vertexCount); + + // There must be at least 3 points so don't check for the first 2 edges. + sum += IntegrateEdgeDiffuse(v[0], v[1]); + sum += IntegrateEdgeDiffuse(v[1], v[2]); + + if (vertexCount > 3) + { + sum += IntegrateEdgeDiffuse(v[2], v[3]); + if (vertexCount == 5) + { + sum += IntegrateEdgeDiffuse(v[3], v[4]); + } + } + + // Close the polygon + sum += IntegrateEdgeDiffuse(v[vertexCount - 1], v[0]); + + // Note: negated due to winding order + sum = doubleSided ? abs(sum) : max(0.0, -sum); + + return sum; +} + +// Integrates the edges of a quad for specular contribution. +float IntegrateQuadSpecular(in float3 v[5], in float vertexCount, in bool doubleSided) +{ + float sum = 0.0; + + NormalizeQuadPoints(v, vertexCount); + // There must be at least 3 points so don't check for the first 2 edges. sum += IntegrateEdge(v[0], v[1]); sum += IntegrateEdge(v[1], v[2]); @@ -247,26 +329,62 @@ float IntegrateQuad(in float3 v[5], in float vertexCount, in bool doubleSided) return sum; } -float LtcQuadEvaluate(in float3 normal, in float3 dirToView, in float3x3 ltcMat, in float3 p[4], in bool doubleSided) +// Evaluate linear transform cosine lighting for a 4 point quad. +// normal - The surface normal +// dirToView - Normalized direction from the surface to the view +// ltcMat - The LTC matrix for specular, or identity for diffuse. +// p[4] - The 4 light positions relative to the surface position. +// doubleSided - If the quad emits light from both sides +// diffuse - The output diffuse response for the quad light +// specular - The output specular response for the quad light +void LtcQuadEvaluate( + in float3 normal, + in float3 dirToView, + in float3x3 ltcMat, + in float3 p[4], + in bool doubleSided, + out float diffuse, + out float specular) { + // Transform the points of the light into the space of the normal's hemisphere. + TransformQuadToOrthonormalBasis(normal, dirToView, p); + // Initialize quad with dummy point at end in case one corner is clipped (resulting in 5 sided polygon) float3 v[5] = {p[0], p[1], p[2], p[3], float3(0.0, 0.0, 0.0)}; + // Clip the light polygon to the normal hemisphere. This is done before the LTC matrix is applied to prevent + // parts of the light below the horizon from impacting the surface. The number of points remaining after + // the clip is returned in vertexCount. It's possible for the vertexCount of the resulting clipped quad to be + // 0 - all points clipped (no work to do, so return) + // 3 - 3 points clipped, leaving only a triangular corner of the quad + // 4 - 2 or 0 points clipped, leaving a quad + // 5 - 1 point clipped leaving a pentagon. + int vertexCount = 0; - LtcClipAndNormalizeQuad(normal, dirToView, ltcMat, v, vertexCount); + ClipQuadToHorizon(v, vertexCount); - if (vertexCount > 0) + if (vertexCount == 0) { - return IntegrateQuad(v, vertexCount, doubleSided); + // Entire light is below the horizon. + return; } - return 0.0; + + // IntegrateQuadDiffuse is a cheap approximation compared to specular. + diffuse = IntegrateQuadDiffuse(v, vertexCount, doubleSided); + + ApplyLtcMatrixToQuad(ltcMat, v, vertexCount); + + // IntegrateQuadSpecular uses more accurate integration to handle smooth surfaces correctly. + specular = IntegrateQuadSpecular(v, vertexCount, doubleSided); } // Checks an edge against the horizon and integrates it. -// p0 - first point -// p1 - second point -// prevClipPoint - the clip point saved from the last time an edge went from above to below the horizon -// sum - the sum total of all integrations to contribute to. +// p0 - First point +// p1 - Second point +// prevClipPoint - The clip point saved from the last time an edge went from above to below the horizon +// ltcMat - The ltc lookup matrix for specular +// diffuse - The current sum total of diffuse contribution to apply additional contribution to +// specular - The current sum total of specular contribution to apply additional contribution to // // Explanation: // When evaluating edges of a polygon there are four possible states to deal with @@ -283,28 +401,34 @@ float LtcQuadEvaluate(in float3 normal, in float3 dirToView, in float3x3 ltcMat, // 4. Both points are below the horizon // - Do nothing. -void EvaluatePolyEdge(in float3 p0, in float3 p1, inout float3 prevClipPoint, inout float sum) +void EvaluatePolyEdge(in float3 p0, in float3 p1, inout float3 prevClipPoint, in float3x3 ltcMat, inout float diffuse, inout float specular) { if (p0.z > 0.0) { if (p1.z > 0.0) { // Both above horizon - sum += IntegrateEdge(normalize(p0), normalize(p1)); + diffuse += IntegrateEdgeDiffuse(normalize(p0), normalize(p1)); + specular += IntegrateEdge(normalize(mul(ltcMat, p0)), normalize(mul(ltcMat, p1))); } else { // Going from above to below horizon - prevClipPoint = normalize(ClipEdge(p0, p1)); - sum += IntegrateEdge(normalize(p0), prevClipPoint); + prevClipPoint = ClipEdge(p0, p1); + diffuse += IntegrateEdgeDiffuse(normalize(p0), normalize(prevClipPoint)); + specular += IntegrateEdge(normalize(mul(ltcMat, p0)), normalize(mul(ltcMat, prevClipPoint))); } } else if (p1.z > 0.0) { // Going from below to above horizon - float3 clipPoint = normalize(ClipEdge(p1, p0)); - sum += IntegrateEdge(prevClipPoint, clipPoint); - sum += IntegrateEdge(clipPoint, normalize(p1)); + float3 clipPoint = ClipEdge(p1, p0); + diffuse += IntegrateEdgeDiffuse(normalize(prevClipPoint), normalize(clipPoint)); + diffuse += IntegrateEdgeDiffuse(normalize(clipPoint), normalize(p1)); + + clipPoint = mul(ltcMat, clipPoint); + specular += IntegrateEdge(normalize(mul(ltcMat, prevClipPoint)), normalize(clipPoint)); + specular += IntegrateEdge(normalize(clipPoint), normalize(mul(ltcMat, p1))); } } @@ -316,26 +440,38 @@ void EvaluatePolyEdge(in float3 p0, in float3 p1, inout float3 prevClipPoint, in // positions - The buffer where the polygon positions are // startIdx - The index of the first polygon position // endIdx - The index of the point directly after the last polygon position -// +// diffuse - The output diffuse response for the polygon light +// specular - The output specular response for the polygon light // The most complicated aspect of this function is clipping the polygon against the horizon of the surface point. See // EvaluatePolyEdge() above for details on the general concept. However, this function must deal with the case of the // first point being below the horizon. In that case, it needs to search in reverse from the end for the first point // above the horizon, and save the intersection point between the above and below points so it can be used in // EvaluatePolyEdge() later. During this search it also adjusts the end point index as necessary to avoid processing // those points that are below the horizon. -float LtcPolygonEvaluate(in float3 pos, in float3 normal, in float3 dirToView, in float3x3 ltcMat, in StructuredBuffer positions, in uint startIdx, in uint endIdx) +void LtcPolygonEvaluate( + in float3 pos, + in float3 normal, + in float3 dirToView, + in float3x3 ltcMat, + in StructuredBuffer positions, + in uint startIdx, + in uint endIdx, + out float diffuse, + out float specular +) { if (endIdx - startIdx < 3) { - return 0.0; // Must have at least 3 points to form a polygon. + return; // Must have at least 3 points to form a polygon. } // Rotate ltc matrix - ltcMat = mul(ltcMat, BuildViewAlignedOrthonormalBasis(normal, dirToView)); + float3x3 orthonormalMat = BuildViewAlignedOrthonormalBasis(normal, dirToView); // Prepare initial values - float sum = 0.0; // sum of edge integation - float3 p0 = mul(ltcMat, positions[startIdx].xyz - pos); // First point in polygon + float3 p0 = mul(orthonormalMat, positions[startIdx].xyz - pos); // First point in polygon + diffuse = 0.0; + specular = 0.0; float3 prevClipPoint = float3(0.0, 0.0, 0.0); // Used to hold previous clip point when polygon dips below horizon. float3 closePoint = p0; @@ -349,10 +485,10 @@ float LtcPolygonEvaluate(in float3 pos, in float3 normal, in float3 dirToView, i // searching backwards, updating the endIdx along the way to avoid reprocessing those points later for ( ; endIdx > startIdx + 1; --endIdx) { - float3 prevPoint = mul(ltcMat, positions[endIdx - 1].xyz - pos); - if (prevPoint.z > 0) + float3 prevPoint = mul(orthonormalMat, positions[endIdx - 1].xyz - pos); + if (prevPoint.z > 0.0) { - prevClipPoint = normalize(ClipEdge(prevPoint, p0)); + prevClipPoint = ClipEdge(prevPoint, p0); closePoint = prevClipPoint; break; } @@ -362,7 +498,7 @@ float LtcPolygonEvaluate(in float3 pos, in float3 normal, in float3 dirToView, i // Check if all points below horizon if (endIdx == startIdx + 1) { - return 0.0; + return; } p0 = firstPoint; // Restore the original p0 @@ -371,13 +507,14 @@ float LtcPolygonEvaluate(in float3 pos, in float3 normal, in float3 dirToView, i // Evaluate all the points for (uint curIdx = startIdx + 1; curIdx < endIdx; ++curIdx) { - float3 p1 = mul(ltcMat, positions[curIdx].xyz - pos); // Current point in polygon - EvaluatePolyEdge(p0, p1, prevClipPoint, sum); + float3 p1 = mul(orthonormalMat, positions[curIdx].xyz - pos); // Current point in polygon + EvaluatePolyEdge(p0, p1, prevClipPoint, ltcMat, diffuse, specular); p0 = p1; } - EvaluatePolyEdge(p0, closePoint, prevClipPoint, sum); + EvaluatePolyEdge(p0, closePoint, prevClipPoint, ltcMat, diffuse, specular); // Note: negated due to winding order - return -sum; + diffuse = -diffuse; + specular = -specular; } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PolygonLight.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PolygonLight.azsli index bbd3c1c9e3..d7313556a2 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PolygonLight.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/PolygonLight.azsli @@ -50,26 +50,25 @@ void ApplyPoylgonLight(ViewSrg::PolygonLight light, Surface surface, inout Light float radiusAttenuation = 1.0 - (falloff * falloff); radiusAttenuation = radiusAttenuation * radiusAttenuation; - // Diffuse - static const float3x3 identityMatrix = float3x3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); - float diffuse = LtcPolygonEvaluate(surface.position, surface.normal, lightingData.dirToCamera, identityMatrix, ViewSrg::m_polygonLightPoints, startIndex, endIndex); - diffuse = doubleSided ? abs(diffuse) : max(0.0, diffuse); - - // Specular float2 ltcCoords = LtcCoords(dot(surface.normal, lightingData.dirToCamera), surface.roughnessLinear); float3x3 ltcMat = LtcMatrix(SceneSrg::m_ltcMatrix, ltcCoords); - float3 specular = LtcPolygonEvaluate(surface.position, surface.normal, lightingData.dirToCamera, ltcMat, ViewSrg::m_polygonLightPoints, startIndex, endIndex); + + float diffuse = 0.0; + float specular = 0.0; + + LtcPolygonEvaluate(surface.position, surface.normal, lightingData.dirToCamera, ltcMat, ViewSrg::m_polygonLightPoints, startIndex, endIndex, diffuse, specular); + diffuse = doubleSided ? abs(diffuse) : max(0.0, diffuse); specular = doubleSided ? abs(specular) : max(0.0, specular); // Apply BRDF scale terms (BRDF magnitude and Schlick Fresnel) float2 schlick = SceneSrg::m_ltcAmplification.Sample(PassSrg::LinearSampler, ltcCoords).xy; - specular *= schlick.x + (1.0 - surface.specularF0) * schlick.y; + float3 specularRGB = specular * (schlick.x + (1.0 - surface.specularF0) * schlick.y); // Scale by inverse surface area of hemisphere (1/2pi), attenuation, and light intensity float3 intensity = 0.5 * INV_PI * radiusAttenuation * abs(light.m_rgbIntensityNits); lightingData.diffuseLighting += surface.albedo * diffuse * intensity; - lightingData.specularLighting += surface.specularF0 * specular * intensity; + lightingData.specularLighting += surface.specularF0 * specularRGB * intensity; } void ApplyPolygonLights(Surface surface, inout LightingData lightingData) diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/QuadLight.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/QuadLight.azsli index 91981e4f4d..29ee97fd6f 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/QuadLight.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/QuadLight.azsli @@ -111,24 +111,22 @@ void ApplyQuadLight(ViewSrg::QuadLight light, Surface surface, inout LightingDat { float3 p[4] = {p0, p1, p2, p3}; - // Diffuse - float3x3 identityMatrix = float3x3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); - float diffuse = LtcQuadEvaluate(surface.normal, lightingData.dirToCamera, identityMatrix, p, doubleSided); - - // Specular float2 ltcCoords = LtcCoords(dot(surface.normal, lightingData.dirToCamera), surface.roughnessLinear); float3x3 ltcMat = LtcMatrix(SceneSrg::m_ltcMatrix, ltcCoords); - float3 specular = LtcQuadEvaluate(surface.normal, lightingData.dirToCamera, ltcMat, p, doubleSided); + + float diffuse = 0.0; + float specular = 0.0; + LtcQuadEvaluate(surface.normal, lightingData.dirToCamera, ltcMat, p, doubleSided, diffuse, specular); // Apply BRDF scale terms (BRDF magnitude and Schlick Fresnel) float2 schlick = SceneSrg::m_ltcAmplification.Sample(PassSrg::LinearSampler, ltcCoords).xy; - specular *= schlick.x + (1.0 - surface.specularF0) * schlick.y; + float3 specularRGB = specular * (schlick.x + (1.0 - surface.specularF0) * schlick.y); // Scale by inverse surface area of hemisphere (1/2pi), attenuation, and light intensity float3 intensity = 0.5 * INV_PI * radiusAttenuation * light.m_rgbIntensityNits; lightingData.diffuseLighting += surface.albedo * diffuse * intensity; - lightingData.specularLighting += surface.specularF0 * specular * intensity; + lightingData.specularLighting += surface.specularF0 * specularRGB * intensity; } else {