Skip to content

Commit

Permalink
Revert "fix a couple shadow stability bugs"
Browse files Browse the repository at this point in the history
This reverts commit 1b0db0f.
  • Loading branch information
bejado committed Nov 8, 2023
1 parent 1357186 commit 763950e
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 282 deletions.
1 change: 0 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ Instead, if you are authoring a PR for the main branch, add your release note to
- engine: New tone mapper: `AgXTonemapper`.
- matinfo: Add support for viewing ESSL 1.0 shaders
- engine: Add `Renderer::getClearOptions()` [b/243846268]
- engine: Fix stable shadows (again) when an IBL rotation is used

## v1.45.0

Expand Down
2 changes: 1 addition & 1 deletion filament/src/PerShadowMapUniforms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void PerShadowMapUniforms::prepareCamera(Transaction const& transaction,
s.viewFromClipMatrix = viewFromClip; // 1/projection
s.clipFromWorldMatrix[0] = clipFromWorld; // projection * view
s.worldFromClipMatrix = worldFromClip; // 1/(projection * view)
s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldTransform));
s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldOrigin));
s.clipTransform = camera.clipTransform;
s.cameraFar = camera.zf;
s.oneOverFarMinusNear = 1.0f / (camera.zf - camera.zn);
Expand Down
4 changes: 2 additions & 2 deletions filament/src/PerViewUniforms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ void PerViewUniforms::prepareCamera(FEngine& engine, const CameraInfo& camera) n
s.clipFromViewMatrix = clipFromView; // projection
s.viewFromClipMatrix = viewFromClip; // 1/projection
s.worldFromClipMatrix = worldFromClip; // 1/(projection * view)
s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldTransform));
s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldOrigin));
s.clipTransform = camera.clipTransform;
s.cameraFar = camera.zf;
s.oneOverFarMinusNear = 1.0f / (camera.zf - camera.zn);
Expand Down Expand Up @@ -151,7 +151,7 @@ void PerViewUniforms::prepareFog(FEngine& engine, const CameraInfo& cameraInfo,
// why we store the cofactor matrix.

mat4f const viewFromWorld = cameraInfo.view;
mat4 const worldFromUserWorld = cameraInfo.worldTransform;
mat4 const worldFromUserWorld = cameraInfo.worldOrigin;
mat4 const worldFromFog = worldFromUserWorld * userWorldFromFog;
mat4 const viewFromFog = viewFromWorld * worldFromFog;

Expand Down
187 changes: 113 additions & 74 deletions filament/src/ShadowMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,20 @@ void ShadowMap::initialize(size_t lightIndex, ShadowType shadowType,
mFace = face;
}

math::mat4f ShadowMap::getDirectionalLightViewMatrix(math::float3 direction, math::float3 up,
math::float3 position) noexcept {
// 1. we use the x-axis as the "up" reference so that the math is stable when the light
// is pointing down, which is a common case for lights.
// 2. we do the math in double to avoid some precision issues when the light is almost
// straight (i.e. parallel to the x-axis)
mat4f const Mm = mat4f{ mat4::lookTo(direction, position, up) };
mat4f ShadowMap::getDirectionalLightViewMatrix(float3 direction, float3 position) noexcept {
auto z_axis = direction;
auto norm_up = float3{ 0, 1, 0 };
if (UTILS_UNLIKELY(std::abs(dot(z_axis, norm_up)) > 0.999f)) {
// Fix up vector if we're degenerate (looking straight up, basically)
norm_up = { norm_up.z, norm_up.x, norm_up.y };
}
auto x_axis = normalize(cross(z_axis, norm_up));
auto y_axis = cross(x_axis, z_axis);
const mat4f Mm{
float4{ x_axis, 0 },
float4{ y_axis, 0 },
float4{ -z_axis, 0 },
float4{ position, 1 }};
return FCamera::rigidTransformInverse(Mm);
}

Expand All @@ -98,7 +105,7 @@ math::mat4f ShadowMap::getPointLightViewMatrix(backend::TextureCubemapFace face,
case TextureCubemapFace::POSITIVE_Z: direction = { 0, 0, 1 }; break;
case TextureCubemapFace::NEGATIVE_Z: direction = { 0, 0, -1 }; break;
}
const mat4f Mv = getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position);
const mat4f Mv = getDirectionalLightViewMatrix(direction, position);
return Mv;
}

