Skip to content

Commit

Permalink
Improvements to shadowing
Browse files Browse the repository at this point in the history
- use the geometric normal to apply the shadow bias. This affects
  cascades > 0 and spot/point lights.

- use the scene's origin as a reference point for stabilizing the
  shadowmap, this is more robust.

- clamp directional shadowmap correctly to the 1-texel border, which
  needs to be reachable, as it is a valid value.

- don't snap the shadowmap to texel boundaries if stable mode is not
  active (before we only didn't do it based on lispsm). Stable mode can
  make the shadow unstable when both the camera and the scene move 
  together, so it's better to have a more predictable API where
  "stable" mode means that the snapping occurs and doesn't otherwise.

- add "far origin" distance slider to the debug ui

FIXES=[299310624]
  • Loading branch information
pixelflinger committed Sep 12, 2023
1 parent da96b45 commit 1ff0a2d
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 29 deletions.
2 changes: 2 additions & 0 deletions NEW_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ for next branch cut* header.
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).

## Release notes for next branch cut

- engine: Fixes "stable" shadows (see b/299310624)
71 changes: 47 additions & 24 deletions filament/src/ShadowMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,9 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
if (params.options.stable) {
if (viewVolumeBoundingSphere.w > 0) {
s = 1.0f / viewVolumeBoundingSphere.w;
o = mat4f::project(Mv * camera.model, viewVolumeBoundingSphere.xyz).xy;
o = mat4f::project(LMpMv * camera.model, viewVolumeBoundingSphere.xyz).xy;
} else {
Aabb const bounds = compute2DBounds(Mv,
Aabb const bounds = compute2DBounds(LMpMv,
wsClippedShadowReceiverVolume.data(), vertexCount);
if (UTILS_UNLIKELY((bounds.min.x >= bounds.max.x) || (bounds.min.y >= bounds.max.y))) {
// this could happen if the only thing visible is a perfectly horizontal or
Expand Down Expand Up @@ -379,9 +379,9 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
// adjust offset for scale
o = -s * o;

if (!useLispsm) {
// stabilize the shadowmap in all modes, except lispsm which can never be stable
snapLightFrustum(s, o, Mv, camera.worldOrigin, shadowMapInfo.shadowDimension);
if (params.options.stable) {
snapLightFrustum(s, o, LMpMv,
sceneInfo.wsShadowCastersVolume.center(), shadowMapInfo.shadowDimension);
}

const mat4f F(mat4f::row_major_init {
Expand All @@ -400,8 +400,7 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,

// Computes St the transform to use in the shader to access the shadow map texture
// i.e. it transforms a world-space vertex to a texture coordinate in the shadowmap
const backend::Viewport viewport = getViewport();
const auto [Mt, Mn] = ShadowMap::getTextureCoordsMapping(shadowMapInfo, viewport);
const auto [Mt, Mn] = ShadowMap::getTextureCoordsMapping(shadowMapInfo, getViewport());
const mat4f St = math::highPrecisionMultiply(Mt, S);

ShadowMap::ShaderParameters shaderParameters;
Expand All @@ -423,7 +422,7 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
shaderParameters.lightSpace = computeVsmLightSpaceMatrix(St, Mv, znear, zfar);
}

shaderParameters.scissorNormalized = getViewportNormalized(shadowMapInfo);
shaderParameters.scissorNormalized = getClampToEdgeCoords(shadowMapInfo);

// We apply the constant bias in world space (as opposed to light-space) to account
// for perspective and lispsm shadow maps. This also allows us to do this at zero-cost
Expand Down Expand Up @@ -456,9 +455,8 @@ ShadowMap::ShaderParameters ShadowMap::updatePunctual(
assert_invariant(shadowMapInfo.textureDimension == mOptions->mapSize);

// Final shadow transform
const backend::Viewport viewport = getViewport();
const mat4f S = math::highPrecisionMultiply(Mp, Mv);
const auto [Mt, Mn] = ShadowMap::getTextureCoordsMapping(shadowMapInfo, viewport);
const auto [Mt, Mn] = ShadowMap::getTextureCoordsMapping(shadowMapInfo, getViewport());
const mat4f St = math::highPrecisionMultiply(Mt, S);

// TODO: focus projection
Expand All @@ -485,7 +483,7 @@ ShadowMap::ShaderParameters ShadowMap::updatePunctual(
shaderParameters.lightSpace = computeVsmLightSpaceMatrix(St, Mv, nearPlane, farPlane);
}

shaderParameters.scissorNormalized = getViewportNormalized(shadowMapInfo);
shaderParameters.scissorNormalized = getClampToEdgeCoords(shadowMapInfo);

const float3 direction = -transpose(Mv)[2].xyz;
const float constantBias = shadowMapInfo.vsm ? 0.0f : params.options.constantBias;
Expand Down Expand Up @@ -848,9 +846,9 @@ void ShadowMap::computeFrustumCorners(float3* UTILS_RESTRICT out,
}

void ShadowMap::snapLightFrustum(float2& s, float2& o,
mat4f const& Mv, mat4 worldOrigin, int2 resolution) noexcept {
mat4f const& Mv, double3 wsSnapCoords, int2 resolution) noexcept {

auto proj = [](mat4 m, double4 v) -> double3 {
auto proj = [](mat4 m, double3 v) -> double3 {
// for directional light p.w == 1, exactly
auto p = m * v;
assert_invariant(p.w == 1.0);
Expand All @@ -876,7 +874,7 @@ void ShadowMap::snapLightFrustum(float2& s, float2& o,
mat4 const FMv{ F * Mv };

// This offsets the texture coordinates, so it has a fixed offset w.r.t the world
double2 const lsOrigin = proj(FMv, worldOrigin[3]).xy;
double2 const lsOrigin = proj(FMv, wsSnapCoords).xy;
double2 const d = (fract(lsOrigin * resolution * 0.5) * 2.0) / resolution;

// adjust offset
Expand Down Expand Up @@ -1258,33 +1256,58 @@ void ShadowMap::updateSceneInfoSpot(mat4f const& Mv, FScene const& scene,
}

backend::Viewport ShadowMap::getViewport() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the
// texture. This can only happen for the directional light when "focus shadow casters is used".
// We set a viewport with a 1-texel border for when we index outside the texture.
// This happens only for directional lights when "focus shadow casters" is used,
// or when shadowFar is smaller than the camera far.
// For spot- and point-lights we also use a 1-texel border, so that bilinear filtering
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp).
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
return { border, border, dim - 2u * border, dim - 2u * border };
}

backend::Viewport ShadowMap::getScissor() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the
// texture. This can only happen for the directional light when "focus shadow casters is used".
// We set a viewport with a 1-texel border for when we index outside the texture.
// This happens only for directional lights when "focus shadow casters" is used,
// or when shadowFar is smaller than the camera far.
// For spot- and point-lights we also use a 1-texel border, so that bilinear filtering
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp), so we
// don't scissor the border, so it gets filled with correct neighboring texels.
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;

switch (mShadowType) {
case ShadowType::DIRECTIONAL:
return { border, border, dim - 2u * border, dim - 2u * border };
case ShadowType::SPOT:
case ShadowType::POINT:
default:
return { 0, 0, dim, dim };
}
}

math::float4 ShadowMap::getViewportNormalized(ShadowMapInfo const& shadowMapInfo) const noexcept {
const auto [l, b, w, h] = getViewport();
const float texel = 1.0f / float(shadowMapInfo.atlasDimension);
const float4 v = float4{ l, b, l + w, b + h } * texel;
math::float4 ShadowMap::getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept {
float border; // shadowmap border in texels
switch (mShadowType) {
case ShadowType::DIRECTIONAL:
// For directional lights, we need to allow the sampling to reach the border, it
// happens when "focus shadow casters" is used for instance.
border = 0.5f;
break;
case ShadowType::SPOT:
case ShadowType::POINT:
// For spot and point light, this is equal to the viewport. i.e. the valid
// texels are inside the viewport (w/ 1-texel border), the border will be used
// for bilinear filtering.
border = 1.0f;
break;
}

float const texel = 1.0f / float(shadowMapInfo.atlasDimension);
float const dim = float(mOptions->mapSize);
float const l = border;
float const b = border;
float const w = dim - 2.0f * border;
float const h = dim - 2.0f * border;
float4 const v = float4{ l, b, l + w, b + h } * texel;
if (shadowMapInfo.textureSpaceFlipped) {
// this is equivalent to calling uvToRenderTargetUV() in the shader *after* clamping
// texture coordinates to this normalized viewport.
Expand Down
4 changes: 2 additions & 2 deletions filament/src/ShadowMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class ShadowMap {
const math::float3& dir);

static inline void snapLightFrustum(math::float2& s, math::float2& o,
math::mat4f const& Mv, math::mat4 worldOrigin, math::int2 resolution) noexcept;
math::mat4f const& Mv, math::double3 wsSnapCoords, math::int2 resolution) noexcept;

static inline void computeFrustumCorners(math::float3* out,
const math::mat4f& projectionViewInverse, math::float2 csNearFar = { -1.0f, 1.0f }) noexcept;
Expand Down Expand Up @@ -281,7 +281,7 @@ class ShadowMap {
static math::mat4f computeVsmLightSpaceMatrix(const math::mat4f& lightSpacePcf,
const math::mat4f& Mv, float znear, float zfar) noexcept;

math::float4 getViewportNormalized(ShadowMapInfo const& shadowMapInfo) const noexcept;
math::float4 getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept;

float texelSizeWorldSpace(const math::mat3f& worldToShadowTexture,
uint16_t shadowDimension) const noexcept;
Expand Down
4 changes: 3 additions & 1 deletion samples/gltf_viewer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ struct App {

bool actualSize = false;
bool originIsFarAway = false;
float originDistance = 6378137; // Earth's radius in [m]

struct Scene {
Entity groundPlane;
Expand Down Expand Up @@ -759,6 +760,7 @@ int main(int argc, char** argv) {
ImGui::Checkbox("Disable buffer padding", debug.getPropertyAddress<bool>("d.renderer.disable_buffer_padding"));
ImGui::Checkbox("Camera at origin", debug.getPropertyAddress<bool>("d.view.camera_at_origin"));
ImGui::Checkbox("Far Origin", &app.originIsFarAway);
ImGui::SliderFloat("Origin", &app.originDistance, 0, 10000000);
auto dataSource = debug.getDataSource("d.view.frame_info");
if (dataSource.data) {
ImGuiExt::PlotLinesSeries("FrameInfo", 6,
Expand Down Expand Up @@ -956,7 +958,7 @@ int main(int argc, char** argv) {
tcm.setParent(tcm.getInstance(camera.getEntity()), root);
tcm.setParent(tcm.getInstance(app.asset->getRoot()), root);
tcm.setParent(tcm.getInstance(view->getFogEntity()), root);
tcm.setTransform(root, mat4f::translation(float3{ app.originIsFarAway ? 1e6f : 0.0f }));
tcm.setTransform(root, mat4f::translation(float3{ app.originIsFarAway ? app.originDistance : 0.0f }));

// Check if color grading has changed.
ColorGradingSettings& options = app.viewer->getSettings().view.colorGrading;
Expand Down
4 changes: 2 additions & 2 deletions shaders/src/getters.fs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ highp vec4 getSpotLightSpacePosition(int index, highp vec3 dir, highp float zLig
// for spotlights, the bias depends on z
float bias = shadowUniforms.shadows[index].normalBias * zLight;

return computeLightSpacePosition(getWorldPosition(), getWorldNormalVector(),
return computeLightSpacePosition(getWorldPosition(), getWorldGeometricNormalVector(),
dir, bias, lightFromWorldMatrix);
}
#endif
Expand Down Expand Up @@ -141,7 +141,7 @@ highp vec4 getCascadeLightSpacePosition(int cascade) {
return vertex_lightSpacePosition;
}

return computeLightSpacePosition(getWorldPosition(), getWorldNormalVector(),
return computeLightSpacePosition(getWorldPosition(), getWorldGeometricNormalVector(),
frameUniforms.lightDirection,
shadowUniforms.shadows[cascade].normalBias,
shadowUniforms.shadows[cascade].lightFromWorldMatrix);
Expand Down

0 comments on commit 1ff0a2d

Please sign in to comment.