diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 3d19e99b47..9423dd3c1d 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -4,6 +4,7 @@ This file contains one line summaries of commits that are worthy of mentioning i
A new header is inserted each time a *tag* is created.
## v1.10.6 (currently main branch)
+- engine: Use exponential VSM and improve VSM user settings [⚠️ **Recompile Materials for VSM**].
## v1.10.5
diff --git a/android/filament-android/src/main/cpp/View.cpp b/android/filament-android/src/main/cpp/View.cpp
index f7df75cb45..a1b8896467 100644
--- a/android/filament-android/src/main/cpp/View.cpp
+++ b/android/filament-android/src/main/cpp/View.cpp
@@ -138,10 +138,15 @@ Java_com_google_android_filament_View_nSetShadowType(JNIEnv*, jclass, jlong nati
extern "C" JNIEXPORT void JNICALL
Java_com_google_android_filament_View_nSetVsmShadowOptions(JNIEnv*, jclass, jlong nativeView,
- jint anisotropy) {
+ jint anisotropy, jboolean mipmapping, jfloat exponent, jfloat minVarianceScale,
+ jfloat lightBleedReduction) {
View* view = (View*) nativeView;
View::VsmShadowOptions options;
- options.anisotropy = (uint8_t) anisotropy;
+ options.anisotropy = (uint8_t)anisotropy;
+ options.mipmapping = (bool)mipmapping;
+ options.exponent = exponent;
+ options.minVarianceScale = minVarianceScale;
+ options.lightBleedReduction = lightBleedReduction;
view->setVsmShadowOptions(options);
}
diff --git a/android/filament-android/src/main/java/com/google/android/filament/LightManager.java b/android/filament-android/src/main/java/com/google/android/filament/LightManager.java
index 02fff3614b..3b8629fc2e 100644
--- a/android/filament-android/src/main/java/com/google/android/filament/LightManager.java
+++ b/android/filament-android/src/main/java/com/google/android/filament/LightManager.java
@@ -242,15 +242,17 @@ public class LightManager {
/** Constant bias in world units (e.g. meters) by which shadows are moved away from the
* light. 1mm by default.
+ * This is ignored when the View's ShadowType is set to VSM.
*/
public float constantBias = 0.05f;
/** Amount by which the maximum sampling error is scaled. The resulting value is used
* to move the shadow away from the fragment normal. Should be 1.0.
+ * This is ignored when the View's ShadowType is set to VSM.
*/
public float normalBias = 0.4f;
- /** Distance from the camera after which shadows are clipped. this is used to clip
+ /** Distance from the camera after which shadows are clipped. This is used to clip
* shadows that are too far and wouldn't contribute to the scene much, improving
* performance and quality. This value is always positive.
* Use 0.0f to use the camera far distance.
diff --git a/android/filament-android/src/main/java/com/google/android/filament/View.java b/android/filament-android/src/main/java/com/google/android/filament/View.java
index a1fa68bbc6..41cfe9e379 100644
--- a/android/filament-android/src/main/java/com/google/android/filament/View.java
+++ b/android/filament-android/src/main/java/com/google/android/filament/View.java
@@ -687,20 +687,44 @@ public class View {
/**
* View-level options for VSM shadowing.
*
+ * Warning: This API is still experimental and subject to change.
+ *
* @see View#setVsmShadowOptions
*/
public static class VsmShadowOptions {
/**
* Sets the number of anisotropic samples to use when sampling a VSM shadow map. If greater
* than 0, mipmaps will automatically be generated each frame for all lights.
+ * This implies mipmapping below.
*
*
* The number of anisotropic samples = 2 ^ vsmAnisotropy.
*
*
- * Warning: This API is still experimental and subject to change.
*/
public int anisotropy = 0;
+
+ /**
+ * Whether to generate mipmaps for all VSM shadow maps.
+ */
+ public boolean mipmapping = false;
+
+ /**
+ * EVSM exponent
+ * The maximum value permissible is 5.54 for a shadow map in fp16, or 42.0 for a
+ * shadow map in fp32. Currently the shadow map bit depth is always fp16.
+ */
+ public float exponent = 5.54f;
+
+ /**
+ * VSM minimum variance scale, must be positive.
+ */
+ public float minVarianceScale = 1.0f;
+
+ /**
+ * VSM light bleeding reduction amount, between 0 and 1.
+ */
+ public float lightBleedReduction = 0.2f;
}
/**
@@ -1308,7 +1332,8 @@ public class View {
*/
public void setVsmShadowOptions(@NonNull VsmShadowOptions options) {
mVsmShadowOptions = options;
- nSetVsmShadowOptions(getNativeObject(), options.anisotropy);
+ nSetVsmShadowOptions(getNativeObject(), options.anisotropy, options.mipmapping,
+ options.exponent, options.minVarianceScale, options.lightBleedReduction);
}
/**
@@ -1522,7 +1547,7 @@ public class View {
private static native void nSetRenderQuality(long nativeView, int hdrColorBufferQuality);
private static native void nSetDynamicLightingOptions(long nativeView, float zLightNear, float zLightFar);
private static native void nSetShadowType(long nativeView, int type);
- private static native void nSetVsmShadowOptions(long nativeView, int anisotropy);
+ private static native void nSetVsmShadowOptions(long nativeView, int anisotropy, boolean mipmapping, float exponent, float minVarianceScale, float lightBleedReduction);
private static native void nSetColorGrading(long nativeView, long nativeColorGrading);
private static native void nSetPostProcessingEnabled(long nativeView, boolean enabled);
private static native boolean nIsPostProcessingEnabled(long nativeView);
diff --git a/filament/backend/include/private/backend/SamplerGroup.h b/filament/backend/include/private/backend/SamplerGroup.h
index 71843961ff..2551df6a76 100644
--- a/filament/backend/include/private/backend/SamplerGroup.h
+++ b/filament/backend/include/private/backend/SamplerGroup.h
@@ -80,6 +80,10 @@ public:
setSampler(index, { t, s });
}
+ inline void clearSampler(size_t index) {
+ setSampler(index, {});
+ }
+
private:
#if !defined(NDEBUG)
friend utils::io::ostream& operator<<(utils::io::ostream& out, const SamplerGroup& rhs);
diff --git a/filament/include/filament/LightManager.h b/filament/include/filament/LightManager.h
index 64d3211885..93113bbd00 100644
--- a/filament/include/filament/LightManager.h
+++ b/filament/include/filament/LightManager.h
@@ -230,15 +230,17 @@ public:
/** Constant bias in world units (e.g. meters) by which shadows are moved away from the
* light. 1mm by default.
+ * This is ignored when the View's ShadowType is set to VSM.
*/
float constantBias = 0.001f;
/** Amount by which the maximum sampling error is scaled. The resulting value is used
* to move the shadow away from the fragment normal. Should be 1.0.
+ * This is ignored when the View's ShadowType is set to VSM.
*/
float normalBias = 1.0f;
- /** Distance from the camera after which shadows are clipped. this is used to clip
+ /** Distance from the camera after which shadows are clipped. This is used to clip
* shadows that are too far and wouldn't contribute to the scene much, improving
* performance and quality. This value is always positive.
* Use 0.0f to use the camera far distance.
diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h
index b9a68542ac..08f81a383a 100644
--- a/filament/include/filament/View.h
+++ b/filament/include/filament/View.h
@@ -345,6 +345,7 @@ public:
/**
* View-level options for VSM Shadowing.
* @see setVsmShadowOptions()
+ * @warning This API is still experimental and subject to change.
*/
struct VsmShadowOptions {
/**
@@ -352,10 +353,30 @@ public:
* than 0, mipmaps will automatically be generated each frame for all lights.
*
* The number of anisotropic samples = 2 ^ vsmAnisotropy.
- *
- * @warning This API is still experimental and subject to change.
*/
uint8_t anisotropy = 0;
+
+ /**
+ * Whether to generate mipmaps for all VSM shadow maps.
+ */
+ bool mipmapping = false;
+
+ /**
+ * EVSM exponent.
+ * The maximum value permissible is 5.54 for a shadow map in fp16, or 42.0 for a
+ * shadow map in fp32. Currently the shadow map bit depth is always fp16.
+ */
+ float exponent = 5.54f;
+
+ /**
+ * VSM minimum variance scale, must be positive.
+ */
+ float minVarianceScale = 0.5f;
+
+ /**
+ * VSM light bleeding reduction amount, between 0 and 1.
+ */
+ float lightBleedReduction = 0.15f;
};
/**
diff --git a/filament/src/ShadowMap.cpp b/filament/src/ShadowMap.cpp
index 36594dd7af..682be6833a 100644
--- a/filament/src/ShadowMap.cpp
+++ b/filament/src/ShadowMap.cpp
@@ -90,57 +90,55 @@ void ShadowMap::render(DriverApi& driver, FView::Range const& range, RenderPass&
pass.sortCommands();
}
-void ShadowMap::computeSceneCascadeParams(const FScene::LightSoa& lightData, size_t index,
- FView const& view, filament::CameraInfo const& camera, uint8_t visibleLayers,
- CascadeParameters& cascadeParams) {
- // Calculate the directional light's "position".
- // We could choose any position; we pick the camera position so we have a fixed
- // reference -- that's "not too far" from the scene.
- cascadeParams.wsLightPosition = camera.getPosition();
+mat4f ShadowMap::getLightViewMatrix(float3 position, float3 direction) noexcept {
+ const mat4f M = mat4f::lookAt(position, position + direction, float3{ 0, 1, 0 });
+ return FCamera::rigidTransformInverse(M);
+}
- // Compute the light's model matrix.
- const float3 lightPosition = cascadeParams.wsLightPosition;
- const float3 dir = lightData.elementAt(index);
- const mat4f M = mat4f::lookAt(lightPosition, lightPosition + dir, float3{ 0, 1, 0 });
- const mat4f Mv = FCamera::rigidTransformInverse(M);
+void ShadowMap::computeSceneInfo(float3 dir,
+ FScene const& scene, filament::CameraInfo const& camera, uint8_t visibleLayers,
+ SceneInfo& sceneInfo) {
+ // We assume the light is at the origin to compute the SceneInfo. This is consumed later by
+ // computeShadowCameraDirectional() which takes this into account.
+ const mat4f Mv = getLightViewMatrix({}, dir);
const mat4f V = camera.view;
// Compute scene bounds in world space, as well as the light-space and view-space near/far planes
- cascadeParams.lsNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() };
- cascadeParams.vsNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() };
- cascadeParams.wsShadowCastersVolume = {};
- cascadeParams.wsShadowReceiversVolume = {};
- visitScene(*view.getScene(), visibleLayers,
+ sceneInfo.lsNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() };
+ sceneInfo.vsNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() };
+ sceneInfo.wsShadowCastersVolume = {};
+ sceneInfo.wsShadowReceiversVolume = {};
+ visitScene(scene, visibleLayers,
[&](Aabb caster) {
- cascadeParams.wsShadowCastersVolume.min =
- min(cascadeParams.wsShadowCastersVolume.min, caster.min);
- cascadeParams.wsShadowCastersVolume.max =
- max(cascadeParams.wsShadowCastersVolume.max, caster.max);
+ sceneInfo.wsShadowCastersVolume.min =
+ min(sceneInfo.wsShadowCastersVolume.min, caster.min);
+ sceneInfo.wsShadowCastersVolume.max =
+ max(sceneInfo.wsShadowCastersVolume.max, caster.max);
float2 nf = computeNearFar(Mv, caster);
- cascadeParams.lsNearFar.x = std::max(cascadeParams.lsNearFar.x, nf.x); // near
- cascadeParams.lsNearFar.y = std::min(cascadeParams.lsNearFar.y, nf.y); // far
+ sceneInfo.lsNearFar.x = std::max(sceneInfo.lsNearFar.x, nf.x); // near
+ sceneInfo.lsNearFar.y = std::min(sceneInfo.lsNearFar.y, nf.y); // far
},
[&](Aabb receiver) {
- cascadeParams.wsShadowReceiversVolume.min =
- min(cascadeParams.wsShadowReceiversVolume.min, receiver.min);
- cascadeParams.wsShadowReceiversVolume.max =
- max(cascadeParams.wsShadowReceiversVolume.max, receiver.max);
+ sceneInfo.wsShadowReceiversVolume.min =
+ min(sceneInfo.wsShadowReceiversVolume.min, receiver.min);
+ sceneInfo.wsShadowReceiversVolume.max =
+ max(sceneInfo.wsShadowReceiversVolume.max, receiver.max);
float2 nf = computeNearFar(V, receiver);
- cascadeParams.vsNearFar.x = std::max(cascadeParams.vsNearFar.x, nf.x);
- cascadeParams.vsNearFar.y = std::min(cascadeParams.vsNearFar.y, nf.y);
+ sceneInfo.vsNearFar.x = std::max(sceneInfo.vsNearFar.x, nf.x);
+ sceneInfo.vsNearFar.y = std::min(sceneInfo.vsNearFar.y, nf.y);
}
);
}
-void ShadowMap::update(const FScene::LightSoa& lightData, size_t index, FScene const* scene,
- filament::CameraInfo const& camera, uint8_t visibleLayers, ShadowMapLayout layout,
- const CascadeParameters& cascadeParams) noexcept {
+void ShadowMap::update(const FScene::LightSoa& lightData, size_t index,
+ filament::CameraInfo const& camera, const ShadowMapInfo& shadowMapInfo,
+ const SceneInfo& cascadeParams) noexcept {
// this is the hard part here, find a good frustum for our camera
auto& lcm = mEngine.getLightManager();
FLightManager::Instance li = lightData.elementAt(index);
- mShadowMapLayout = layout;
+ mShadowMapInfo = shadowMapInfo;
FLightManager::ShadowParams params = lcm.getShadowParams(li);
mPolygonOffset = {
@@ -148,6 +146,10 @@ void ShadowMap::update(const FScene::LightSoa& lightData, size_t index, FScene c
.slope = -params.options.polygonOffsetSlope,
.constant = -params.options.polygonOffsetConstant
};
+
+ // Note: we keep the polygon offset even with VSM as it seems to help.
+
+ // Adjust the camera's projection for the light's shadowFar
mat4f projection(camera.cullingProjection);
if (params.options.shadowFar > 0.0f) {
float n = camera.zn;
@@ -163,7 +165,7 @@ void ShadowMap::update(const FScene::LightSoa& lightData, size_t index, FScene c
}
}
- CameraInfo cameraInfo = {
+ const CameraInfo cameraInfo = {
.projection = projection,
.model = camera.model,
.view = camera.view,
@@ -181,45 +183,43 @@ void ShadowMap::update(const FScene::LightSoa& lightData, size_t index, FScene c
else params.options.shadowNearHint = dzn * dz - camera.zn;
if (dzf > 0) dzf =-std::max(0.0f, camera.zf - params.options.shadowFarHint) / dz;
else params.options.shadowFarHint = dzf * dz + camera.zf;
- using Type = FLightManager::Type;
+
+ using LightType = FLightManager::Type;
switch (lcm.getType(li)) {
- case Type::SUN:
- case Type::DIRECTIONAL:
+ case LightType::SUN:
+ case LightType::DIRECTIONAL:
computeShadowCameraDirectional(
- lightData.elementAt(index), scene, cameraInfo, params,
- visibleLayers, cascadeParams);
+ lightData.elementAt(index),
+ cameraInfo, params, cascadeParams);
break;
- case Type::FOCUSED_SPOT:
- case Type::SPOT:
- computeShadowCameraSpot(lightData.elementAt(index).xyz,
+ case LightType::FOCUSED_SPOT:
+ case LightType::SPOT:
+ computeShadowCameraSpot(
+ lightData.elementAt(index).xyz,
lightData.elementAt(index), lcm.getSpotLightOuterCone(li),
lightData.elementAt(index).w, cameraInfo, params);
break;
- case Type::POINT:
+ case LightType::POINT:
break;
}
}
void ShadowMap::computeShadowCameraDirectional(
- float3 const& dir, FScene const* scene, CameraInfo const& camera,
+ float3 const& dir, CameraInfo const& camera,
FLightManager::ShadowParams const& params,
- uint8_t visibleLayers, CascadeParameters cascadeParams) noexcept {
+ SceneInfo cascadeParams) noexcept {
/*
* Compute the light's model matrix
- * (direction & position)
- *
- * The light's model matrix contains the light position and direction.
- *
- * The directional light position is chosen inside computeSceneCascadeParams.
*/
- const float3 lightPosition = cascadeParams.wsLightPosition;
- const mat4f M = mat4f::lookAt(lightPosition, lightPosition + dir, float3{ 0, 1, 0 });
- const mat4f Mv = FCamera::rigidTransformInverse(M);
- const Aabb& wsShadowCastersVolume = cascadeParams.wsShadowCastersVolume;
- const Aabb& wsShadowReceiversVolume = cascadeParams.wsShadowReceiversVolume;
+ // 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 mater for a directional light.
+ // This will be adjusted later because of how we compute the depth metric for VSM.
+ const mat4f MvAtOrigin = getLightViewMatrix({}, dir);
+ const Aabb wsShadowCastersVolume = cascadeParams.wsShadowCastersVolume;
+ const Aabb wsShadowReceiversVolume = cascadeParams.wsShadowReceiversVolume;
if (wsShadowCastersVolume.isEmpty() || wsShadowReceiversVolume.isEmpty()) {
mHasVisibleShadows = false;
return;
@@ -257,7 +257,7 @@ void ShadowMap::computeShadowCameraDirectional(
}
for (size_t i = 0; i < vertexCount; ++i) {
// far: figure out farthest shadow receivers
- float3 v = mat4f::project(Mv, mWsClippedShadowReceiverVolume[i]);
+ float3 v = mat4f::project(MvAtOrigin, mWsClippedShadowReceiverVolume[i]);
lsLightFrustumBounds.min.z = std::min(lsLightFrustumBounds.min.z, v.z);
if (USE_DEPTH_CLAMP) {
// further tighten to the shadow receiver volume
@@ -269,10 +269,14 @@ void ShadowMap::computeShadowCameraDirectional(
lsLightFrustumBounds.min.z = std::max(lsLightFrustumBounds.min.z, cascadeParams.lsNearFar[1]);
}
+ // Now that we know the znear (-lsLightFrustumBounds.max.z), adjust the light's position such
+ // that znear = 0, this is only need for VSM, but doesn't hurt PCF.
+ const mat4f Mv = getLightViewMatrix(dir * -lsLightFrustumBounds.max.z, dir);
+
// near / far planes are specified relative to the direction the eye is looking at
// i.e. the -z axis (see: ortho)
- const float znear = -lsLightFrustumBounds.max.z;
- const float zfar = -lsLightFrustumBounds.min.z;
+ const float znear = 0.0f;
+ const float zfar = lsLightFrustumBounds.max.z - lsLightFrustumBounds.min.z;
// if znear >= zfar, it means we don't have any shadow caster in front of a shadow receiver
if (UTILS_UNLIKELY(znear >= zfar)) {
@@ -397,7 +401,7 @@ void ShadowMap::computeShadowCameraDirectional(
if (params.options.stable) {
// Use the world origin as reference point, fixed w.r.t. the camera
- snapLightFrustum(s, o, Mv, camera.worldOrigin[3].xyz, 1.0f / mShadowMapLayout.shadowDimension);
+ snapLightFrustum(s, o, Mv, camera.worldOrigin[3].xyz, 1.0f / mShadowMapInfo.shadowDimension);
}
const mat4f F(mat4f::row_major_init {
@@ -407,7 +411,6 @@ void ShadowMap::computeShadowCameraDirectional(
0.0f, 0.0f, 0.0f, 1.0f,
});
-
/*
* Final shadow map transform
*/
@@ -428,32 +431,37 @@ void ShadowMap::computeShadowCameraDirectional(
// We know we're using an ortho projection
mTexelSizeWs = texelSizeWorldSpace(St.upperLeft());
}
- mLightSpace = St;
-
- mLightSpaceVsm = computeVsmLightSpaceMatrix(mLightSpace, Mv, zfar);
+ if (!mShadowMapInfo.vsm) {
+ mLightSpace = St;
+ } else {
+ mLightSpace = computeVsmLightSpaceMatrix(St, Mv, zfar);
+ }
// 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
// by baking it in the shadow-map itself.
-
- const mat4f b = mat4f::translation(dir * params.options.constantBias);
- const mat4f Sb = S * b;
+ const float constantBias = mShadowMapInfo.vsm ? 0.0f : params.options.constantBias;
+ const mat4f b = mat4f::translation(dir * constantBias);
// It's important to set the light camera's model matrix separately from its projection, so
// that the cameraPosition uniform gets set correctly.
// mLightSpace is used in the shader to access the shadow map texture, and has the model
// matrix baked in.
- mCamera->setModelMatrix(FCamera::rigidTransformInverse(b) * M);
+ // The model matrix below is in fact inverted to get the view matrix and passed to the
+ // shader as 'viewFromWorldMatrix', and is used in the VSM case to compute the depth metric.
+ // (see depth_main.fs). Note that in the case of VSM, 'b' below is identity.
+ mCamera->setModelMatrix(FCamera::rigidTransformInverse(Mv * b));
mCamera->setCustomProjection(mat4(F * W * L * Mp), znear, zfar);
// for the debug camera, we need to undo the world origin
- mDebugCamera->setCustomProjection(mat4(Sb * camera.worldOrigin), znear, zfar);
+ mDebugCamera->setCustomProjection(mat4(S * b * camera.worldOrigin), znear, zfar);
}
}
void ShadowMap::computeShadowCameraSpot(math::float3 const& position, math::float3 const& dir,
- float outerConeAngle, float radius, CameraInfo const& camera, FLightManager::ShadowParams const& params) noexcept {
+ float outerConeAngle, float radius, CameraInfo const& camera,
+ FLightManager::ShadowParams const& params) noexcept {
// TODO: correctly compute if this spot light has any visible shadows.
mHasVisibleShadows = true;
@@ -463,12 +471,9 @@ void ShadowMap::computeShadowCameraSpot(math::float3 const& position, math::floa
*/
// Choose a reasonable value for the near plane.
- const float nearPlane = 0.1f;
+ const float nearPlane = radius * (1.0f / 1024.0f);
const float farPlane = radius;
-
- const float3 lightPosition = position;
- const mat4f M = mat4f::lookAt(lightPosition, lightPosition + dir, float3{0, 1, 0});
- const mat4f Mv = FCamera::rigidTransformInverse(M);
+ const mat4f Mv = getLightViewMatrix(position, dir);
float outerConeAngleDegrees = outerConeAngle * f::RAD_TO_DEG;
const mat4f Mp = mat4f::perspective(outerConeAngleDegrees * 2, 1.0f, nearPlane, farPlane,
@@ -482,11 +487,15 @@ void ShadowMap::computeShadowCameraSpot(math::float3 const& position, math::floa
const mat4f MbMt = getTextureCoordsMapping();
const mat4f St = MbMt * S;
mTexelSizeWs = texelSizeWorldSpace(Mp, MbMt);
- mLightSpace = St;
- mLightSpaceVsm = computeVsmLightSpaceMatrix(mLightSpace, Mv, farPlane);
+ if (!mShadowMapInfo.vsm) {
+ mLightSpace = St;
+ } else {
+ mLightSpace = computeVsmLightSpaceMatrix(St, Mv, farPlane);
+ }
- const mat4f b = mat4f::translation(dir * params.options.constantBias);
+ const float constantBias = mShadowMapInfo.vsm ? 0.0f : params.options.constantBias;
+ const mat4f b = mat4f::translation(dir * constantBias);
const mat4f Sb = S * b;
// It's important to set the light camera's model matrix separately from its projection, so that
@@ -494,7 +503,10 @@ void ShadowMap::computeShadowCameraSpot(math::float3 const& position, math::floa
// mLightSpace is used in the shader to access the shadow map texture, and has the model matrix
// baked in.
- mCamera->setModelMatrix(FCamera::rigidTransformInverse(b) * M);
+ // The model matrix below is in fact inverted to get the view matrix and passed to the
+ // shader as 'viewFromWorldMatrix', and is used in the VSM case to compute the depth metric.
+ // (see depth_main.fs). Note that in the case of VSM, 'b' below is identity.
+ mCamera->setModelMatrix(FCamera::rigidTransformInverse(Mv * b));
mCamera->setCustomProjection(mat4(Mp), nearPlane, farPlane);
// for the debug camera, we need to undo the world origin
@@ -595,7 +607,7 @@ mat4f ShadowMap::getTextureCoordsMapping() const noexcept {
// the shadow map texture might be larger than the shadow map dimension, so we add a scaling
// factor
- const float v = (float) mShadowMapLayout.textureDimension / mShadowMapLayout.atlasDimension;
+ const float v = (float) mShadowMapInfo.textureDimension / mShadowMapInfo.atlasDimension;
const mat4f Mv(mat4f::row_major_init{
v, 0.0f, 0.0f, 0.0f,
0.0f, v, 0.0f, 0.0f,
@@ -604,8 +616,8 @@ mat4f ShadowMap::getTextureCoordsMapping() const noexcept {
});
// apply the 1-texel border viewport transform
- const float o = 1.0f / mShadowMapLayout.atlasDimension;
- const float s = 1.0f - 2.0f * (1.0f / mShadowMapLayout.textureDimension);
+ const float o = 1.0f / mShadowMapInfo.atlasDimension;
+ const float s = 1.0f - 2.0f * (1.0f / mShadowMapInfo.textureDimension);
const mat4f Mb(mat4f::row_major_init{
s, 0.0f, 0.0f, o,
0.0f, s, 0.0f, o,
@@ -624,22 +636,22 @@ mat4f ShadowMap::getTextureCoordsMapping() const noexcept {
return Mf * Mb * Mv * Mt;
}
-math::mat4f ShadowMap::computeVsmLightSpaceMatrix(const math::mat4f& lightSpacePcf, const
- math::mat4f& Mv, float zfar) noexcept {
+math::mat4f ShadowMap::computeVsmLightSpaceMatrix(const math::mat4f& lightSpacePcf,
+ const math::mat4f& Mv, float zfar) noexcept {
// The lightSpacePcf matrix transforms coordinates from world space into (u, v, z) coordinates,
- // where (u, v) are used to access the shadow map, and z is the PCF comparison value between 0
- // and 1.
- // For VSM, we want to leave the z coordinate in linear light space, with a few adjustments:
- // - We scale the z by zfar, which prevents us from overflowing when we square z in the depth
- // shader.
- // - The negative sign accounts for our right-handed coordinate system, where -z is forward.
+ // where (u, v) are used to access the shadow map, and z is the (non linear) PCF comparison
+ // value [0, 1].
+ //
+ // For VSM, we want to leave the z coordinate in linear light space, normalized between [0, 1]
+ // (the normalization factor is therefore -1/zfar).
+ //
// When sampling a VSM shadow map, the shader follows suit, and doesn't divide by w for the z
// coordinate. See getters.fs.
- math::mat4f lightSpaceVsm = lightSpacePcf;
- lightSpaceVsm[0][2] = Mv[0][2] * (-1.0f / std::abs(zfar));
- lightSpaceVsm[1][2] = Mv[1][2] * (-1.0f / std::abs(zfar));
- lightSpaceVsm[2][2] = Mv[2][2] * (-1.0f / std::abs(zfar));
- lightSpaceVsm[3][2] = Mv[3][2] * (-1.0f / std::abs(zfar));
+ math::mat4f lightSpaceVsm{ lightSpacePcf };
+ lightSpaceVsm[0].z = Mv[0].z * (-1.0f / zfar);
+ lightSpaceVsm[1].z = Mv[1].z * (-1.0f / zfar);
+ lightSpaceVsm[2].z = Mv[2].z * (-1.0f / zfar);
+ lightSpaceVsm[3].z = Mv[3].z * (-1.0f / zfar);
return lightSpaceVsm;
}
@@ -984,8 +996,8 @@ float ShadowMap::texelSizeWorldSpace(const mat3f& worldToShadowTexture) const no
// orthographic projections. We just need to inverse worldToShadowTexture,
// which is guaranteed to be orthographic.
// The two first columns give us the how a texel maps in world-space.
- const float ures = 1.0f / mShadowMapLayout.shadowDimension;
- const float vres = 1.0f / mShadowMapLayout.shadowDimension;
+ const float ures = 1.0f / mShadowMapInfo.shadowDimension;
+ const float vres = 1.0f / mShadowMapInfo.shadowDimension;
const mat3f shadowTextureToWorld(inverse(worldToShadowTexture));
const float3 Jx = shadowTextureToWorld[0];
const float3 Jy = shadowTextureToWorld[1];
@@ -1008,9 +1020,9 @@ float ShadowMap::texelSizeWorldSpace(const mat4f& Wp, const mat4f& MbMtF) const
// It might be better to do this computation in the vertex shader.
float3 p = {0.5, 0.5, 0.0};
- const float ures = 1.0f / mShadowMapLayout.shadowDimension;
- const float vres = 1.0f / mShadowMapLayout.shadowDimension;
- const float dres = mShadowMapLayout.zResolution;
+ const float ures = 1.0f / mShadowMapInfo.shadowDimension;
+ const float vres = 1.0f / mShadowMapInfo.shadowDimension;
+ const float dres = mShadowMapInfo.zResolution;
constexpr bool JACOBIAN_ESTIMATE = false;
if (JACOBIAN_ESTIMATE) {
diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp
index dd3ed90fc2..0c2ac6c82d 100644
--- a/filament/src/ShadowMapManager.cpp
+++ b/filament/src/ShadowMapManager.cpp
@@ -90,7 +90,10 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
auto passes = utils::FixedCapacityVector::with_capacity(MAX_SHADOW_LAYERS);
uint8_t layerSampleCount[MAX_SHADOW_LAYERS] = {};
- assert_invariant(mTextureRequirements.layers <= MAX_SHADOW_LAYERS);
+ // make a copy here, because it's a very small structure
+ TextureRequirements const textureRequirements = mTextureRequirements;
+
+ assert_invariant(textureRequirements.layers <= MAX_SHADOW_LAYERS);
// These loops fill render passes with appropriate rendering commands for each shadow map.
// The actual render pass execution is deferred to the frame graph.
@@ -99,9 +102,9 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
continue;
}
- map.getShadowMap()->render(driver, view.getVisibleDirectionalShadowCasters(), pass, view);
+ map.getShadowMap().render(driver, view.getVisibleDirectionalShadowCasters(), pass, view);
- assert_invariant(map.getLayout().layer < mTextureRequirements.layers);
+ assert_invariant(map.getLayout().layer < textureRequirements.layers);
passes.emplace_back(&map, pass);
const uint8_t layer = map.getLayout().layer;
@@ -115,26 +118,26 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
}
pass.setVisibilityMask(VISIBLE_SPOT_SHADOW_RENDERABLE_N(i));
- map.getShadowMap()->render(driver, view.getVisibleSpotShadowCasters(), pass, view);
+ map.getShadowMap().render(driver, view.getVisibleSpotShadowCasters(), pass, view);
pass.clearVisibilityMask();
- assert_invariant(map.getLayout().layer < mTextureRequirements.layers);
+ assert_invariant(map.getLayout().layer < textureRequirements.layers);
passes.emplace_back(&map, pass);
const uint8_t layer = map.getLayout().layer;
assert_invariant(layer < MAX_SHADOW_LAYERS);
layerSampleCount[layer] = map.getLayout().vsmSamples;
}
- assert_invariant(passes.size() <= mTextureRequirements.layers);
+ assert_invariant(passes.size() <= textureRequirements.layers);
const bool fillWithCheckerboard = engine.debug.shadowmap.checkerboard && !view.hasVsm();
auto& shadowPass = fg.addPass("Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
FrameGraphTexture::Descriptor shadowTextureDesc {
- .width = mTextureRequirements.size, .height = mTextureRequirements.size,
- .depth = mTextureRequirements.layers,
- .levels = mTextureRequirements.levels,
+ .width = textureRequirements.size, .height = textureRequirements.size,
+ .depth = textureRequirements.layers,
+ .levels = textureRequirements.levels,
.type = SamplerType::SAMPLER_2D_ARRAY,
.format = mTextureFormat
};
@@ -149,7 +152,7 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
// When rendering VSM shadow maps, we still need a depth texture for correct
// sorting. The texture is cleared before each pass and discarded afterwards.
data.tempDepth = builder.createTexture("Temporary VSM Depth Texture", {
- .width = mTextureRequirements.size, .height = mTextureRequirements.size,
+ .width = textureRequirements.size, .height = textureRequirements.size,
.depth = 1,
.levels = 1,
// Each shadow pass has its own sample count. We specify samples = 1 here to
@@ -157,14 +160,12 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
// sample counts automatically.
.samples = 1,
.type = SamplerType::SAMPLER_2D,
- .format = TextureFormat::DEPTH16,
+ .format = mTextureFormat, // use the same format that we'd use for regular shadows
});
- // We specify "read" for the temporary shadow texture, so it isn't culled.
-// data.tempDepth = builder.read(data.tempDepth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
}
// Create a render target for each layer of the texture array.
- for (uint8_t i = 0u; i < mTextureRequirements.layers; i++) {
+ for (uint8_t i = 0u; i < textureRequirements.layers; i++) {
FrameGraphRenderPass::Descriptor renderTargetDesc {};
if (view.hasVsm()) {
auto attachment = builder.createSubresource(data.shadows, "Shadow Texture Mip", { .layer = i });
@@ -172,7 +173,8 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
data.tempDepth = builder.write(data.tempDepth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
renderTargetDesc.attachments = { .color = { attachment }, .depth = data.tempDepth };
renderTargetDesc.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
- renderTargetDesc.clearColor = { 1.0f, 1.0f, 0.0f, 0.0f };
+ // we need to clear the shadow map with the max EVSM moments
+ renderTargetDesc.clearColor = { 256.0f, 65536.f, 256.0f, 65536.f };
renderTargetDesc.samples = layerSampleCount[i];
} else {
auto attachment = builder.createSubresource(data.shadows, "Shadow Texture Mip", { .layer = i });
@@ -187,7 +189,8 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
[=, passes = std::move(passes), &view, &engine](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) mutable {
for (auto& [map, pass] : passes) {
- FCamera const& camera = map->getShadowMap()->getCamera();
+ // TODO: camera is already set inside 'pass', we could get it from there
+ FCamera const& camera = map->getShadowMap().getCamera();
filament::CameraInfo cameraInfo(camera);
view.prepareCamera(cameraInfo);
@@ -204,13 +207,18 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
filament::Viewport viewport { 1, 1, dim - 2, dim - 2 };
view.prepareViewport(viewport);
+ // set uniforms needed to render this ShadowMap
+ // Currently these uniforms are owned by View and are global, but eventully
+ // this will set a separate per shadowmap UBO
+ view.prepareShadowMap();
+
view.commitUniforms(driver);
const auto layer = map->getLayout().layer;
auto rt = resources.getRenderPassInfo(data.rt[layer]);
rt.params.viewport = viewport;
- auto polygonOffset = map->getShadowMap()->getPolygonOffset();
+ auto polygonOffset = map->getShadowMap().getPolygonOffset();
pass.overridePolygonOffset(&polygonOffset);
pass.execute("Shadow Pass", rt.target, rt.params);
@@ -235,19 +243,19 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
[=](FrameGraphResources const& resources, DebugPatternData const& data,
DriverApi& driver) {
fillWithDebugPattern(driver, resources.getTexture(data.shadows),
- mTextureRequirements.size);
+ textureRequirements.size);
});
shadows = debugPatternPass.getData().shadows;
}
- // If the shadow texture has more than one level, then anisotropy was specified and we should
- // generate VSM mipmaps.
- if (mTextureRequirements.levels > 1) {
+ // If the shadow texture has more than one level, mipmapping was requested, either directly
+ // or indirectly via anisotropic filtering.
+ if (textureRequirements.levels > 1) {
auto& ppm = engine.getPostProcessManager();
- for (uint8_t layer = 0; layer < mTextureRequirements.layers; layer++) {
- for (size_t level = 0; level < mTextureRequirements.levels - 1; level++) {
- const bool finalize = mTextureRequirements.levels - 2;
+ for (uint8_t layer = 0; layer < textureRequirements.layers; layer++) {
+ for (size_t level = 0; level < textureRequirements.levels - 1; level++) {
+ const bool finalize = textureRequirements.levels - 2;
shadows = ppm.vsmMipmapPass(fg, shadows, layer, level, finalize);
}
}
@@ -256,26 +264,6 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view,
fg.getBlackboard().put("shadows", shadows);
}
-void ShadowMapManager::prepareShadow(backend::Handle texture,
- FView const& view) const noexcept {
- uint8_t anisotropy = 0;
- SamplerMinFilter filterMin = SamplerMinFilter::LINEAR;
- if (view.hasVsm()) {
- anisotropy = view.getVsmShadowOptions().anisotropy;
- if (anisotropy > 0) {
- filterMin = SamplerMinFilter::LINEAR_MIPMAP_LINEAR;
- }
- }
- view.getViewSamplers().setSampler(PerViewSib::SHADOW_MAP, {
- texture, {
- .filterMag = SamplerMagFilter::LINEAR,
- .filterMin = filterMin,
- .anisotropyLog2 = anisotropy,
- .compareMode = SamplerCompareMode::COMPARE_TO_TEXTURE,
- .compareFunc = SamplerCompareFunc::GE
- }});
-}
-
ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(
FEngine& engine, FView& view,
UniformBuffer& perViewUb, FScene::RenderableSoa& renderableData,
@@ -289,58 +277,59 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(
FLightManager::Instance directionalLight = lightData.elementAt(0);
LightManager::ShadowOptions const& options = lcm.getShadowOptions(directionalLight);
- ShadowMap::CascadeParameters cascadeParams;
+ ShadowMap::SceneInfo sceneInfo;
if (!mCascadeShadowMaps.empty()) {
- // Compute scene-dependent values shared across all cascades.
- ShadowMap::computeSceneCascadeParams(lightData, 0, view, viewingCameraInfo, visibleLayers,
- cascadeParams);
+ // Compute scene-dependent values shared across all cascades
+ const float3 dir = lightData.elementAt(0);
+ ShadowMap::computeSceneInfo(dir,
+ *scene, viewingCameraInfo, visibleLayers, sceneInfo);
// Even if we have more than one cascade, we cull directional shadow casters against the
// entire camera frustum, as if we only had a single cascade.
- ShadowMap& map = *mCascadeShadowMaps[0].getShadowMap();
- const size_t textureDimension = mCascadeShadowMaps[0].getLayout().size;
- const ShadowMap::ShadowMapLayout layout {
+ ShadowMapEntry& entry = mCascadeShadowMaps[0];
+ ShadowMap& map = entry.getShadowMap();
+ const size_t textureDimension = entry.getLayout().size;
+ const ShadowMap::ShadowMapInfo shadowMapInfo {
.zResolution = mTextureZResolution,
.atlasDimension = textureSize,
.textureDimension = textureDimension,
- .shadowDimension = textureDimension - 2
+ .shadowDimension = textureDimension - 2,
+ .vsm = view.hasVsm()
};
- map.update(lightData, 0, scene, viewingCameraInfo, visibleLayers,
- layout, cascadeParams);
+
+ map.update(lightData, 0, viewingCameraInfo, shadowMapInfo, sceneInfo);
+
Frustum const& frustum = map.getCamera().getFrustum();
FView::cullRenderables(engine.getJobSystem(), renderableData, frustum,
VISIBLE_DIR_SHADOW_RENDERABLE_BIT);
+ // note: normalBias is ignored for VSM
+ const float normalBias = lcm.getShadowNormalBias(0);
// Set shadowBias, using the first directional cascade.
const float texelSizeWorldSpace = map.getTexelSizeWorldSpace();
- const float normalBias = lcm.getShadowNormalBias(0);
perViewUb.setUniform(offsetof(PerViewUib, shadowBias),
float3{0, normalBias * texelSizeWorldSpace, 0});
-
- // Set the directional light position. Only used when VSM is active.
- perViewUb.setUniform(offsetof(PerViewUib, lightPosition), cascadeParams.wsLightPosition);
}
- // Adjust the near and far planes to tighly bound the scene.
+ // Adjust the near and far planes to tightly bound the scene.
float vsNear = -viewingCameraInfo.zn;
float vsFar = -viewingCameraInfo.zf;
if (engine.debug.shadowmap.tightly_bound_scene) {
- vsNear = std::min(vsNear, cascadeParams.vsNearFar.x);
- vsFar = std::max(vsFar, cascadeParams.vsNearFar.y);
+ vsNear = std::min(vsNear, sceneInfo.vsNearFar.x);
+ vsFar = std::max(vsFar, sceneInfo.vsNearFar.y);
}
const size_t cascadeCount = mCascadeShadowMaps.size();
// We divide the camera frustum into N cascades. This gives us N + 1 split positions.
// The first split position is the near plane; the last split position is the far plane.
- std::array splitPercentages{};
+ std::array splitPercentages;
splitPercentages[0] = 0.0f;
- size_t i = 1;
- for (; i < cascadeCount; i++) {
+ splitPercentages[cascadeCount] = 1.0f;
+ for (size_t i = 1 ; i < cascadeCount; i++) {
splitPercentages[i] = options.cascadeSplitPositions[i - 1];
}
- splitPercentages[i] = 1.0f;
const CascadeSplits::Params p {
.proj = viewingCameraInfo.cullingProjection,
@@ -350,7 +339,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(
.splitPositions = splitPercentages
};
if (p != mCascadeSplitParams) {
- mCascadeSplits = CascadeSplits(p);
+ mCascadeSplits = CascadeSplits{ p };
mCascadeSplitParams = p;
}
@@ -360,8 +349,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(
// (which is the near plane, and doesn't need to be communicated to the shaders).
static_assert(CONFIG_MAX_SHADOW_CASCADES <= 5,
"At most, a float4 can fit 4 split positions for 5 shadow cascades");
- float4 wsSplitPositionUniform;
- std::fill_n(&wsSplitPositionUniform[0], 4, -std::numeric_limits::infinity());
+ float4 wsSplitPositionUniform{ -std::numeric_limits::infinity() };
std::copy(splits.beginWs() + 1, splits.endWs(), &wsSplitPositionUniform[0]);
float csSplitPosition[CONFIG_MAX_SHADOW_CASCADES + 1];
@@ -374,31 +362,27 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(
uint32_t directionalShadowsMask = 0;
uint32_t cascadeHasVisibleShadows = 0;
float screenSpaceShadowDistance = 0.0f;
- for (size_t i = 0; i < mCascadeShadowMaps.size(); i++) {
+ for (size_t i = 0, c = mCascadeShadowMaps.size(); i < c; i++) {
auto& entry = mCascadeShadowMaps[i];
// Compute the frustum for the directional light.
- ShadowMap& shadowMap = *entry.getShadowMap();
- UTILS_UNUSED_IN_RELEASE size_t l = entry.getLightIndex();
- assert_invariant(l == 0);
+ ShadowMap& shadowMap = entry.getShadowMap();
+ assert_invariant(entry.getLightIndex() == 0);
const size_t textureDimension = entry.getLayout().size;
- const ShadowMap::ShadowMapLayout layout{
+ const ShadowMap::ShadowMapInfo shadowMapInfo{
.zResolution = mTextureZResolution,
.atlasDimension = textureSize,
.textureDimension = textureDimension,
- .shadowDimension = textureDimension - 2
+ .shadowDimension = textureDimension - 2,
+ .vsm = view.hasVsm()
};
- cascadeParams.csNearFar = { csSplitPosition[i], csSplitPosition[i + 1] };
- shadowMap.update(lightData, 0, scene, viewingCameraInfo, visibleLayers, layout, cascadeParams);
+ sceneInfo.csNearFar = { csSplitPosition[i], csSplitPosition[i + 1] };
+ shadowMap.update(lightData, 0, viewingCameraInfo, shadowMapInfo, sceneInfo);
if (shadowMap.hasVisibleShadows()) {
entry.setHasVisibleShadows(true);
-
- mat4f const& lightFromWorldMatrix =
- view.hasVsm() ? shadowMap.getLightSpaceMatrixVsm() : shadowMap.getLightSpaceMatrix();
- perViewUb.setUniform(offsetof(PerViewUib, lightFromWorldMatrix) +
- sizeof(mat4f) * i, lightFromWorldMatrix);
-
+ perViewUb.setUniform(offsetof(PerViewUib, lightFromWorldMatrix[i]),
+ shadowMap.getLightSpaceMatrix());
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
cascadeHasVisibleShadows |= 0x1u << i;
}
@@ -437,9 +421,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(
FScene::RenderableSoa& renderableData, FScene::LightSoa& lightData) noexcept {
ShadowTechnique shadowTechnique{};
- FScene* scene = view.getScene();
const CameraInfo& viewingCameraInfo = view.getCameraInfo();
- uint8_t visibleLayers = view.getVisibleLayers();
const uint16_t textureSize = mTextureRequirements.size;
// shadow-map shadows for point/spot lights
@@ -449,17 +431,18 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(
auto& entry = mSpotShadowMaps[i];
// compute the frustum for this light
- ShadowMap& shadowMap = *entry.getShadowMap();
+ ShadowMap& shadowMap = entry.getShadowMap();
size_t l = entry.getLightIndex();
const size_t textureDimension = entry.getLayout().size;
- const ShadowMap::ShadowMapLayout layout{
+ const ShadowMap::ShadowMapInfo layout{
.zResolution = mTextureZResolution,
.atlasDimension = textureSize,
.textureDimension = textureDimension,
- .shadowDimension = textureDimension - 2
+ .shadowDimension = textureDimension - 2,
+ .vsm = view.hasVsm()
};
- shadowMap.update(lightData, l, scene, viewingCameraInfo, visibleLayers, layout, {});
+ shadowMap.update(lightData, l, viewingCameraInfo, layout, {});
FLightManager::Instance light = lightData.elementAt(l);
if (shadowMap.hasVisibleShadows()) {
@@ -471,20 +454,19 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(
FView::cullRenderables(engine.getJobSystem(), renderableData, frustum,
VISIBLE_SPOT_SHADOW_RENDERABLE_N_BIT(i));
- mat4f const& lightFromWorldMatrix =
- view.hasVsm() ? shadowMap.getLightSpaceMatrixVsm() : shadowMap.getLightSpaceMatrix();
- u.setUniform(offsetof(ShadowUib, spotLightFromWorldMatrix) +
- sizeof(mat4f) * i, lightFromWorldMatrix);
+ u.setUniform(offsetof(ShadowUib, spotLightFromWorldMatrix[i]),
+ shadowMap.getLightSpaceMatrix());
shadowInfo[l].castsShadows = true;
shadowInfo[l].index = i;
shadowInfo[l].layer = mSpotShadowMaps[i].getLayout().layer;
+ // note: normalBias is ignored for VSM
const float3 dir = lightData.elementAt(l);
const float texelSizeWorldSpace = shadowMap.getTexelSizeWorldSpace();
const float normalBias = lcm.getShadowNormalBias(light);
- u.setUniform(offsetof(ShadowUib, directionShadowBias) + sizeof(float4) * i,
- float4{ dir.x, dir.y, dir.z, normalBias * texelSizeWorldSpace });
+ u.setUniform(offsetof(ShadowUib, directionShadowBias[i]),
+ float4{ dir, normalBias * texelSizeWorldSpace });
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
}
@@ -569,8 +551,10 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
const uint8_t layersNeeded = layer;
- // Only generate mipmaps for VSM when anisotropy is enabled.
- const bool useMipmapping = view.hasVsm() && view.getVsmShadowOptions().anisotropy > 0;
+ // Generate mipmaps for VSM when anisotropy is enabled or when requested
+ auto const& vsmShadowOptions = view.getVsmShadowOptions();
+ const bool useMipmapping = view.hasVsm() &&
+ ((vsmShadowOptions.anisotropy > 0) || vsmShadowOptions.mipmapping);
uint8_t mipLevels = 1u;
if (useMipmapping) {
@@ -587,11 +571,11 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
};
}
-
-ShadowMapManager::CascadeSplits::CascadeSplits(Params p) : mSplitCount(p.cascadeCount + 1) {
+ShadowMapManager::CascadeSplits::CascadeSplits(Params const& params) noexcept
+ : mSplitCount(params.cascadeCount + 1) {
for (size_t s = 0; s < mSplitCount; s++) {
- mSplitsWs[s] = p.near + (p.far - p.near) * p.splitPositions[s];
- mSplitsCs[s] = mat4f::project(p.proj, float3(0.0f, 0.0f, mSplitsWs[s])).z;
+ mSplitsWs[s] = params.near + (params.far - params.near) * params.splitPositions[s];
+ mSplitsCs[s] = mat4f::project(params.proj, float3(0.0f, 0.0f, mSplitsWs[s])).z;
}
}
diff --git a/filament/src/View.cpp b/filament/src/View.cpp
index 95d84bbd0a..364459e21e 100644
--- a/filament/src/View.cpp
+++ b/filament/src/View.cpp
@@ -690,14 +690,43 @@ void FView::prepareStructure(backend::Handle structure) cons
}
void FView::prepareShadow(backend::Handle texture) const noexcept {
- mShadowMapManager.prepareShadow(texture, *this);
+ uint8_t anisotropy = 0;
+ SamplerMinFilter filterMin = SamplerMinFilter::LINEAR;
+ if (hasVsm()) {
+ auto const& vsmShadowOptions = mVsmShadowOptions;
+ anisotropy = vsmShadowOptions.anisotropy;
+ if (anisotropy > 0 || vsmShadowOptions.mipmapping) {
+ filterMin = SamplerMinFilter::LINEAR_MIPMAP_LINEAR;
+ }
+ }
+
+ mPerViewSb.setSampler(PerViewSib::SHADOW_MAP, {
+ texture, {
+ .filterMag = SamplerMagFilter::LINEAR,
+ .filterMin = filterMin,
+ .anisotropyLog2 = anisotropy,
+ .compareMode = SamplerCompareMode::COMPARE_TO_TEXTURE, // ignored for VSM
+ .compareFunc = SamplerCompareFunc::GE // ignored for VSM
+ }});
+}
+
+void FView::prepareShadowMap() const noexcept {
+ auto const& vsmShadowOptions = mVsmShadowOptions;
+ // fp16: max 5.54f, fp32: max 42.0
+ float vsmExponent = vsmShadowOptions.exponent;
+ float vsmDepthScale = vsmShadowOptions.minVarianceScale * 0.01f * vsmExponent;
+ float vsmLightBleedReduction = vsmShadowOptions.lightBleedReduction;
+ mPerViewUb.setUniform(offsetof(PerViewUib, vsmExponent), vsmExponent);
+ mPerViewUb.setUniform(offsetof(PerViewUib, vsmDepthScale), vsmDepthScale);
+ mPerViewUb.setUniform(offsetof(PerViewUib, vsmLightBleedReduction), vsmLightBleedReduction);
}
void FView::cleanupRenderPasses() const noexcept {
auto& samplerGroup = mPerViewSb;
- samplerGroup.setSampler(PerViewSib::SSAO, {}, {});
- samplerGroup.setSampler(PerViewSib::SSR, {}, {});
- samplerGroup.setSampler(PerViewSib::STRUCTURE, {}, {});
+ samplerGroup.clearSampler(PerViewSib::SSAO);
+ samplerGroup.clearSampler(PerViewSib::SSR);
+ samplerGroup.clearSampler(PerViewSib::STRUCTURE);
+ samplerGroup.clearSampler(PerViewSib::SHADOW_MAP);
}
void FView::froxelize(FEngine& engine) const noexcept {
diff --git a/filament/src/details/ShadowMap.h b/filament/src/details/ShadowMap.h
index d1bb34cdb2..8cf8cdcdf6 100644
--- a/filament/src/details/ShadowMap.h
+++ b/filament/src/details/ShadowMap.h
@@ -40,7 +40,7 @@ public:
explicit ShadowMap(FEngine& engine) noexcept;
~ShadowMap();
- struct ShadowMapLayout {
+ struct ShadowMapInfo {
// the smallest increment in depth precision
// e.g., for 16 bit depth textures, is this 1 / (2^16)
float zResolution = 0.0f;
@@ -55,38 +55,45 @@ public:
// the dimension of the actual shadow map, taking into account the 1 texel border
// e.g., for a texture dimension of 512, shadowDimension would be 510
size_t shadowDimension = 0;
+
+ // whether we're using vsm
+ bool vsm = false;
};
- struct CascadeParameters {
+ struct SceneInfo {
// The near and far planes, in clip space, to use for this shadow map
math::float2 csNearFar = { -1.0f, 1.0f };
// The following fields are set by computeSceneCascadeParams.
- // Light-space near/far planes for the scene.
+ // light's near/far expressed in light-space, calculated from the scene's content
+ // assuming the light is at the origin.
math::float2 lsNearFar;
- // View-space near/far planes for the scene.
+ // Viewing camera's near/far expressed in view-space, calculated from the scene's content
math::float2 vsNearFar;
+ // World-space shadow-casters volume
Aabb wsShadowCastersVolume;
- Aabb wsShadowReceiversVolume;
- // Position of the directional light in world space.
- math::float3 wsLightPosition;
+ // World-space shadow-receivers volume
+ Aabb wsShadowReceiversVolume;
};
+ static math::mat4f getLightViewMatrix(
+ math::float3 position, math::float3 direction) noexcept;
+
// Call once per frame to populate the CascadeParameters struct, then pass to update().
// This computes values constant across all cascades.
- static void computeSceneCascadeParams(const FScene::LightSoa& lightData, size_t index,
- FView const& view, filament::CameraInfo const& camera, uint8_t visibleLayers,
- CascadeParameters& cascadeParams);
+ static void computeSceneInfo(math::float3 dir,
+ FScene const& scene, filament::CameraInfo const& camera, uint8_t visibleLayers,
+ SceneInfo& sceneInfo);
// Call once per frame if the light, scene (or visible layers) or camera changes.
// This computes the light's camera.
- void update(const FScene::LightSoa& lightData, size_t index, FScene const* scene,
- filament::CameraInfo const& camera, uint8_t visibleLayers,
- ShadowMapLayout layout, const CascadeParameters& cascadeParams) noexcept;
+ void update(const FScene::LightSoa& lightData, size_t index,
+ filament::CameraInfo const& camera,
+ const ShadowMapInfo& shadowMapInfo, const SceneInfo& cascadeParams) noexcept;
void render(backend::DriverApi& driver, utils::Range const& range, RenderPass& pass,
FView& view) noexcept;
@@ -98,10 +105,6 @@ public:
// Valid after calling update().
math::mat4f const& getLightSpaceMatrix() const noexcept { return mLightSpace; }
- // Computes the transform to use in the shader to access the shadow map for VSM.
- // Valid after calling update().
- math::mat4f const& getLightSpaceMatrixVsm() const noexcept { return mLightSpaceVsm; }
-
// return the size of a texel in world space (pre-warping)
float getTexelSizeWorldSpace() const noexcept { return mTexelSizeWs; }
@@ -142,9 +145,9 @@ private:
using FrustumBoxIntersection = std::array;
void computeShadowCameraDirectional(
- math::float3 const& direction, FScene const* scene,
+ math::float3 const& direction,
CameraInfo const& camera, FLightManager::ShadowParams const& params,
- uint8_t visibleLayers, CascadeParameters cascadeParams) noexcept;
+ SceneInfo cascadeParams) noexcept;
void computeShadowCameraSpot(math::float3 const& position, math::float3 const& dir,
float outerConeAngle, float radius, CameraInfo const& camera,
FLightManager::ShadowParams const& params) noexcept;
@@ -232,11 +235,10 @@ private:
FCamera* mCamera = nullptr;
FCamera* mDebugCamera = nullptr;
math::mat4f mLightSpace;
- math::mat4f mLightSpaceVsm;
float mTexelSizeWs = 0.0f;
// set-up in update()
- ShadowMapLayout mShadowMapLayout;
+ ShadowMapInfo mShadowMapInfo;
bool mHasVisibleShadows = false;
backend::PolygonOffset mPolygonOffset{};
diff --git a/filament/src/details/ShadowMapManager.h b/filament/src/details/ShadowMapManager.h
index 95aebc867f..af0f3ed5b4 100644
--- a/filament/src/details/ShadowMapManager.h
+++ b/filament/src/details/ShadowMapManager.h
@@ -74,11 +74,8 @@ public:
void render(FrameGraph& fg, FEngine& engine, FView& view, backend::DriverApi& driver,
RenderPass& pass) noexcept;
- // Prepares the shadow sampler.
- void prepareShadow(backend::Handle texture, FView const& view)
- const noexcept;
-
const ShadowMap* getCascadeShadowMap(size_t c) const noexcept {
+ assert_invariant(c < mCascadeShadowMapCache.size());
return mCascadeShadowMapCache[c].get();
}
@@ -108,14 +105,14 @@ private:
class ShadowMapEntry {
public:
ShadowMapEntry() = default;
- ShadowMapEntry(ShadowMap* shadowMap, const size_t light) :
+ ShadowMapEntry(ShadowMap* shadowMap, size_t light) :
mShadowMap(shadowMap),
- mLightIndex(light),
- mLayout({}) {}
+ mLightIndex(light) {
+ }
explicit operator bool() const { return mShadowMap != nullptr; }
- ShadowMap* getShadowMap() const { return mShadowMap; }
+ ShadowMap& getShadowMap() const { return *mShadowMap; }
size_t getLightIndex() const { return mLightIndex; }
const ShadowLayout& getLayout() const { return mLayout; }
bool hasVisibleShadows() const { return mHasVisibleShadows; }
@@ -125,8 +122,8 @@ private:
private:
ShadowMap* mShadowMap = nullptr;
- size_t mLightIndex = 0;
ShadowLayout mLayout = {};
+ size_t mLightIndex = 0; // TODO: does this need to be size_t?
bool mHasVisibleShadows = false;
};
@@ -135,7 +132,7 @@ private:
constexpr static size_t SPLIT_COUNT = CONFIG_MAX_SHADOW_CASCADES + 1;
struct Params {
- math::mat4f proj = {};
+ math::mat4f proj;
float near = 0.0f;
float far = 0.0f;
size_t cascadeCount = 1;
@@ -150,8 +147,8 @@ private:
}
};
- CascadeSplits() : CascadeSplits(Params {}) {}
- explicit CascadeSplits(Params p);
+ CascadeSplits() noexcept : CascadeSplits(Params{}) {}
+ explicit CascadeSplits(Params const& params) noexcept;
// Split positions in world-space.
const float* beginWs() const { return mSplitsWs; }
@@ -165,9 +162,10 @@ private:
float mSplitsWs[SPLIT_COUNT];
float mSplitsCs[SPLIT_COUNT];
size_t mSplitCount;
+ };
- } mCascadeSplits;
CascadeSplits::Params mCascadeSplitParams;
+ CascadeSplits mCascadeSplits;
// 16-bits seems enough.
// TODO: make it an option.
@@ -183,8 +181,6 @@ private:
utils::FixedCapacityVector::with_capacity(
CONFIG_MAX_SHADOW_CASTING_SPOTS) };
- backend::RenderPassParams mRenderPassParams;
-
std::array, CONFIG_MAX_SHADOW_CASCADES> mCascadeShadowMapCache;
std::array, CONFIG_MAX_SHADOW_CASTING_SPOTS> mSpotShadowMapCache;
};
diff --git a/filament/src/details/View.h b/filament/src/details/View.h
index d6ff400a9f..9764dbc4d6 100644
--- a/filament/src/details/View.h
+++ b/filament/src/details/View.h
@@ -163,10 +163,13 @@ public:
FScene::RenderableSoa& renderableData, FScene::LightSoa& lightData) noexcept;
void prepareLighting(FEngine& engine, FEngine::DriverApi& driver,
ArenaScope& arena, Viewport const& viewport) noexcept;
+
void prepareSSAO(backend::Handle ssao) const noexcept;
void prepareSSR(backend::Handle ssr, float refractionLodOffset) const noexcept;
void prepareStructure(backend::Handle structure) const noexcept;
void prepareShadow(backend::Handle structure) const noexcept;
+ void prepareShadowMap() const noexcept;
+
void cleanupRenderPasses() const noexcept;
void froxelize(FEngine& engine) const noexcept;
void commitUniforms(backend::DriverApi& driver) const noexcept;
@@ -497,7 +500,7 @@ private:
bool mHasPostProcessPass = true;
AmbientOcclusionOptions mAmbientOcclusionOptions{};
ShadowType mShadowType = ShadowType::PCF;
- VsmShadowOptions mVsmShadowOptions = {};
+ VsmShadowOptions mVsmShadowOptions = {}; // FIXME: this should probably be per-light
BloomOptions mBloomOptions;
FogOptions mFogOptions;
DepthOfFieldOptions mDepthOfFieldOptions;
diff --git a/libs/filabridge/include/private/filament/UibGenerator.h b/libs/filabridge/include/private/filament/UibGenerator.h
index c79c231a0b..aefa7845a4 100644
--- a/libs/filabridge/include/private/filament/UibGenerator.h
+++ b/libs/filabridge/include/private/filament/UibGenerator.h
@@ -72,8 +72,7 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
filament::math::float4 sun; // cos(sunAngle), sin(sunAngle), 1/(sunAngle*HALO_SIZE-sunAngle), HALO_EXP
- filament::math::float3 lightPosition;
- uint32_t padding;
+ filament::math::float4 padding0;
filament::math::float3 lightDirection;
uint32_t fParamsX; // stride-x
@@ -131,8 +130,13 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
math::float2 clipControl;
math::float2 padding1;
+ float vsmExponent;
+ float vsmDepthScale;
+ float vsmLightBleedReduction;
+ float vsmReserved0;
+
// bring PerViewUib to 2 KiB
- filament::math::float4 padding2[60];
+ filament::math::float4 padding2[59];
};
// 2 KiB == 128 float4s
diff --git a/libs/filabridge/src/UibGenerator.cpp b/libs/filabridge/src/UibGenerator.cpp
index b74c75d599..0a317e9e85 100644
--- a/libs/filabridge/src/UibGenerator.cpp
+++ b/libs/filabridge/src/UibGenerator.cpp
@@ -56,8 +56,7 @@ UniformInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
// directional light
.add("lightColorIntensity", 1, UniformInterfaceBlock::Type::FLOAT4)
.add("sun", 1, UniformInterfaceBlock::Type::FLOAT4)
- .add("lightPosition", 1, UniformInterfaceBlock::Type::FLOAT3)
- .add("padding0", 1, UniformInterfaceBlock::Type::UINT)
+ .add("padding0", 1, UniformInterfaceBlock::Type::FLOAT4)
.add("lightDirection", 1, UniformInterfaceBlock::Type::FLOAT3)
.add("fParamsX", 1, UniformInterfaceBlock::Type::UINT)
// shadow
@@ -109,8 +108,13 @@ UniformInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
.add("clipControl", 1, UniformInterfaceBlock::Type::FLOAT2)
.add("padding1", 1, UniformInterfaceBlock::Type::FLOAT2)
+ .add("vsmExponent", 1, UniformInterfaceBlock::Type::FLOAT)
+ .add("vsmDepthScale", 1, UniformInterfaceBlock::Type::FLOAT, Precision::HIGH)
+ .add("vsmLightBleedReduction", 1, UniformInterfaceBlock::Type::FLOAT)
+ .add("vsmReserved0", 1, UniformInterfaceBlock::Type::FLOAT)
+
// bring PerViewUib to 2 KiB
- .add("padding2", 60, UniformInterfaceBlock::Type::FLOAT4)
+ .add("padding2", 59, UniformInterfaceBlock::Type::FLOAT4)
.build();
return uib;
}
diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp
index d80281909f..a17865ef65 100644
--- a/libs/viewer/src/Settings.cpp
+++ b/libs/viewer/src/Settings.cpp
@@ -222,6 +222,14 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk,
CHECK_KEY(tok);
if (0 == compare(tok, jsonChunk, "anisotropy")) {
i = parse(tokens, i + 1, jsonChunk, &out->anisotropy);
+ } else if (0 == compare(tok, jsonChunk, "mipmapping")) {
+ i = parse(tokens, i + 1, jsonChunk, &out->mipmapping);
+ } else if (0 == compare(tok, jsonChunk, "exponent")) {
+ i = parse(tokens, i + 1, jsonChunk, &out->exponent);
+ } else if (0 == compare(tok, jsonChunk, "minVarianceScale")) {
+ i = parse(tokens, i + 1, jsonChunk, &out->minVarianceScale);
+ } else if (0 == compare(tok, jsonChunk, "lightBleedReduction")) {
+ i = parse(tokens, i + 1, jsonChunk, &out->lightBleedReduction);
} else {
slog.w << "Invalid shadow options key: '" << STR(tok, jsonChunk) << "'" << io::endl;
i = parse(tokens, i + 1);
@@ -1275,7 +1283,11 @@ static std::ostream& operator<<(std::ostream& out, const DynamicLightingSettings
static std::ostream& operator<<(std::ostream& out, const VsmShadowOptions& in) {
return out << "{\n"
- << "\"anisotropy\": " << int(in.anisotropy) << "\n"
+ << "\"anisotropy\": " << int(in.anisotropy) << ",\n"
+ << "\"mipmapping\": " << int(in.mipmapping) << ",\n"
+ << "\"exponent\": " << int(in.exponent) << ",\n"
+ << "\"minVarianceScale\": " << int(in.minVarianceScale) << ",\n"
+ << "\"lightBleedReduction\": " << int(in.lightBleedReduction) << "\n"
<< "}";
}
diff --git a/libs/viewer/src/SimpleViewer.cpp b/libs/viewer/src/SimpleViewer.cpp
index 6abdac3551..eacc9395c9 100644
--- a/libs/viewer/src/SimpleViewer.cpp
+++ b/libs/viewer/src/SimpleViewer.cpp
@@ -669,6 +669,10 @@ void SimpleViewer::updateUserInterface() {
ImGuiExt::DirectionWidget("Sun direction", light.sunlightDirection.v);
ImGui::Checkbox("Enable sunlight", &light.enableSunlight);
ImGui::Checkbox("Enable shadows", &light.enableShadows);
+ int mapSize = light.shadowOptions.mapSize;
+ ImGui::SliderInt("Shadow map size", &mapSize, 32, 1024);
+ light.shadowOptions.mapSize = mapSize;
+
bool enableVsm = mSettings.view.shadowType == ShadowType::VSM;
ImGui::Checkbox("Enable VSM", &enableVsm);
@@ -683,6 +687,12 @@ void SimpleViewer::updateUserInterface() {
snprintf(label, 32, "%d", 1 << vsmAnisotropy);
ImGui::SliderInt("VSM anisotropy", &vsmAnisotropy, 0, 3, label);
mSettings.view.vsmShadowOptions.anisotropy = vsmAnisotropy;
+ ImGui::Checkbox("VSM mipmapping", &mSettings.view.vsmShadowOptions.mipmapping);
+
+ // These are not very useful in practice (defaults are good), but we keep them here for debugging
+ //ImGui::SliderFloat("VSM exponent", &mSettings.view.vsmShadowOptions.exponent, 0.0, 6.0f);
+ //ImGui::SliderFloat("VSM Light bleed", &mSettings.view.vsmShadowOptions.lightBleedReduction, 0.0, 1.0f);
+ //ImGui::SliderFloat("VSM min variance scale", &mSettings.view.vsmShadowOptions.minVarianceScale, 0.0, 10.0f);
int shadowCascades = light.shadowOptions.shadowCascades;
ImGui::SliderInt("Cascades", &shadowCascades, 1, 4);
diff --git a/shaders/src/common_shadowing.fs b/shaders/src/common_shadowing.fs
index 44f20265fc..200810bc19 100644
--- a/shaders/src/common_shadowing.fs
+++ b/shaders/src/common_shadowing.fs
@@ -9,12 +9,17 @@
* shadowing artifacts such as "acne". To achieve this, the world space
* normal at the point must also be passed to this function.
*/
-vec4 computeLightSpacePosition(const vec3 p, const vec3 n, const vec3 l,
- const float b, const mat4 lightFromWorldMatrix) {
+vec4 computeLightSpacePosition(const highp vec3 p, const highp vec3 n, const highp vec3 l,
+ const float b, const highp mat4 lightFromWorldMatrix) {
+#if defined(HAS_VSM)
+ // VSM don't apply the shadow bias
+ vec4 lightSpacePosition = (lightFromWorldMatrix * vec4(p, 1.0));
+#else
float NoL = saturate(dot(n, l));
float sinTheta = sqrt(1.0 - NoL * NoL);
vec3 offsetPosition = p + n * (sinTheta * b);
vec4 lightSpacePosition = (lightFromWorldMatrix * vec4(offsetPosition, 1.0));
+#endif
return lightSpacePosition;
}
#endif
diff --git a/shaders/src/depth_main.fs b/shaders/src/depth_main.fs
index 8ac7f75d1d..a9c6010d3d 100644
--- a/shaders/src/depth_main.fs
+++ b/shaders/src/depth_main.fs
@@ -21,20 +21,30 @@ void main() {
#if defined(HAS_VSM)
// For VSM, we use the linear light space Z coordinate as the depth metric, which works for both
// directional and spot lights.
- // We negate it, because we're using a right-handed coordinate system (-Z points forward).
- highp float depth = -mulMat4x4Float3(frameUniforms.viewFromWorldMatrix, vertex_worldPosition).z;
+ // The value is guaranteed to be between [0, -zfar] by construction of viewFromWorldMatrix,
+ // (see ShadowMap.cpp).
+ highp float z = (frameUniforms.viewFromWorldMatrix * vec4(vertex_worldPosition, 1.0)).z;
- // Scale by cameraFar to help prevent a floating point overflow below when squaring the depth.
- depth /= abs(frameUniforms.cameraFar);
+ // rescale the depth between [0, 1]
+ highp float depth = -z / abs(frameUniforms.cameraFar);
+ // We use positive only EVSM which helps a lot with light bleeding.
+ depth = depth * 2.0 - 1.0;
+ depth = exp(frameUniforms.vsmExponent * depth);
+
+ // computes the moments
+ // See GPU Gems 3
+ // https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps
+ highp vec2 moments;
+
+ // the first moment is just the depth (average)
+ moments.x = depth;
+
+ // compute the 2nd moment over the pixel extents.
highp float dx = dFdx(depth);
highp float dy = dFdy(depth);
+ moments.y = depth * depth + 0.25 * (dx * dx + dy * dy);
- // Output the first and second depth moments.
- // The first moment is mean depth.
- // The second moment is mean depth squared.
- // These values are retrieved when sampling the shadow map to compute variance.
- highp float bias = 0.25 * (dx * dx + dy * dy);
- fragColor = vec4(depth, depth * depth + bias, 0.0, 0.0);
+ fragColor = vec4(moments, 0.0, 0.0);
#endif
}
diff --git a/shaders/src/getters.fs b/shaders/src/getters.fs
index d4d660fcf8..a9c20449f9 100644
--- a/shaders/src/getters.fs
+++ b/shaders/src/getters.fs
@@ -141,6 +141,7 @@ highp vec3 getCascadeLightSpacePosition(uint cascade) {
// This branch will be coherent (mostly) for neighboring fragments, and it's worth avoiding
// the matrix multiply inside computeLightSpacePosition.
if (cascade == 0u) {
+ // Note: this branch may cause issues with derivatives
return getLightSpacePosition();
}
diff --git a/shaders/src/light_directional.fs b/shaders/src/light_directional.fs
index b1a3d4cb9a..e6ed9ae9e4 100644
--- a/shaders/src/light_directional.fs
+++ b/shaders/src/light_directional.fs
@@ -45,11 +45,7 @@ void evaluateDirectionalLight(const MaterialInputs material,
bool hasDirectionalShadows = bool(frameUniforms.directionalShadows & 1u);
if (hasDirectionalShadows && cascadeHasVisibleShadows) {
uint layer = cascade;
-#if defined(HAS_VSM)
- visibility = shadowVsm(light_shadowMap, layer, getCascadeLightSpacePosition(cascade));
-#else
visibility = shadow(light_shadowMap, layer, getCascadeLightSpacePosition(cascade));
-#endif
}
if ((frameUniforms.directionalShadows & 0x2u) != 0u && visibility > 0.0) {
if (objectUniforms.screenSpaceContactShadows != 0u) {
diff --git a/shaders/src/light_punctual.fs b/shaders/src/light_punctual.fs
index b166fd1a4e..410be4935a 100644
--- a/shaders/src/light_punctual.fs
+++ b/shaders/src/light_punctual.fs
@@ -179,13 +179,8 @@ void evaluatePunctualLights(const MaterialInputs material,
#if defined(HAS_SHADOWING)
if (light.NoL > 0.0){
if (light.castsShadows) {
-#if defined(HAS_VSM)
- visibility = shadowVsm(light_shadowMap, light.shadowLayer,
- getSpotLightSpacePosition(light.shadowIndex));
-#else
visibility = shadow(light_shadowMap, light.shadowLayer,
getSpotLightSpacePosition(light.shadowIndex));
-#endif
}
if (light.contactShadows && visibility > 0.0) {
if (objectUniforms.screenSpaceContactShadows != 0u) {
diff --git a/shaders/src/shading_unlit.fs b/shaders/src/shading_unlit.fs
index 2e87a62515..e53109c3fb 100644
--- a/shaders/src/shading_unlit.fs
+++ b/shaders/src/shading_unlit.fs
@@ -45,11 +45,7 @@ vec4 evaluateMaterial(const MaterialInputs material) {
if ((frameUniforms.directionalShadows & 1u) != 0u) {
uint cascade = getShadowCascade();
uint layer = cascade;
-#if defined(HAS_VSM)
- visibility = shadowVsm(light_shadowMap, layer, getCascadeLightSpacePosition(cascade));
-#else
visibility = shadow(light_shadowMap, layer, getCascadeLightSpacePosition(cascade));
-#endif
}
if ((frameUniforms.directionalShadows & 0x2u) != 0u && visibility > 0.0) {
if (objectUniforms.screenSpaceContactShadows != 0u) {
diff --git a/shaders/src/shadowing.fs b/shaders/src/shadowing.fs
index b883493a24..c8da3586e1 100644
--- a/shaders/src/shadowing.fs
+++ b/shaders/src/shadowing.fs
@@ -304,23 +304,26 @@ float screenSpaceContactShadow(vec3 lightDirection) {
// VSM
//------------------------------------------------------------------------------
-highp float linstep(const highp float a, const highp float b, const highp float v) {
- return clamp((v - a) / (b - a), 0.0, 1.0);
+float linstep(const float min, const float max, const float v) {
+ // we could use smoothstep() too
+ return clamp((v - min) / (max - min), 0.0, 1.0);
}
-highp float reduceLightBleed(const highp float pMax, const highp float amount) {
+float reduceLightBleed(const float pMax, const float amount) {
+ // Remove the [0, amount] tail and linearly rescale (amount, 1].
return linstep(amount, 1.0, pMax);
}
-highp float chebyshevUpperBound(const highp vec2 moments, const highp float mean,
- const highp float minVariance, const highp float lightBleedReduction) {
+float chebyshevUpperBound(const highp vec2 moments, const highp float mean,
+ const highp float minVariance, const float lightBleedReduction) {
// Donnelly and Lauritzen 2006, "Variance Shadow Maps"
highp float variance = moments.y - (moments.x * moments.x);
variance = max(variance, minVariance);
highp float d = mean - moments.x;
- highp float pMax = variance / (variance + d * d);
+ float pMax = variance / (variance + d * d);
+
pMax = reduceLightBleed(pMax, lightBleedReduction);
return mean <= moments.x ? 1.0 : pMax;
@@ -335,7 +338,10 @@ highp float chebyshevUpperBound(const highp vec2 moments, const highp float mean
* space. The output is a filtered visibility factor that can be used to multiply
* the light intensity.
*/
-float shadow(const mediump sampler2DArrayShadow shadowMap, const uint layer, vec3 shadowPosition) {
+
+// PCF sampling
+float shadow(const mediump sampler2DArrayShadow shadowMap,
+ const uint layer, vec3 shadowPosition) {
vec2 size = vec2(textureSize(shadowMap, 0));
// note: shadowPosition.z is in the [1, 0] range (reversed Z)
#if SHADOW_SAMPLING_METHOD == SHADOW_SAMPLING_PCF_HARD
@@ -349,20 +355,23 @@ float shadow(const mediump sampler2DArrayShadow shadowMap, const uint layer, vec
#endif
}
-highp float shadowVsm(const highp sampler2DArray shadowMap, const uint layer,
- const highp vec3 shadowPosition) {
+// VSM sampling
+float shadow(const mediump sampler2DArray shadowMap,
+ const uint layer, const highp vec3 shadowPosition) {
+ // note: shadowPosition.z is in linear light space normalized to [0, 1]
+ // see: ShadowMap::computeVsmLightSpaceMatrix() in ShadowMap.cpp
+ // see: computeLightSpacePosition() in common_shadowing.fs
+
+ // Read the shadow map with all available filtering
highp vec2 moments = texture(shadowMap, vec3(shadowPosition.xy, layer)).xy;
highp float depth = shadowPosition.z;
- // depth must be clamped to support floating-point depth formats. This is to avoid comparing a
- // value from the depth texture (which is never greater than 1.0) with a greater-than-one
- // comparison value (which is possible with floating-point formats).
- depth = min(depth, 1.0f);
+ // EVSM depth warping
+ depth = depth * 2.0 - 1.0;
+ depth = exp(frameUniforms.vsmExponent * depth);
- // TODO: bias and lightBleedReduction should be uniforms
- const float bias = 0.01;
- const float lightBleedReduction = 0.2;
-
- const float minVariance = bias * 0.001;
+ highp float depthScale = frameUniforms.vsmDepthScale * depth;
+ highp float minVariance = depthScale * depthScale;
+ float lightBleedReduction = frameUniforms.vsmLightBleedReduction;
return chebyshevUpperBound(moments, depth, minVariance, lightBleedReduction);
}