Expand All @@ -113,7 +120,7 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
FLightManager::ShadowParams const params = lcm.getShadowParams(li);

// We can't use LISPSM in stable mode
const auto direction = lightData.elementAt<FScene::SHADOW_DIRECTION>(index);
const auto direction = params.options.transform * lightData.elementAt<FScene::DIRECTION>(index);

auto [Mv, znear, zfar, lsClippedShadowVolume, vertexCount, visibleShadows] =
computeDirectionalShadowBounds(engine, direction, params, camera, sceneInfo);
Expand Down Expand Up @@ -159,22 +166,11 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
// This is the most important step to increase the quality of the shadow map.
//
// In LiPSM mode, we're using the warped space here.
float4 f = computeFocusParams(LMpMv, WLMp,
const mat4f F = computeFocusMatrix(LMpMv, WLMp,
sceneInfo.wsShadowReceiversVolume,
lsClippedShadowVolume, vertexCount,
camera, sceneInfo.csNearFar,
params.options.shadowFar, params.options.stable);

if (params.options.stable) {
const auto lsRef = lightData.elementAt<FScene::SHADOW_REF>(index);
snapLightFrustum(f.xy, f.zw, lsRef, shadowMapInfo.shadowDimension);
}

const mat4f F(mat4f::row_major_init {
f.x, 0.0f, 0.0f, f.z,
0.0f, f.y, 0.0f, f.w,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f,
});
shadowMapInfo.shadowDimension, params.options.stable);

