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); }