diff --git a/NEW_RELEASE_NOTES.md b/NEW_RELEASE_NOTES.md index f408ba3462..23fb0a7e4a 100644 --- a/NEW_RELEASE_NOTES.md +++ b/NEW_RELEASE_NOTES.md @@ -6,3 +6,6 @@ appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md). ## Release notes for next branch cut + +- engine: fix crash when using variance shadow maps +- materials: better shadow normal-bias calculations [⚠️ **New Material Version**] diff --git a/filament/src/ShadowMap.cpp b/filament/src/ShadowMap.cpp index 065fe23fa0..eb81b62eee 100644 --- a/filament/src/ShadowMap.cpp +++ b/filament/src/ShadowMap.cpp @@ -1117,16 +1117,16 @@ bool ShadowMap::intersectSegmentWithPlanarQuad(float3& UTILS_RESTRICT p, return hit; } -float ShadowMap::texelSizeWorldSpace(const mat3f& clipFromWorld, uint16_t shadowDimension) noexcept { +float2 ShadowMap::texelSizeWorldSpace(const mat3f& clipFromWorld, uint16_t shadowDimension) noexcept { // The Jacobian of the transformation from texture-to-world is the matrix itself for // orthographic projections. We just need to inverse shadowMapFromWorld, // which is guaranteed to be orthographic. // The two first columns give us how a texel maps in world-space. float const oneTexel = 2.0f / float(shadowDimension); - const mat3f worldFromClip(inverse(clipFromWorld)); - const float3 Jx = worldFromClip[0]; - const float3 Jy = worldFromClip[1]; - const float s = std::max(length(Jx), length(Jy)) * oneTexel; + mat3f const worldFromClip(inverse(clipFromWorld)); + float3 const Jx = worldFromClip[0]; + float3 const Jy = worldFromClip[1]; + float2 const s = float2{ length(Jx), length(Jy) } * oneTexel; return s; } @@ -1171,13 +1171,13 @@ static mat3f jacobian(mat4f const& M, float3 const& p) noexcept { return (M_sub - t_cross_w / T.w) / T.w; } -float ShadowMap::texelSizeWorldSpace(mat4f const& S, uint16_t const shadowDimension) noexcept { +float2 ShadowMap::texelSizeWorldSpace(mat4f const& S, uint16_t const shadowDimension) noexcept { // The Jacobian is not constant, so we evaluate it in the center of the shadow-map texture. // It might be better to do this computation in the vertex shader. float3 const p = { 0.0f, 0.0f, 0.0f }; // clip-space float const oneTexel = 2.0f / float(shadowDimension); mat3f const J = jacobian(inverse(S), p); - const float s = std::max(length(J[0]), length(J[1])) * oneTexel; + float2 const s = float2{ length(J[0]), length(J[1]) } * oneTexel; return s; } diff --git a/filament/src/ShadowMap.h b/filament/src/ShadowMap.h index 2a57e076eb..1b9fa5f68c 100644 --- a/filament/src/ShadowMap.h +++ b/filament/src/ShadowMap.h @@ -135,7 +135,7 @@ public: math::mat4f lightSpace{}; math::float4 lightFromWorldZ{}; math::float4 scissorNormalized{}; - float texelSizeAtOneMeterWs{}; + math::float2 texelSizeAtOneMeterWs{}; }; // Call once per frame if the light, scene (or visible layers) or camera changes. @@ -319,8 +319,8 @@ private: math::float4 getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept; - static float texelSizeWorldSpace(const math::mat3f& clipFromWorld, uint16_t shadowDimension) noexcept; - static float texelSizeWorldSpace(const math::mat4f& clipFromWorld, uint16_t shadowDimension) noexcept; + static math::float2 texelSizeWorldSpace(const math::mat3f& clipFromWorld, uint16_t shadowDimension) noexcept; + static math::float2 texelSizeWorldSpace(const math::mat4f& clipFromWorld, uint16_t shadowDimension) noexcept; static constexpr Segment sBoxSegments[12] = { { 0, 1 }, { 1, 3 }, { 3, 2 }, { 2, 0 }, diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index f50fd6aed4..45cb9cf4f4 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -742,17 +742,16 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng // Texel size is constant for directional light (although that's not true when LISPSM // is used, but in that case we're pretending it is). - const float wsTexelSize = shaderParameters.texelSizeAtOneMeterWs; + float2 const wsTexelSize = shaderParameters.texelSizeAtOneMeterWs; auto& s = mShadowUb.edit(); s.shadows[shadowIndex].layer = shadowMap.getLayer(); s.shadows[shadowIndex].lightFromWorldMatrix = shaderParameters.lightSpace; s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized; - s.shadows[shadowIndex].normalBias = normalBias * wsTexelSize; - s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSize; + s.shadows[shadowIndex].normalBias = wsTexelSize * normalBias; s.shadows[shadowIndex].elvsm = options.vsm.elvsm; s.shadows[shadowIndex].bulbRadiusLs = - mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / wsTexelSize; + mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSize); shadowTechnique |= ShadowTechnique::SHADOW_MAP; cascadeHasVisibleShadows |= 0x1u << i; @@ -833,7 +832,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin // and if we need to generate it, update all the UBO data if (shadowMap.hasVisibleShadows()) { const size_t shadowIndex = shadowMap.getShadowIndex(); - const float wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs; + const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs; // note: normalBias is set to zero for VSM const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias; @@ -845,12 +844,11 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized; s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter; s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ; - s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSizeAtOneMeter; s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n)); s.shadows[shadowIndex].elvsm = options->vsm.elvsm; s.shadows[shadowIndex].bulbRadiusLs = mSoftShadowOptions.penumbraScale * options->shadowBulbRadius - / wsTexelSizeAtOneMeter; + / length(wsTexelSizeAtOneMeter); } } @@ -926,7 +924,7 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap, // and if we need to generate it, update all the UBO data if (shadowMap.hasVisibleShadows()) { const size_t shadowIndex = shadowMap.getShadowIndex(); - const float wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs; + const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs; // note: normalBias is set to zero for VSM const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias; @@ -938,12 +936,11 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap, s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized; s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter; s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ; - s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSizeAtOneMeter; s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n)); s.shadows[shadowIndex].elvsm = options->vsm.elvsm; s.shadows[shadowIndex].bulbRadiusLs = mSoftShadowOptions.penumbraScale * options->shadowBulbRadius - / wsTexelSizeAtOneMeter; + / length(wsTexelSizeAtOneMeter); } } diff --git a/libs/filabridge/include/private/filament/UibStructs.h b/libs/filabridge/include/private/filament/UibStructs.h index 286b7d6490..ed664a24e3 100644 --- a/libs/filabridge/include/private/filament/UibStructs.h +++ b/libs/filabridge/include/private/filament/UibStructs.h @@ -299,10 +299,9 @@ struct ShadowUib { // NOLINT(cppcoreguidelines-pro-type-member-init) math::mat4f lightFromWorldMatrix; // 64 math::float4 lightFromWorldZ; // 16 math::float4 scissorNormalized; // 16 - float texelSizeAtOneMeter; // 4 float bulbRadiusLs; // 4 float nearOverFarMinusNear; // 4 - float normalBias; // 4 + math::float2 normalBias; // 4 bool elvsm; // 4 uint32_t layer; // 4 uint32_t reserved1; // 4 diff --git a/shaders/src/surface_getters.fs b/shaders/src/surface_getters.fs index a6254764eb..bf455bf885 100644 --- a/shaders/src/surface_getters.fs +++ b/shaders/src/surface_getters.fs @@ -111,7 +111,7 @@ highp vec4 getSpotLightSpacePosition(int index, highp vec3 dir, highp float zLig highp mat4 lightFromWorldMatrix = shadowUniforms.shadows[index].lightFromWorldMatrix; // for spotlights, the bias depends on z - float bias = shadowUniforms.shadows[index].normalBias * zLight; + highp vec2 bias = shadowUniforms.shadows[index].normalBias * zLight; return computeLightSpacePosition(getWorldPosition(), getWorldGeometricNormalVector(), dir, bias, lightFromWorldMatrix); diff --git a/shaders/src/surface_shadowing.glsl b/shaders/src/surface_shadowing.glsl index 66ee6320e9..ee355516d4 100644 --- a/shaders/src/surface_shadowing.glsl +++ b/shaders/src/surface_shadowing.glsl @@ -12,12 +12,51 @@ */ highp vec4 computeLightSpacePosition(highp vec3 p, const highp vec3 n, - const highp vec3 dir, const float b, highp_mat4 lightFromWorldMatrix) { + const highp vec3 dir, const highp vec2 b, highp_mat4 lightFromWorldMatrix) { #if !defined(VARIANT_HAS_VSM) - highp float cosTheta = saturate(dot(n, dir)); - highp float sinTheta = sqrt(1.0 - cosTheta * cosTheta); - p += n * (sinTheta * b); + // -------------------------------------------------------------------------------------- + // Anisotropic Normal Bias for Shadow Mapping + // -------------------------------------------------------------------------------------- + // To prevent shadow acne, we must push the geometry along its normal to clear the + // quantization steps of the shadow map's discrete depth grid. The exact physical depth + // error we must clear is proportional to the shadow texel's world-space dimensions. + // + // This implementation computes the exact geometric projection of the rectangular + // shadow map texel onto the surface normal. + // + // 1. Coordinate Space Transition: + // We project the world-space normal onto the light's X and Y basis vectors (L_right, + // L_up). This gives us the lateral components of the normal in Light Space (n_Lx, n_Ly). + // + // 2. The Implicit sin(theta) Slope Scale: + // Because the normal is a unit vector, the magnitude of its lateral components in + // light space inherently equals sin(theta), where theta is the angle of incidence. + // This perfectly and automatically scales the bias from 0.0 (top-down, flat surface) + // to maximum (grazing angle). + // + // 3. Exact Anisotropic Footprint (The L1 Norm): + // Shadow texels are rarely perfectly square due to Cascaded Shadow Maps (CSM) or + // Light Space Perspective Shadow Maps (LiSPSM). Jx and Jy are the physical world-space + // dimensions of the texel. + // By evaluating `abs(n_Lx * Jx) + abs(n_Ly * Jy)`, we compute the exact scalar + // projection of the rectangular texel footprint. + // - It is superior to `max(Jx, Jy)` which assumes a massive square and causes Peter Panning. + // - It is superior to `length()` which assumes an ellipse and under-biases the corners. + // -------------------------------------------------------------------------------------- + + // Extract the first row (Light's Right vector in World Space) + highp vec3 L_right = vec3(lightFromWorldMatrix[0][0], lightFromWorldMatrix[1][0], lightFromWorldMatrix[2][0]); + + // Extract the second row (Light's Up vector in World Space) + highp vec3 L_up = vec3(lightFromWorldMatrix[0][1], lightFromWorldMatrix[1][1], lightFromWorldMatrix[2][1]); + + // Project the world normal onto the shadow map's 2D grid + highp float n_Lx = dot(n, L_right); + highp float n_Ly = dot(n, L_up); + + // Apply the anisotropic normal bias + p += n * (abs(n_Lx * b.x) + abs(n_Ly * b.y)); #endif return mulMat4x4Float3(lightFromWorldMatrix, p); diff --git a/shaders/src/surface_types.glsl b/shaders/src/surface_types.glsl index d2e01657bf..f6973d8544 100644 --- a/shaders/src/surface_types.glsl +++ b/shaders/src/surface_types.glsl @@ -5,10 +5,9 @@ struct ShadowData { highp mat4 lightFromWorldMatrix; highp vec4 lightFromWorldZ; highp vec4 scissorNormalized; - mediump float texelSizeAtOneMeter; mediump float bulbRadiusLs; mediump float nearOverFarMinusNear; - mediump float normalBias; + highp vec2 normalBias; bool elvsm; mediump uint layer; mediump uint reserved1;