/*
* Final shadow map transform
Expand Down Expand Up @@ -226,7 +222,7 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine,
mCamera->setCustomProjection(mat4(Mn * F * WLMp), znear, zfar);

// for the debug camera, we need to undo the world origin
mDebugCamera->setCustomProjection(mat4(S * b * camera.worldTransform), znear, zfar);
mDebugCamera->setCustomProjection(mat4(S * b * camera.worldOrigin), znear, zfar);

mHasVisibleShadows = true;

Expand Down Expand Up @@ -301,7 +297,7 @@ ShadowMap::ShaderParameters ShadowMap::updateSpot(FEngine& engine,
auto radius = lightData.elementAt<FScene::POSITION_RADIUS>(index).w;
auto li = lightData.elementAt<FScene::LIGHT_INSTANCE>(index);
const FLightManager::ShadowParams& params = lcm.getShadowParams(li);
const mat4f Mv = getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position);
const mat4f Mv = getDirectionalLightViewMatrix(direction, position);

// We only keep this for reference. updateSceneInfoSpot() is quite expensive on large scenes
// currently, and only needed to find a near/far. Instead, we just use a small near and the
Expand Down Expand Up @@ -405,8 +401,7 @@ ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds(
// We compute the directional light's model matrix using the origin's as the light position.
// The choice of the light's origin initially doesn't matter for a directional light.
// This will be adjusted later because of how we compute the depth metric for VSM.
mat4f const MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction,
normalize(camera.worldTransform[0].xyz));
mat4f const MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction);


Aabb lsLightFrustumBounds = computeLightFrustumBounds(
Expand Down Expand Up @@ -460,6 +455,10 @@ ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds(
std::max(lsLightFrustumBounds.min.z, sceneInfo.lsCastersNearFar[1]);
}

// Now that we know the znear (-lsLightFrustumBounds.max.z), adjust the light's position such
// that znear = 0, this is only needed for VSM, but doesn't hurt PCF.
const mat4f Mv = getDirectionalLightViewMatrix(direction, direction * -lsLightFrustumBounds.max.z);

// near / far planes are specified relative to the direction the eye is looking at
// i.e. the -z axis (see: ortho)
const float znear = 0.0f;
Expand All @@ -474,11 +473,6 @@ ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds(
v.z -= lsLightFrustumBounds.max.z;
}

// Now that we know the znear (-lsLightFrustumBounds.max.z), adjust the light's position such
// that znear = 0, this is only needed for VSM, but doesn't hurt PCF.
const mat4f Mv = getDirectionalLightViewMatrix(direction, normalize(camera.worldTransform[0].xyz),
direction * -lsLightFrustumBounds.max.z);

return { Mv, znear, zfar, lsClippedShadowVolume, vertexCount, true };
}

Expand Down Expand Up @@ -583,48 +577,95 @@ math::mat4f ShadowMap::computeLightRotation(math::float3 const& lsDirection) noe
return L;
}

math::float4 ShadowMap::computeFocusParams(
mat4f const& LMpMv,
mat4f const& WLMp,
math::mat4f ShadowMap::computeFocusMatrix(
const mat4f& LMpMv, const mat4f& WLMp,
Aabb const& wsShadowReceiversVolume,
FrustumBoxIntersection const& lsShadowVolume, size_t vertexCount,
filament::CameraInfo const& camera, float2 const& csNearFar,
float shadowFar, bool stable) noexcept {
uint16_t shadowDimension, bool stable) noexcept {

float2 s, o;
float4 wsViewVolumeBoundingSphere = {};

if (stable) {
// In stable mode, the light frustum size must be fixed, so we choose the
// whole view frustum.
// We simply take the view volume bounding sphere, but we calculate it
// in view space, so that it's perfectly stable.
// In stable mode, the light frustum size must be fixed, so we can choose either the
// whole view frustum, or the whole scene bounding volume. We simply pick whichever
// is smaller.

auto getViewVolumeBoundingSphere = [&]() {
if (shadowFar > 0) {
float4 const wsViewVolumeBoundingSphere = { camera.getPosition(), shadowFar };
return wsViewVolumeBoundingSphere;
} else {
mat4f const viewFromClip = inverse(camera.cullingProjection);
Corners const wsFrustumVertices = computeFrustumCorners(viewFromClip, csNearFar);
float4 const wsViewVolumeBoundingSphere =
computeBoundingSphere(wsFrustumVertices.vertices, 8);
return wsViewVolumeBoundingSphere;
}
};
// in stable mode we simply take the shadow receivers volume
const float4 shadowReceiverVolumeBoundingSphere = computeBoundingSphere(
wsShadowReceiversVolume.getCorners().data(), 8);

float4 const wsViewVolumeBoundingSphere = getViewVolumeBoundingSphere();
s = 1.0f / wsViewVolumeBoundingSphere.w;
o = mat4f::project(LMpMv * camera.model, wsViewVolumeBoundingSphere.xyz).xy;
o = -s * o;
// in stable mode we simply take the view volume bounding sphere, but we calculate it
// in view space, so that it's perfectly stable.
mat4f const viewFromClip = inverse(camera.cullingProjection);
Corners const wsFrustumVertices = computeFrustumCorners(viewFromClip, csNearFar);
wsViewVolumeBoundingSphere = computeBoundingSphere(wsFrustumVertices.vertices, 8);

if (shadowReceiverVolumeBoundingSphere.w < wsViewVolumeBoundingSphere.w) {
// When using the shadowReceiver volume, we don't have to use its enclosing sphere
// because (we assume) the scene volume doesn't change. Seen from the light it only
// changes when the light moves or rotates, and it is acceptable in that case to have
// non "stable" shadows (the shadow will never be stable when the light moves).
//
// On the other hand, when using the view volume, we must use a sphere because otherwise
// its projection's bounds in light space change with the camera, leading to unstable
// shadows with camera movement.

wsViewVolumeBoundingSphere.w = 0;
}

if (wsViewVolumeBoundingSphere.w > 0) {
s = 1.0f / wsViewVolumeBoundingSphere.w;
o = mat4f::project(LMpMv * camera.model, wsViewVolumeBoundingSphere.xyz).xy;
} else {
// TODO: another options is the sphere around the intersections of receiver & casters
// FIXME: this is not stable with the global rotation because wsShadowReceiversVolume
// is not stable with it.
Aabb const bounds = compute2DBounds(LMpMv,
wsShadowReceiversVolume.getCorners().data(),
wsShadowReceiversVolume.getCorners().size());
assert_invariant(bounds.min.x < bounds.max.x);
assert_invariant(bounds.min.y < bounds.max.y);

s = 2.0f / float2(bounds.max.xy - bounds.min.xy);
o = float2(bounds.max.xy + bounds.min.xy) * 0.5f;

// Quantize the scale in world-space units. This value can be very small because
// if it wasn't for floating-point imprecision, the scale would be a constant.
double2 const quantizer = 0.0625;
s = 1.0 / (ceil(1.0 / (s * quantizer)) * quantizer);
}
} else {
Aabb const bounds = compute2DBounds(WLMp, lsShadowVolume.data(), vertexCount);
assert_invariant(bounds.min.x < bounds.max.x);
assert_invariant(bounds.min.y < bounds.max.y);

s = 2.0f / float2(bounds.max.xy - bounds.min.xy);
o = float2(bounds.max.xy + bounds.min.xy) * 0.5f;
o = -s * o;

// TODO: we could quantize `s` here to give some stability when lispsm is disabled,
// however, the quantization paramater should probably be user settable.
}

// adjust offset for scale
o = -s * o;

if (stable) {
snapLightFrustum(s, o, LMpMv, wsShadowReceiversVolume.center(), shadowDimension);
}
return { s, o };

const mat4f F(mat4f::row_major_init {
s.x, 0.0f, 0.0f, o.x,
0.0f, s.y, 0.0f, o.y,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f,
});

return F;
}


// Apply these remapping in double to maintain a high precision for the depth axis
ShadowMap::TextureCoordsMapping ShadowMap::getTextureCoordsMapping(ShadowMapInfo const& info,
backend::Viewport const& viewport) noexcept {
Expand All @@ -638,8 +679,6 @@ ShadowMap::TextureCoordsMapping ShadowMap::getTextureCoordsMapping(ShadowMapInfo
0.0f, 0.0f, 0.0f, 1.0f
}};

constexpr mat4f MtInverse = inverse(Mt);

// apply the viewport transform
const float2 o = float2{ viewport.left, viewport.bottom } / float(info.atlasDimension);
const float2 s = float2{ viewport.width, viewport.height } / float(info.atlasDimension);
Expand All @@ -661,7 +700,7 @@ ShadowMap::TextureCoordsMapping ShadowMap::getTextureCoordsMapping(ShadowMapInfo
}} : mat4f{};

// Compute shadow-map texture access and viewport transform
return { Mf * (Mv * Mt), MtInverse * (Mv * Mt) };
return { Mf * (Mv * Mt), inverse(Mt) * (Mv * Mt) };
}

mat4f ShadowMap::computeVsmLightSpaceMatrix(const mat4f& lightSpacePcf,
Expand Down Expand Up @@ -802,8 +841,8 @@ ShadowMap::Corners ShadowMap::computeFrustumCorners(

Aabb ShadowMap::computeLightFrustumBounds(mat4f const& lightView,
Aabb const& wsShadowReceiversVolume, Aabb const& wsShadowCastersVolume,
ShadowMap::SceneInfo const& sceneInfo,
bool stable, bool focusShadowCasters, bool farUsesShadowCasters) noexcept {
ShadowMap::SceneInfo const& sceneInfo, bool stable, bool focusShadowCasters,
bool farUsesShadowCasters) noexcept {
Aabb lsLightFrustumBounds{};

float const receiversFar = sceneInfo.lsReceiversNearFar[1];
Expand Down Expand Up @@ -834,13 +873,13 @@ Aabb ShadowMap::computeLightFrustumBounds(mat4f const& lightView,
}

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

auto proj2 = [](mat4 m, double2 v) -> double2 {
double2 p;
p.x = dot(double2{ m[0].x, m[1].x }, v) + m[3].x;
p.y = dot(double2{ m[0].y, m[1].y }, v) + m[3].y;
return p;
auto proj = [](mat4 m, double3 v) -> double3 {
// for directional light p.w == 1, exactly
auto p = m * v;
assert_invariant(p.w == 1.0);
return p.xyz;
};

auto fract = [](auto v) {
Expand All @@ -857,13 +896,13 @@ void ShadowMap::snapLightFrustum(float2& s, float2& o,
});

// The (resolution * 0.5) comes from Mv having a NDC in the range -1,1 (so a range of 2).
// Another (resolution * 0.5) is there to snap on even texels, which helps with debugging

// This offsets the texture coordinates, so it has a fixed offset w.r.t the world
// F * Mv * ref
// focused light-space
mat4 const FMv{ F * Mv };

double2 const lsFocusedOrigin = proj2(F, lsRef);
double2 const d = fract(lsFocusedOrigin * (resolution * 0.25)) / (resolution * 0.25);
// This offsets the texture coordinates, so it has a fixed offset w.r.t the world
double2 const lsOrigin = proj(FMv, wsSnapCoords).xy;
double2 const d = (fract(lsOrigin * resolution * 0.5) * 2.0) / resolution;

// adjust offset
o -= d;
Expand Down
15 changes: 8 additions & 7 deletions filament/src/ShadowMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ class ShadowMap {
uint8_t visibleLayers;
};

static math::mat4f getDirectionalLightViewMatrix(math::float3 direction, math::float3 up,
math::float3 position = {}) noexcept;
static math::mat4f getDirectionalLightViewMatrix(
math::float3 direction, math::float3 position = {}) noexcept;

static math::mat4f getPointLightViewMatrix(backend::TextureCubemapFace face,
math::float3 position) noexcept;
Expand Down Expand Up @@ -246,15 +246,16 @@ class ShadowMap {

static inline math::mat4f computeLightRotation(math::float3 const& lsDirection) noexcept;

static inline math::float4 computeFocusParams(
math::mat4f const& LMpMv,
math::mat4f const& WLMp,
static inline math::mat4f computeFocusMatrix(
const math::mat4f& LMpMv,
const math::mat4f& WLMp,
Aabb const& wsShadowReceiversVolume,
FrustumBoxIntersection const& lsShadowVolume, size_t vertexCount,
filament::CameraInfo const& camera, math::float2 const& csNearFar,
float shadowFar, bool stable) noexcept;
uint16_t shadowDimension, bool stable) noexcept;

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

static inline Aabb computeLightFrustumBounds(const math::mat4f& lightView,
Aabb const& wsShadowReceiversVolume, Aabb const& wsShadowCastersVolume,
Expand Down
5 changes: 2 additions & 3 deletions filament/src/ShadowMapManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
// We compute the directional light's model matrix using the origin's as the light position.
// The choice of the light's origin initially doesn't matter for a directional light.
// This will be adjusted later because of how we compute the depth metric for VSM.
const mat4f MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction,
normalize(cameraInfo.worldTransform[0].xyz));
const mat4f MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction);

// Compute scene-dependent values shared across all cascades
ShadowMap::updateSceneInfoDirectional(MvAtOrigin, *scene, sceneInfo);
Expand Down Expand Up @@ -696,7 +695,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap,
const auto outerConeAngle = lcm.getSpotLightOuterCone(li);

// compute shadow map frustum for culling
const mat4f Mv = ShadowMap::getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position);
const mat4f Mv = ShadowMap::getDirectionalLightViewMatrix(direction, position);
const mat4f Mp = mat4f::perspective(outerConeAngle * f::RAD_TO_DEG * 2.0f, 1.0f, 0.01f, radius);
const mat4f MpMv = math::highPrecisionMultiply(Mp, Mv);
const Frustum frustum(MpMv);
Expand Down
Loading

0 comments on commit 763950e

Please sign in to comment.