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 be4ab84490..31c3c6cadb 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 @@ -2121,7 +2121,7 @@ public class View { */ public boolean highPrecision = false; /** - * VSM minimum variance scale, must be positive. + * @deprecated has no effect. */ public float minVarianceScale = 0.5f; /** diff --git a/filament/include/filament/LightManager.h b/filament/include/filament/LightManager.h index 22d663f2ea..a50d06693f 100644 --- a/filament/include/filament/LightManager.h +++ b/filament/include/filament/LightManager.h @@ -372,7 +372,7 @@ public: } vsm; /** - * Light bulb radius used for soft shadows. Currently this is only used when DPCF or PCSS is + * Light bulb radius used for soft shadows. Currently, this is only used when DPCF or PCSS is * enabled. (2cm by default). */ float shadowBulbRadius = 0.02f; diff --git a/filament/include/filament/Options.h b/filament/include/filament/Options.h index 336eb13463..0a86a0d403 100644 --- a/filament/include/filament/Options.h +++ b/filament/include/filament/Options.h @@ -735,7 +735,7 @@ struct VsmShadowOptions { bool highPrecision = false; /** - * VSM minimum variance scale, must be positive. + * @deprecated has no effect. */ float minVarianceScale = 0.5f; diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 8ab759a4b0..5d5a9a4b42 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -3809,15 +3809,13 @@ FrameGraphId PostProcessManager::vsmMipmapPass(FrameGraph& fg auto const& inDesc = resources.getDescriptor(data.in); auto width = inDesc.width; assert_invariant(width == inDesc.height); - int const dim = width >> (level + 1); + uint32_t const dim = std::max(1u, width >> (level + 1)); auto& material = getPostProcessMaterial("vsmMipmap"); FMaterial const* const ma = material.getMaterial(mEngine, driver); - // When generating shadow map mip levels, we want to preserve the 1 texel border. - // (note clearing never respects the scissor in Filament) auto const pipeline = getPipelineState(ma); - backend::Viewport const scissor = { 1u, 1u, dim - 2u, dim - 2u }; + backend::Viewport const scissor = { 0, 0, dim, dim }; FMaterialInstance* const mi = getMaterialInstance(ma); mi->setParameter("color", in, SamplerParams{ diff --git a/filament/src/ShadowMap.cpp b/filament/src/ShadowMap.cpp index eb81b62eee..7e6a1c5b39 100644 --- a/filament/src/ShadowMap.cpp +++ b/filament/src/ShadowMap.cpp @@ -62,6 +62,8 @@ ShadowMap::ShadowMap(FEngine& engine) noexcept : mPerShadowMapUniforms(engine), mShadowType(ShadowType::DIRECTIONAL), mHasVisibleShadows(false), + mVsm(false), + mReservedBit(false), mFace(0) { Entity entities[2]; engine.getEntityManager().create(2, entities); @@ -85,7 +87,7 @@ void ShadowMap::terminate(FEngine& engine) { ShadowMap::~ShadowMap() = default; -void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType, +void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType, bool const vsm, uint16_t const shadowIndex, uint8_t const face, LightManager::ShadowOptions const* options) { mLightIndex = lightIndex; @@ -93,6 +95,7 @@ void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType, mOptions = options; mShadowType = shadowType; mFace = face; + mVsm = vsm; } mat4f ShadowMap::getDirectionalLightViewMatrix(float3 direction, float3 up, @@ -1181,9 +1184,8 @@ float2 ShadowMap::texelSizeWorldSpace(mat4f const& S, uint16_t const shadowDimen return s; } -template -void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, - Casters casters, Receivers receivers) noexcept { +template +void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, Visitor visitor) noexcept { FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT); using State = FRenderableManager::Visibility; @@ -1196,14 +1198,11 @@ void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, size_t const c = soa.size(); for (size_t i = 0; i < c; i++) { if (layers[i] & visibleLayers) { - const Aabb aabb{ worldAABBCenter[i] - worldAABBExtent[i], - worldAABBCenter[i] + worldAABBExtent[i] }; - if (visibility[i].castShadows) { - casters(aabb, visibleMasks[i]); - } - if (visibility[i].receiveShadows) { - receivers(aabb, visibleMasks[i]); - } + Aabb const aabb{ + worldAABBCenter[i] - worldAABBExtent[i], + worldAABBCenter[i] + worldAABBExtent[i] + }; + visitor(aabb, visibleMasks[i], visibility[i]); } } } @@ -1222,13 +1221,15 @@ ShadowMap::SceneInfo::SceneInfo( wsShadowCastersVolume = {}; wsShadowReceiversVolume = {}; visitScene(scene, visibleLayers, - [&](Aabb caster, Culler::result_type) { - wsShadowCastersVolume.min = min(wsShadowCastersVolume.min, caster.min); - wsShadowCastersVolume.max = max(wsShadowCastersVolume.max, caster.max); - }, - [&](Aabb receiver, Culler::result_type) { - wsShadowReceiversVolume.min = min(wsShadowReceiversVolume.min, receiver.min); - wsShadowReceiversVolume.max = max(wsShadowReceiversVolume.max, receiver.max); + [this](Aabb const& aabb, Culler::result_type, FRenderableManager::Visibility const visibility) { + if (visibility.castShadows) { + wsShadowCastersVolume.min = min(wsShadowCastersVolume.min, aabb.min); + wsShadowCastersVolume.max = max(wsShadowCastersVolume.max, aabb.max); + } + if (visibility.receiveShadows) { + wsShadowReceiversVolume.min = min(wsShadowReceiversVolume.min, aabb.min); + wsShadowReceiversVolume.max = max(wsShadowReceiversVolume.max, aabb.max); + } } ); } @@ -1242,18 +1243,20 @@ void ShadowMap::updateSceneInfoDirectional(mat4f const& Mv, FScene const& scene, sceneInfo.lsCastersNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; sceneInfo.lsReceiversNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; visitScene(scene, sceneInfo.visibleLayers, - [&](Aabb caster, Culler::result_type) { - auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster); - sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z); - sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z); - }, - [&](Aabb receiver, Culler::result_type const vis) { - // account only for objects that are visible by the camera - auto mask = 1u << VISIBLE_RENDERABLE_BIT; - if ((vis & mask) == mask) { - auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, receiver); - sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z); - sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z); + [&](Aabb const& aabb, Culler::result_type const vis, FRenderableManager::Visibility const visibility) { + if (visibility.castShadows) { + auto const r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb); + sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z); + sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z); + } + if (visibility.castShadows) { + // account only for objects that are visible by the camera + constexpr auto mask = 1u << VISIBLE_RENDERABLE_BIT; + if ((vis & mask) == mask) { + auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb); + sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z); + sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z); + } } } ); @@ -1268,15 +1271,15 @@ void ShadowMap::updateSceneInfoSpot(mat4f const& Mv, FScene const& scene, sceneInfo.lsCastersNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; // account only for objects that are visible by both the camera and the light visitScene(scene, sceneInfo.visibleLayers, - [&](Aabb caster, Culler::result_type const vis) { - auto mask = VISIBLE_DYN_SHADOW_RENDERABLE; - if ((vis & mask) == mask) { - auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster); - sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near - sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far + [&](Aabb const& aabb, Culler::result_type const vis, FRenderableManager::Visibility const visibility) { + if (visibility.castShadows) { + constexpr auto mask = VISIBLE_DYN_SHADOW_RENDERABLE; + if ((vis & mask) == mask) { + auto const r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb); + sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near + sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far + } } - }, - [&](Aabb receiver, Culler::result_type) { } ); } @@ -1286,47 +1289,56 @@ void ShadowMap::setAllocation(uint8_t const layer, backend::Viewport viewport) n } backend::Viewport ShadowMap::getViewport() const noexcept { - // We set a viewport with a 1-texel border for when we index outside the texture. - // This happens only for directional lights when "focus shadow casters" is used, - // or when shadowFar is smaller than the camera far. - // For spot- and point-lights we also use a 1-texel border, so that bilinear filtering - // can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp). + // This is used for calculating the light space; in particular, if the shadowmap is in a 2D atlas, this + // returns the valid area of the shadowmap. By definition all values must be integer. + + // We set a viewport with a 1-texel border for when we index outside the texture, which should only happen for + // directional lights when "focus shadow casters" is used, or when shadowFar is smaller than the camera far. + // For directional lights, the border is filed with "fully lit". + // + // For point-lights, we also use a 1-texel border, but for a different reason; the border is filled with data + // so that bilinear (PCF) filtering works properly. + // + // Spot-light a treated like point lights (the border will be filled with "fully-lit" anyways) + const uint32_t dim = mOptions->mapSize; const uint16_t border = 1u; return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border }; } backend::Viewport ShadowMap::getScissor() const noexcept { - // We set a viewport with a 1-texel border for when we index outside the texture. - // This happens only for directional lights when "focus shadow casters" is used, - // or when shadowFar is smaller than the camera far. - // For spot- and point-lights we also use a 1-texel border, so that bilinear filtering - // can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp), so we - // don't scissor the border, so it gets filled with correct neighboring texels. + // This is used while rendering the shadowmap. + const uint32_t dim = mOptions->mapSize; const uint16_t border = 1u; - switch (mShadowType) { - case ShadowType::DIRECTIONAL: - return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border }; + switch (mShadowType) { // NOLINT(*-multiway-paths-covered) + case ShadowType::DIRECTIONAL: { + // Don't render anything into the border; it's already filled with "fully lit". + return {mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border}; + } case ShadowType::SPOT: - case ShadowType::POINT: - return { mOffset.x, mOffset.y, dim, dim }; + case ShadowType::POINT: { + // Render into the border (which will get the adjacent faces texels) + return {mOffset.x, mOffset.y, dim, dim}; + } } + return {}; } float4 ShadowMap::getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept { - float border; // shadowmap border in texels - switch (mShadowType) { + // This is used to clamp the texture coordinates when sampling the shadowmap + + float border = 0; // shadowmap border in texels + switch (mShadowType) { // NOLINT(*-multiway-paths-covered) case ShadowType::DIRECTIONAL: - // For directional lights, we need to allow the sampling to reach the border, it - // happens when "focus shadow casters" is used for instance. + // For directional lights, we need to allow the sampling to reach the center of border texels, + // but no further, so we don't read into the adjacent texture in the 2D atlas (taking bilinear filtering + // into account). border = 0.5f; break; case ShadowType::SPOT: case ShadowType::POINT: - // For spot and point light, this is equal to the viewport. i.e. the valid - // texels are inside the viewport (w/ 1-texel border), the border will be used - // for bilinear filtering. + // For point-light, the border is only needed for bilinear filtering (of the other faces) border = 1.0f; break; } @@ -1355,8 +1367,8 @@ void ShadowMap::prepareCamera(Transaction const& transaction, } void ShadowMap::prepareViewport(Transaction const& transaction, - backend::Viewport const& viewport) noexcept { - ShadowMapDescriptorSet::prepareViewport(transaction, viewport); + backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept { + ShadowMapDescriptorSet::prepareViewport(transaction, physicalViewport, logicalViewport); } void ShadowMap::prepareTime(Transaction const& transaction, @@ -1370,8 +1382,8 @@ void ShadowMap::prepareMaterialGlobals(Transaction const& transaction, } void ShadowMap::prepareShadowMapping(Transaction const& transaction, - bool const highPrecision) noexcept { - ShadowMapDescriptorSet::prepareShadowMapping(transaction, highPrecision); + float const vsmExponent, float const vsmMaxMoment) noexcept { + ShadowMapDescriptorSet::prepareShadowMapping(transaction, vsmExponent, vsmMaxMoment); } ShadowMapDescriptorSet::Transaction ShadowMap::open(DriverApi& driver) noexcept { diff --git a/filament/src/ShadowMap.h b/filament/src/ShadowMap.h index 1b9fa5f68c..4100108e6a 100644 --- a/filament/src/ShadowMap.h +++ b/filament/src/ShadowMap.h @@ -128,7 +128,7 @@ public: static math::mat4f getPointLightViewMatrix(backend::TextureCubemapFace face, math::float3 position) noexcept; - void initialize(size_t lightIndex, ShadowType shadowType, uint16_t shadowIndex, uint8_t face, + void initialize(size_t lightIndex, ShadowType shadowType, bool vsm, uint16_t shadowIndex, uint8_t face, LightManager::ShadowOptions const* options); struct ShaderParameters { @@ -173,7 +173,10 @@ public: static void updateSceneInfoSpot(const math::mat4f& Mv, FScene const& scene, SceneInfo& sceneInfo); - LightManager::ShadowOptions const* getShadowOptions() const noexcept { return mOptions; } + LightManager::ShadowOptions const& getShadowOptions() const noexcept { + assert_invariant(mOptions); + return *mOptions; + } size_t getLightIndex() const { return mLightIndex; } uint16_t getShadowIndex() const { return mShadowIndex; } void setAllocation(uint8_t layer, backend::Viewport viewport) noexcept; @@ -193,13 +196,13 @@ public: static void prepareCamera(Transaction const& transaction, FEngine const& engine, const CameraInfo& cameraInfo) noexcept; static void prepareViewport(Transaction const& transaction, - backend::Viewport const& viewport) noexcept; + backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept; static void prepareTime(Transaction const& transaction, FEngine const& engine, math::float4 const& userTime) noexcept; static void prepareMaterialGlobals(Transaction const& transaction, std::array const& materialGlobals) noexcept; static void prepareShadowMapping(Transaction const& transaction, - bool highPrecision) noexcept; + float vsmExponent, float vsmMaxMoment) noexcept; static ShadowMapDescriptorSet::Transaction open(backend::DriverApi& driver) noexcept; void commit(Transaction& transaction, FEngine& engine, backend::DriverApi& driver) const noexcept; void bind(backend::DriverApi& driver) const noexcept; @@ -273,9 +276,8 @@ private: static inline math::float4 computeBoundingSphere( math::float3 const* vertices, size_t count) noexcept; - template - static void visitScene(FScene const& scene, uint32_t visibleLayers, - Casters casters, Receivers receivers) noexcept; + template + static void visitScene(FScene const& scene, uint32_t visibleLayers, Visitor visitor) noexcept; static inline Aabb compute2DBounds(const math::mat4f& lightView, math::float3 const* wsVertices, size_t count) noexcept; @@ -349,9 +351,11 @@ private: uint8_t mLayer = 0; // our layer in the shadowMap texture // 1 ShadowType mShadowType : 2; // :2 bool mHasVisibleShadows : 1; // :1 + bool mVsm : 1; // :1 + UTILS_UNUSED bool mReservedBit : 1; // :1 uint8_t mFace : 3; // :3 math::ushort2 mOffset{}; // 4 - UTILS_UNUSED uint8_t reserved[4]; // 4 + UTILS_UNUSED uint8_t reserved[4] = {}; // 4 }; } // namespace filament diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index 45cb9cf4f4..9df7c76815 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -151,12 +151,14 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::update( mDirectionalShadowMapCount = builder.mDirectionalShadowMapCount; mSpotShadowMapCount = builder.mSpotShadowMapCount; + const bool vsm = view.hasVSM(); for (auto const& entry : builder.mShadowMaps) { auto& shadowMap = getShadowMap(entry.shadowIndex); shadowMap.initialize( entry.lightIndex, entry.shadowType, + vsm, entry.shadowIndex, entry.face, entry.options); @@ -230,15 +232,12 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG FView& view, CameraInfo const& mainCameraInfo, float4 const& userTime) noexcept { - const float moment2 = std::numeric_limits::max(); - const float moment1 = std::sqrt(moment2); - const float4 vsmClearColor{ moment1, moment2, -moment1, moment2 }; - FScene* scene = view.getScene(); assert_invariant(scene); // make a copy here, because it's a very small structure - const TextureAtlasRequirements textureRequirements = mTextureAtlasRequirements; + VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions(); + TextureAtlasRequirements const textureRequirements = mTextureAtlasRequirements; assert_invariant(textureRequirements.layers <= CONFIG_MAX_SHADOW_LAYERS); // ------------------------------------------------------------------------------------------- @@ -258,8 +257,6 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG utils::FixedCapacityVector passList; }; - VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions(); - auto& prepareShadowPass = fg.addPass("Prepare Shadow Pass", [&](FrameGraph::Builder& builder, auto& data) { data.passList.reserve(getMaxShadowMapCount()); @@ -392,16 +389,18 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG auto transaction = ShadowMap::open(driver); ShadowMap::prepareCamera(transaction, engine, cameraInfo); - ShadowMap::prepareViewport(transaction, shadowMap.getViewport()); + ShadowMap::prepareViewport(transaction, + { 0, 0, textureRequirements.size, textureRequirements.size }, + shadowMap.getViewport()); ShadowMap::prepareTime(transaction, engine, userTime); ShadowMap::prepareMaterialGlobals(transaction, view.getMaterialGlobals()); ShadowMap::prepareShadowMapping(transaction, - vsmShadowOptions.highPrecision); + getWrapExponentEVSM(vsmShadowOptions, shadowMap.getShadowOptions()), + getMaxMomentEVSM(vsmShadowOptions)); shadowMap.commit(transaction, engine, driver); // updatePrimitivesLod must be run before RenderPass::appendCommands. - FView::updatePrimitivesLod(scene->getRenderableData(), - engine, cameraInfo, entry.range); + FView::updatePrimitivesLod(scene->getRenderableData(), engine, cameraInfo, entry.range); // generate and sort the commands for rendering the shadow map @@ -430,10 +429,10 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG entry.executor = pass.getExecutor(); if (!view.hasVSM()) { - auto const* options = shadowMap.getShadowOptions(); + auto const& options = shadowMap.getShadowOptions(); PolygonOffset const polygonOffset = { // handle reversed Z - .slope = -options->polygonOffsetSlope, - .constant = -options->polygonOffsetConstant + .slope = -options.polygonOffsetSlope, + .constant = -options.polygonOffsetConstant }; entry.executor.overridePolygonOffset(&polygonOffset); } @@ -465,10 +464,10 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG auto const& entry = *first; const uint8_t layer = entry.shadowMap->getLayer(); - const auto* options = entry.shadowMap->getShadowOptions(); + const auto& options = entry.shadowMap->getShadowOptions(); const auto msaaSamples = textureRequirements.msaaSamples; const bool blur = entry.shadowMap->hasVisibleShadows() && - view.hasVSM() && options->vsm.blurWidth > 0.0f; + view.hasVSM() && options.vsm.blurWidth > 0.0f; auto last = first; // loop over each shadow pass to find its layer range @@ -508,10 +507,9 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG renderTargetDesc.attachments.color[0] = data.output; renderTargetDesc.attachments.depth = depth; - renderTargetDesc.clearFlags = - TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH; + renderTargetDesc.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH; // we need to clear the shadow map with the max EVSM moments - renderTargetDesc.clearColor = vsmClearColor; + renderTargetDesc.clearColor = textureRequirements.clearColor; renderTargetDesc.samples = msaaSamples; if (UTILS_UNLIKELY(blur)) { @@ -530,16 +528,14 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG .attachments = { .color = { data.tempBlurSrc }, .depth = depth }, - .clearColor = vsmClearColor, + .clearColor = textureRequirements.clearColor, .samples = msaaSamples, - .clearFlags = TargetBufferFlags::COLOR - | TargetBufferFlags::DEPTH + .clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH }); } } else { // the shadowmap layer - data.output = builder.write(data.output, - FrameGraphTexture::Usage::DEPTH_ATTACHMENT); + data.output = builder.write(data.output, FrameGraphTexture::Usage::DEPTH_ATTACHMENT); renderTargetDesc.attachments.depth = data.output; renderTargetDesc.clearFlags = TargetBufferFlags::DEPTH; } @@ -594,27 +590,27 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG // the whole layer. Blurring should happen per shadowmap, not for the whole // layer. - const float blurWidth = options->vsm.blurWidth; + // FIXME: this Gaussian blur is not precise enough for EVSM + + const float blurWidth = options.vsm.blurWidth; if (blurWidth > 0.0f) { const float sigma = (blurWidth + 1.0f) / 6.0f; - size_t kernelWidth = std::ceil((blurWidth - 5.0f) / 4.0f); + size_t kernelWidth = size_t(std::ceil((blurWidth - 5.0f) / 4.0f)); kernelWidth = kernelWidth * 4 + 5; ppm.gaussianBlurPass(fg, shadowPass->tempBlurSrc, shadowPass->output, false, kernelWidth, sigma); } + } - // FIXME: mipmapping here is broken because it'll access texels from adjacent - // shadow maps. - - // If the shadow texture has more than one level, mipmapping was requested, either directly - // or indirectly via anisotropic filtering. - // So generate the mipmaps for each layer - if (textureRequirements.levels > 1) { - for (size_t level = 0; level < textureRequirements.levels - 1; level++) { - ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level, vsmClearColor); - } + // If the shadow texture has more than one level, mipmapping was requested, either directly + // or indirectly via anisotropic filtering. + // So generate the mipmaps for each layer + if (UTILS_UNLIKELY(textureRequirements.levels > 1)) { + auto& ppm = engine.getPostProcessManager(); + for (size_t level = 0; level < textureRequirements.levels - 1; level++) { + ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level, textureRequirements.clearColor); } } } @@ -626,7 +622,8 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng FView& view, CameraInfo cameraInfo, FScene::RenderableSoa& renderableData, FScene::LightSoa const& lightData, ShadowMap::SceneInfo sceneInfo) noexcept { - FScene* scene = view.getScene(); + FScene const* const scene = view.getScene(); + auto const& vsmShadowOptions = view.getVsmShadowOptions(); auto& lcm = engine.getLightManager(); FLightManager::Instance const directionalLight = lightData.elementAt(0); @@ -650,7 +647,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng }; bool hasVisibleShadows = false; - utils::Slice cascadedShadowMaps = getCascadedShadowMap(); + utils::Slice const cascadedShadowMaps = getCascadedShadowMap(); if (!cascadedShadowMaps.empty()) { // 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. @@ -750,6 +747,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized; s.shadows[shadowIndex].normalBias = wsTexelSize * normalBias; s.shadows[shadowIndex].elvsm = options.vsm.elvsm; + s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options); s.shadows[shadowIndex].bulbRadiusLs = mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSize); @@ -801,10 +799,8 @@ void ShadowMapManager::updateSpotVisibilityMasks( const bool visSpotShadowRenderable = v.castShadows && inVisibleLayer && (!v.culling || (mask & VISIBLE_DYN_SHADOW_RENDERABLE)); - using Type = Culler::result_type; - - visibleMask[i] &= ~Type(VISIBLE_DYN_SHADOW_RENDERABLE); - visibleMask[i] |= Type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT); + visibleMask[i] &= ~Culler::result_type(VISIBLE_DYN_SHADOW_RENDERABLE); + visibleMask[i] |= Culler::result_type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT); } } @@ -813,13 +809,14 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin FScene::LightSoa const& lightData, ShadowMap::SceneInfo const& sceneInfo) noexcept { const size_t lightIndex = shadowMap.getLightIndex(); - FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions(); + FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions(); + auto const& vsmShadowOptions = view.getVsmShadowOptions(); // update the shadow map frustum/camera const ShadowMap::ShadowMapInfo shadowMapInfo{ .atlasDimension = mTextureAtlasRequirements.size, - .textureDimension = uint16_t(options->mapSize), - .shadowDimension = uint16_t(options->mapSize - 2u), + .textureDimension = uint16_t(options.mapSize), + .shadowDimension = uint16_t(options.mapSize - 2u), .textureSpaceFlipped = engine.getBackend() == Backend::METAL || engine.getBackend() == Backend::VULKAN || engine.getBackend() == Backend::WEBGPU, @@ -834,7 +831,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin const size_t shadowIndex = shadowMap.getShadowIndex(); const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs; // note: normalBias is set to zero for VSM - const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias; + const float normalBias = shadowMapInfo.vsm ? 0.0f : options.normalBias; auto& s = mShadowUb.edit(); const double n = shadowMap.getCamera().getNear(); @@ -845,9 +842,10 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter; s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ; s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n)); - s.shadows[shadowIndex].elvsm = options->vsm.elvsm; + s.shadows[shadowIndex].elvsm = options.vsm.elvsm; + s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options); s.shadows[shadowIndex].bulbRadiusLs = - mSoftShadowOptions.penumbraScale * options->shadowBulbRadius + mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSizeAtOneMeter); } @@ -905,13 +903,14 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap, const uint8_t face = shadowMap.getFace(); const size_t lightIndex = shadowMap.getLightIndex(); - FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions(); + FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions(); + auto const& vsmShadowOptions = view.getVsmShadowOptions(); // update the shadow map frustum/camera const ShadowMap::ShadowMapInfo shadowMapInfo{ .atlasDimension = mTextureAtlasRequirements.size, - .textureDimension = uint16_t(options->mapSize), - .shadowDimension = uint16_t(options->mapSize), // point-lights don't have a border + .textureDimension = uint16_t(options.mapSize), + .shadowDimension = uint16_t(options.mapSize), // point-lights don't have a border .textureSpaceFlipped = engine.getBackend() == Backend::METAL || engine.getBackend() == Backend::VULKAN || engine.getBackend() == Backend::WEBGPU, @@ -926,7 +925,7 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap, const size_t shadowIndex = shadowMap.getShadowIndex(); const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs; // note: normalBias is set to zero for VSM - const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias; + const float normalBias = shadowMapInfo.vsm ? 0.0f : options.normalBias; auto& s = mShadowUb.edit(); const double n = shadowMap.getCamera().getNear(); @@ -937,9 +936,10 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap, s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter; s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ; s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n)); - s.shadows[shadowIndex].elvsm = options->vsm.elvsm; + s.shadows[shadowIndex].elvsm = options.vsm.elvsm; + s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options); s.shadows[shadowIndex].bulbRadiusLs = - mSoftShadowOptions.penumbraScale * options->shadowBulbRadius + mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSizeAtOneMeter); } } @@ -995,7 +995,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine lightData.data()); ShadowTechnique shadowTechnique{}; - utils::Slice spotShadowMaps = getSpotShadowMaps(); + utils::Slice const spotShadowMaps = getSpotShadowMaps(); if (!spotShadowMaps.empty()) { shadowTechnique |= ShadowTechnique::SHADOW_MAP; for (ShadowMap const& shadowMap : spotShadowMaps) { @@ -1035,14 +1035,14 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view for (ShadowMap const& shadowMap : getCascadedShadowMap()) { // Shadow map size should be the same for all cascades. auto const& options = shadowMap.getShadowOptions(); - maxDimension = std::max(maxDimension, options->mapSize); - elvsm = elvsm || options->vsm.elvsm; + maxDimension = std::max(maxDimension, options.mapSize); + elvsm = elvsm || options.vsm.elvsm; } for (ShadowMap const& shadowMap : getSpotShadowMaps()) { auto const& options = shadowMap.getShadowOptions(); - maxDimension = std::max(maxDimension, options->mapSize); - elvsm = elvsm || options->vsm.elvsm; + maxDimension = std::max(maxDimension, options.mapSize); + elvsm = elvsm || options.vsm.elvsm; } uint8_t layersNeeded = 0; @@ -1052,7 +1052,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view ShadowMap* pShadowMap) mutable { // Allocate shadowmap from our Atlas Allocator auto const& options = pShadowMap->getShadowOptions(); - auto allocation = allocator.allocate(options->mapSize); + auto allocation = allocator.allocate(options.mapSize); assert_invariant(allocation.isValid()); assert_invariant(!allocation.viewport.empty()); pShadowMap->setAllocation(allocation.layer, allocation.viewport); @@ -1088,6 +1088,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view msaaSamples = 1; } + float4 clearColor{}; TextureFormat format = TextureFormat::DEPTH16; if (view.hasVSM()) { if (vsmShadowOptions.highPrecision) { @@ -1095,6 +1096,9 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view } else { format = elvsm ? TextureFormat::RGBA16F : TextureFormat::RG16F; } + const float maxMoment2 = getMaxMomentEVSM(vsmShadowOptions); + const float maxMoment1 = std::sqrt(maxMoment2); + clearColor = { maxMoment1, maxMoment2, 0, 0 }; } mSoftShadowOptions = view.getSoftShadowOptions(); @@ -1116,7 +1120,8 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view layersNeeded, mipLevels, msaaSamples, - format + format, + clearColor }; } diff --git a/filament/src/ShadowMapManager.h b/filament/src/ShadowMapManager.h index 67cf52bdca..326f708a99 100644 --- a/filament/src/ShadowMapManager.h +++ b/filament/src/ShadowMapManager.h @@ -47,9 +47,12 @@ #include #include +#include #include +#include #include +#include #include #include #include @@ -145,6 +148,28 @@ public: // for debugging only utils::FixedCapacityVector getDirectionalShadowCameras() const noexcept; + static float getMaxMomentEVSM(VsmShadowOptions const& vsmShadowOptions) noexcept { + return vsmShadowOptions.highPrecision ? + std::numeric_limits::max() : + float(std::numeric_limits::max()); + } + + static float getMaxWrapExponentEVSM(VsmShadowOptions const& vsmShadowOptions) noexcept { + constexpr float low = 5.2f; // ~ std::log(std::numeric_limits::max()) * 0.5f; + constexpr float high = 40.0f; // ~ std::log(std::numeric_limits::max()) * 0.5f; + return vsmShadowOptions.highPrecision ? high : low; + } + + static float getWrapExponentEVSM( + VsmShadowOptions const& vsmShadowOptions, + LightManager::ShadowOptions const& options) noexcept { + constexpr float ABSOLUTE_FILTER_LIMIT = 42.0f; + float const targetExponent = getMaxWrapExponentEVSM(vsmShadowOptions); + float const effectiveFilterRadius = std::max(1.0f, options.vsm.blurWidth); + float const filterCeiling = ABSOLUTE_FILTER_LIMIT / effectiveFilterRadius; + return std::min(targetExponent, filterCeiling); + } + private: explicit ShadowMapManager(FEngine& engine); @@ -216,6 +241,7 @@ private: uint8_t levels = 0; uint8_t msaaSamples = 1; backend::TextureFormat format = backend::TextureFormat::DEPTH16; + math::float4 clearColor{}; } mTextureAtlasRequirements; SoftShadowOptions mSoftShadowOptions; diff --git a/filament/src/details/MaterialInstance.h b/filament/src/details/MaterialInstance.h index ee0bdaa06d..4e99b38115 100644 --- a/filament/src/details/MaterialInstance.h +++ b/filament/src/details/MaterialInstance.h @@ -93,6 +93,10 @@ public: mHasScissor = true; } + void setScissor(backend::Viewport const& viewport) noexcept { + setScissor(viewport.left, viewport.bottom, viewport.width, viewport.height); + } + void unsetScissor() noexcept { constexpr uint32_t maxvalu = std::numeric_limits::max(); mScissorRect = { 0, 0, maxvalu, maxvalu }; diff --git a/filament/src/details/View.cpp b/filament/src/details/View.cpp index 82a8449187..5cfca218c1 100644 --- a/filament/src/details/View.cpp +++ b/filament/src/details/View.cpp @@ -76,6 +76,7 @@ #include #include #include +#include #include #include #include @@ -1131,8 +1132,6 @@ void FView::prepareShadowMapping() const noexcept { uniforms = mShadowMapManager->getShadowMappingUniforms(); } - constexpr float low = 5.54f; // ~ std::log(std::numeric_limits::max()) * 0.5f; - constexpr float high = 42.0f; // ~ std::log(std::numeric_limits::max()) * 0.5f; constexpr uint32_t SHADOW_SAMPLING_RUNTIME_PCF = 0u; constexpr uint32_t SHADOW_SAMPLING_RUNTIME_EVSM = 1u; constexpr uint32_t SHADOW_SAMPLING_RUNTIME_DPCF = 2u; @@ -1148,8 +1147,8 @@ void FView::prepareShadowMapping() const noexcept { break; case ShadowType::VSM: s.shadowSamplingType = SHADOW_SAMPLING_RUNTIME_EVSM; - s.vsmExponent = mVsmShadowOptions.highPrecision ? high : low; - s.vsmDepthScale = mVsmShadowOptions.minVarianceScale * 0.01f * s.vsmExponent; + s.vsmExponent = 0; // this is only used when rendering the shadowmap, not when using it + s.vsmMaxMoment = ShadowMapManager::getMaxMomentEVSM(mVsmShadowOptions); s.vsmLightBleedReduction = mVsmShadowOptions.lightBleedReduction; break; case ShadowType::DPCF: diff --git a/filament/src/ds/ShadowMapDescriptorSet.cpp b/filament/src/ds/ShadowMapDescriptorSet.cpp index 2e8ff6fa10..465b2f7600 100644 --- a/filament/src/ds/ShadowMapDescriptorSet.cpp +++ b/filament/src/ds/ShadowMapDescriptorSet.cpp @@ -28,9 +28,11 @@ #include +#include #include #include +#include namespace filament { @@ -74,8 +76,8 @@ void ShadowMapDescriptorSet::prepareLodBias(Transaction const& transaction, floa } void ShadowMapDescriptorSet::prepareViewport(Transaction const& transaction, - backend::Viewport const& viewport) noexcept { - PerViewDescriptorSetUtils::prepareViewport(edit(transaction), viewport, viewport); + backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept { + PerViewDescriptorSetUtils::prepareViewport(edit(transaction), physicalViewport, logicalViewport); // TODO: offset calculation is now different } @@ -90,11 +92,10 @@ void ShadowMapDescriptorSet::prepareMaterialGlobals(Transaction const& transacti } void ShadowMapDescriptorSet::prepareShadowMapping(Transaction const& transaction, - bool const highPrecision) noexcept { + float const vsmExponent, float const vsmMaxMoment) noexcept { auto& s = edit(transaction); - constexpr float low = 5.54f; // ~ std::log(std::numeric_limits::max()) * 0.5f; - constexpr float high = 42.0f; // ~ std::log(std::numeric_limits::max()) * 0.5f; - s.vsmExponent = highPrecision ? high : low; + s.vsmExponent = vsmExponent; + s.vsmMaxMoment = vsmMaxMoment; } ShadowMapDescriptorSet::Transaction ShadowMapDescriptorSet::open(DriverApi& driver) noexcept { diff --git a/filament/src/ds/ShadowMapDescriptorSet.h b/filament/src/ds/ShadowMapDescriptorSet.h index 9537aa9ce2..b1e1c9ed9d 100644 --- a/filament/src/ds/ShadowMapDescriptorSet.h +++ b/filament/src/ds/ShadowMapDescriptorSet.h @@ -66,7 +66,7 @@ public: float bias) noexcept; static void prepareViewport(Transaction const& transaction, - backend::Viewport const& viewport) noexcept; + backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept; static void prepareTime(Transaction const& transaction, FEngine const& engine, math::float4 const& userTime) noexcept; @@ -75,7 +75,7 @@ public: std::array const& materialGlobals) noexcept; static void prepareShadowMapping(Transaction const& transaction, - bool highPrecision) noexcept; + float vsmExponent, float vsmMaxMoment) noexcept; static Transaction open(backend::DriverApi& driver) noexcept; diff --git a/filament/src/materials/vsmMipmap.mat b/filament/src/materials/vsmMipmap.mat index 5047447bf2..822f942ded 100644 --- a/filament/src/materials/vsmMipmap.mat +++ b/filament/src/materials/vsmMipmap.mat @@ -19,7 +19,8 @@ material { { name : color, target : color, - type : float4 + type : float4, + precision : high } ], domain : postprocess, @@ -30,8 +31,29 @@ material { fragment { void postProcess(inout PostProcessInputs postProcess) { - highp vec2 uv = gl_FragCoord.xy * materialParams.uvscale; - postProcess.color = textureLod(materialParams_color, - vec3(uv, materialParams.layer), 0.0); + // For EVSM mipmapping, we use a custom box filter (instead of the GPU's bilinear filter), because GPUs often + // use a lower precision for texture interpolation and EVSM moments are very sensitive to that. + + highp ivec2 destCoord = ivec2(gl_FragCoord.xy); + highp ivec3 srcCoord = ivec3(destCoord * 2, materialParams.layer); + +#if defined(TARGET_WEBGPU_ENVIRONMENT) + // it looks like WebGPU doesn't support texelFetchOffset + highp vec4 m0 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(0, 0), srcCoord.z), 0); + highp vec4 m1 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(1, 0), srcCoord.z), 0); + highp vec4 m2 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(0, 1), srcCoord.z), 0); + highp vec4 m3 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(1, 1), srcCoord.z), 0); +#else + highp vec4 m0 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(0, 0)); + highp vec4 m1 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(1, 0)); + highp vec4 m2 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(0, 1)); + highp vec4 m3 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(1, 1)); +#endif + + // Make sure to not overflow the moments when averaging them + postProcess.color = (m0 * 0.25) + + (m1 * 0.25) + + (m2 * 0.25) + + (m3 * 0.25); } } diff --git a/libs/filabridge/include/private/filament/UibStructs.h b/libs/filabridge/include/private/filament/UibStructs.h index ed664a24e3..9d95135627 100644 --- a/libs/filabridge/include/private/filament/UibStructs.h +++ b/libs/filabridge/include/private/filament/UibStructs.h @@ -166,7 +166,7 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init) // VSM shadows [variant: VSM] // -------------------------------------------------------------------------------------------- float vsmExponent; - float vsmDepthScale; + float vsmMaxMoment; float vsmLightBleedReduction; uint32_t shadowSamplingType; // 0: vsm, 1: dpcf @@ -302,9 +302,9 @@ struct ShadowUib { // NOLINT(cppcoreguidelines-pro-type-member-init) float bulbRadiusLs; // 4 float nearOverFarMinusNear; // 4 math::float2 normalBias; // 4 - bool elvsm; // 4 - uint32_t layer; // 4 - uint32_t reserved1; // 4 + bool elvsm; // 4 // could be 1 bit + uint32_t layer; // 4 // could be 8 bits + float vsmExponent; // 4 // could be fp16 uint32_t reserved2; // 4 }; ShadowData shadows[CONFIG_MAX_SHADOWMAPS]; diff --git a/libs/filamat/src/shaders/UibGenerator.cpp b/libs/filamat/src/shaders/UibGenerator.cpp index f88002b954..3a13050050 100644 --- a/libs/filamat/src/shaders/UibGenerator.cpp +++ b/libs/filamat/src/shaders/UibGenerator.cpp @@ -175,7 +175,7 @@ BufferInterfaceBlock const& UibGenerator::getPerViewUib() noexcept { // VSM shadows [variant: VSM] // ------------------------------------------------------------------------------------ { "vsmExponent", 0, Type::FLOAT }, - { "vsmDepthScale", 0, Type::FLOAT }, + { "vsmMaxMoment", 0, Type::FLOAT, Precision::HIGH }, { "vsmLightBleedReduction", 0, Type::FLOAT }, { "shadowSamplingType", 0, Type::UINT }, diff --git a/shaders/src/surface_depth_main.fs b/shaders/src/surface_depth_main.fs index 0de5c0cf17..261608ae4f 100644 --- a/shaders/src/surface_depth_main.fs +++ b/shaders/src/surface_depth_main.fs @@ -20,7 +20,7 @@ layout(location = 0) out highp uvec2 outPicking; // note: VARIANT_HAS_VSM and VARIANT_HAS_PICKING are mutually exclusive //------------------------------------------------------------------------------ -highp vec2 computeDepthMomentsVSM(const highp float depth); +highp vec4 computeDepthMomentsVSM(const highp float depth); void main() { filament_lodBias = frameUniforms.lodBias; @@ -53,9 +53,7 @@ void main() { // we always compute the "negative" side of ELVSM because the cost is small, and this allows // EVSM/ELVSM choice to be done on the CPU side more easily. highp float depth = vertex_worldPosition.w; - depth = exp(frameUniforms.vsmExponent * depth); - fragColor.xy = computeDepthMomentsVSM(depth); - fragColor.zw = computeDepthMomentsVSM(-1.0 / depth); // requires at least RGBA16F + fragColor = computeDepthMomentsVSM(depth); #elif defined(VARIANT_HAS_PICKING) #if FILAMENT_EFFECTIVE_VERSION == 100 outPicking.a = mod(float(object_uniforms_objectId / 65536), 256.0) / 255.0; @@ -74,22 +72,30 @@ void main() { #endif } -highp vec2 computeDepthMomentsVSM(const highp float depth) { - // computes the moments +#if MATERIAL_FEATURE_LEVEL > 0 + +highp vec4 computeDepthMomentsVSM(const highp float depth) { // 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; + // computes the first two moments + float c = frameUniforms.vsmExponent; + highp float MAX_MOMENT = frameUniforms.vsmMaxMoment; - // the first moment is just the depth (average) - moments.x = depth; + // wrap depth for EVSM + highp float z = exp(c * depth); - // compute the 2nd moment over the pixel extents. - moments.y = depth * depth; + // compute EVSM moments + highp vec2 m1 = vec2(z, -1.0 / z); + highp vec2 m2 = m1 * m1; - // the local linear approximation is not correct with a warped depth - //highp float dx = dFdx(depth); - //highp float dy = dFdy(depth); - //moments.y += 0.25 * (dx * dx + dy * dy); + // compute analytic variance (2nd moment), taking into account the change in depth accross the texel + highp float dzdx = dFdx(depth); + highp float dzdy = dFdy(depth); + highp float linearVariance = 0.25 * (dzdx * dzdx + dzdy * dzdy); + highp vec2 analyticVariance = c * c * m2 * linearVariance; + m2 = min(m2 + analyticVariance, MAX_MOMENT); - return moments; + return vec4(m1.x, m2.x, m1.y, m2.y); } + +#endif diff --git a/shaders/src/surface_shadowing.fs b/shaders/src/surface_shadowing.fs index f4c8b18c9c..efc93b16be 100644 --- a/shaders/src/surface_shadowing.fs +++ b/shaders/src/surface_shadowing.fs @@ -335,6 +335,91 @@ float ShadowSample_PCSS(const bool DIRECTIONAL, return 1.0 - percentageOccluded; } +//------------------------------------------------------------------------------ +// VSM +//------------------------------------------------------------------------------ + +float chebyshevUpperBound(const highp vec2 moments, const highp float depth, + const highp float minVariance, const highp float lbrAmount) { + // Fast path: if the receiver is fully in front of the caster + if (depth <= moments.x) { + return 1.0; + } + + // Calculate variance with our dynamically injected floor + highp float variance = max(moments.y - (moments.x * moments.x), minVariance); + + // Standard Chebyshev inequality + highp float d = depth - moments.x; + highp float p_max = variance / (variance + d * d); + + // Apply Light Bleeding Reduction (LBR) + return saturate((p_max - lbrAmount) / (1.0 - lbrAmount)); +} + +float evaluateEVSM(const bool ELVSM, float c, + const highp vec4 moments, const highp float zReceiver, + const highp vec2 dzduv, const highp vec2 texelSize) { + const highp float EPSILON_MULTIPLIER = 0.002; // could be 0.00001 in fp32 + float lbrAmount = frameUniforms.vsmLightBleedReduction; + + // Scale the UV-space gradient down to a single shadow map texel footprint. + highp vec2 texel_dzduv = dzduv * texelSize; + + // squared magnitude of the linear depth gradient across a single shadow map texel footprint + highp float dz2 = dot(texel_dzduv, texel_dzduv); + + // remap depth to [-1, 1] + highp float depth = zReceiver * 2.0 - 1.0; + + // positive wrap + highp float pw = exp(c * depth); + highp float epsilon = EPSILON_MULTIPLIER * (pw * pw); + // Dynamic variance for the positive side (derivative of wraped depth w.r.t. light-space depth via Chain Rule) + highp float dpwdz = 2.0 * c * pw; + highp float pMinVariance = epsilon + 0.25 * (dpwdz * dpwdz) * dz2; + float p = chebyshevUpperBound(moments.xy, pw, pMinVariance, lbrAmount); + + // negative wrap + if (ELVSM) { + highp float nw = -1.0 / pw; + highp float epsilon = EPSILON_MULTIPLIER * (nw * nw); + // Dynamic variance for the negative side (derivative of wraped depth w.r.t. light-space depth via Chain Rule) + highp float dnwdz = 2.0 * c * nw; + highp float nMinVariance = epsilon + 0.25 * (dnwdz * dnwdz) * dz2; + float n = chebyshevUpperBound(moments.zw, nw, nMinVariance, lbrAmount); + p = min(p, n); + } + + return p; +} + +float ShadowSample_VSM(const bool DIRECTIONAL, const highp sampler2DArray shadowMap, + const highp vec4 scissorNormalized, + const uint layer, const int index, + const highp vec4 shadowPosition, const highp float zLight) { + + bool ELVSM = shadowUniforms.shadows[index].elvsm; + float c = shadowUniforms.shadows[index].vsmExponent; + highp vec2 texelSize = vec2(1.0) / vec2(textureSize(shadowMap, 0)); // TODO: put this in a uniform + + // note: shadowPosition.z is in linear light-space normalized to [0, 1] + // see: ShadowMap::computeVsmLightSpaceMatrix() in ShadowMap.cpp + // see: computeLightSpacePosition() in common_shadowing.fs + highp vec3 position = vec3(shadowPosition.xy * (1.0 / shadowPosition.w), shadowPosition.z); + + // plane receiver bias to reduce shadow acnee on the received plane (before clamp) + highp vec2 dzduv = computeReceiverPlaneDepthBias(position); + + // clamp uv to border + position.xy = clamp(position.xy, scissorNormalized.xy, scissorNormalized.zw); + + // Read the shadow map with all available filtering + highp vec4 moments = texture(shadowMap, vec3(position.xy, layer)); + + return evaluateEVSM(ELVSM, c, moments, position.z, dzduv, texelSize); +} + //------------------------------------------------------------------------------ // Screen-space Contact Shadows //------------------------------------------------------------------------------ @@ -409,70 +494,6 @@ float screenSpaceContactShadow(vec3 lightDirection) { return occlusion; } -//------------------------------------------------------------------------------ -// VSM -//------------------------------------------------------------------------------ - -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); -} - -float reduceLightBleed(const float pMax, const float amount) { - // Remove the [0, amount] tail and linearly rescale (amount, 1]. - return linstep(amount, 1.0, pMax); -} - -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; - float pMax = variance / (variance + d * d); - - pMax = reduceLightBleed(pMax, lightBleedReduction); - - return mean <= moments.x ? 1.0 : pMax; -} - -float evaluateShadowVSM(const highp vec2 moments, const highp float depth) { - highp float depthScale = frameUniforms.vsmDepthScale * depth; - highp float minVariance = depthScale * depthScale; - return chebyshevUpperBound(moments, depth, minVariance, frameUniforms.vsmLightBleedReduction); -} - -float ShadowSample_VSM(const bool ELVSM, const highp sampler2DArray shadowMap, - const highp vec4 scissorNormalized, - const uint layer, const highp vec4 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 - highp vec3 position = vec3(shadowPosition.xy * (1.0 / shadowPosition.w), shadowPosition.z); - - // Note: we don't need to clamp to `scissorNormalized` in the VSM case because this is only - // needed when the shadow casters and receivers are different, which is never the case with VSM - // (see ShadowMap.cpp). - - // Read the shadow map with all available filtering - highp vec4 moments = texture(shadowMap, vec3(position.xy, layer)); - highp float depth = position.z; - - // EVSM depth warping - depth = depth * 2.0 - 1.0; - depth = frameUniforms.vsmExponent * depth; - - depth = exp(depth); - float p = evaluateShadowVSM(moments.xy, depth); - if (ELVSM) { - p = min(p, evaluateShadowVSM(moments.zw, -1.0 / depth)); - } - return p; -} - //------------------------------------------------------------------------------ // Shadow sampling dispatch //------------------------------------------------------------------------------ @@ -529,6 +550,9 @@ float shadow(const bool DIRECTIONAL, } else if (CONFIG_SHADOW_SAMPLING_METHOD == SHADOW_SAMPLING_PCF_LOW) { return ShadowSample_PCF_Low(shadowMap, scissorNormalized, layer, shadowPosition); } + + // should not happen + return 0.0; } // Shadow requiring a sampler2D sampler (VSM, DPCF and PCSS) @@ -539,9 +563,8 @@ float shadow(const bool DIRECTIONAL, uint layer = shadowUniforms.shadows[index].layer; // This conditional is resolved at compile time if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_EVSM) { - bool elvsm = shadowUniforms.shadows[index].elvsm; - return ShadowSample_VSM(elvsm, shadowMap, scissorNormalized, layer, - shadowPosition); + return ShadowSample_VSM(DIRECTIONAL, shadowMap, scissorNormalized, layer, index, + shadowPosition, zLight); } if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_DPCF) { @@ -558,8 +581,8 @@ float shadow(const bool DIRECTIONAL, // This is here mostly for debugging at this point. // Note: In this codepath, the normal bias is not applied because we're in the VSM variant. // (see: get{Cascade|Spot}LightSpacePosition) - return ShadowSample_PCF(shadowMap, scissorNormalized, - layer, shadowPosition); + return ShadowSample_PCF(shadowMap, scissorNormalized, layer, + shadowPosition); } // should not happen diff --git a/shaders/src/surface_types.glsl b/shaders/src/surface_types.glsl index f6973d8544..1cfd1910fb 100644 --- a/shaders/src/surface_types.glsl +++ b/shaders/src/surface_types.glsl @@ -10,7 +10,7 @@ struct ShadowData { highp vec2 normalBias; bool elvsm; mediump uint layer; - mediump uint reserved1; + mediump float vsmExponent; mediump uint reserved2; }; #endif diff --git a/web/filament-js/filament.d.ts b/web/filament-js/filament.d.ts index d3319aa1c7..3c28c5161f 100644 --- a/web/filament-js/filament.d.ts +++ b/web/filament-js/filament.d.ts @@ -1747,7 +1747,7 @@ export interface View$VsmShadowOptions { */ highPrecision?: boolean; /** - * VSM minimum variance scale, must be positive. + * @deprecated has no effect. */ minVarianceScale?: number; /**