From ffcccb336e4eee0578e663184cb9b3da5447a9bb Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Mon, 30 Mar 2026 20:55:02 -0700 Subject: [PATCH] Refactor EVSM shadowing to use per-shadowmap blurring (#9850) This commit addresses several issues with EVSM blur precision and shadow atlas boundary handling by replacing the global layer blur with a dedicated, per-shadowmap separated Gaussian blur pass. Key improvements: - Per-ShadowMap Blurring: Moved the EVSM blurring and mipmap passes from `PostProcessManager` to `ShadowMapManager`. Blurring is now applied individually per shadow map using its specific `blurWidth`, rather than being incorrectly applied across the entire atlas layer. - Custom EVSM Gaussian Shader: Introduced a new dedicated post-processing material (`gaussian.fs` and `gaussian.mat`) tailored specifically for EVSM. - Safe FP16 Accumulation: The new blur shader pre-scales the color accumulation using safe FP16 FMA to strictly prevent `+Inf` overflows during Gaussian pass sampling. - Atlas Boundary Clamping: The custom blur shader enforces clamping to an explicit sub-rectangle (`bounds` uniform) preventing texture bleed across adjacent shadow maps in the atlas. - Code Organization: Created a new `evsm/` material directory, extracting `vsmMipmap.mat` and registering the new `gaussian.mat`. --- filament/CMakeLists.txt | 10 +- filament/src/PostProcessManager.cpp | 76 +---- filament/src/PostProcessManager.h | 23 +- filament/src/ShadowMapManager.cpp | 271 ++++++++++++++---- filament/src/ShadowMapManager.h | 15 + filament/src/fg/FrameGraphRenderPass.h | 1 - filament/src/materials/evsm/evsm.cpp | 39 +++ filament/src/materials/evsm/evsm.h | 28 ++ filament/src/materials/evsm/gaussian.fs | 47 +++ filament/src/materials/evsm/gaussian.mat | 52 ++++ .../src/materials/{ => evsm}/vsmMipmap.mat | 4 - 11 files changed, 423 insertions(+), 143 deletions(-) create mode 100644 filament/src/materials/evsm/evsm.cpp create mode 100644 filament/src/materials/evsm/evsm.h create mode 100644 filament/src/materials/evsm/gaussian.fs create mode 100644 filament/src/materials/evsm/gaussian.mat rename filament/src/materials/{ => evsm}/vsmMipmap.mat (96%) diff --git a/filament/CMakeLists.txt b/filament/CMakeLists.txt index 98a14f3c8b..33c6c0de13 100644 --- a/filament/CMakeLists.txt +++ b/filament/CMakeLists.txt @@ -56,6 +56,7 @@ set(SRCS src/materials/bloom/bloom.cpp src/materials/colorGrading/colorGrading.cpp src/materials/dof/dof.cpp + src/materials/evsm/evsm.cpp src/materials/flare/flare.cpp src/materials/fog/fog.cpp src/materials/fsr/fsr.cpp @@ -285,6 +286,8 @@ set(MATERIAL_SRCS src/materials/dof/dofMipmap.mat src/materials/dof/dofTiles.mat src/materials/dof/dofTilesSwizzle.mat + src/materials/evsm/gaussian.mat + src/materials/evsm/vsmMipmap.mat src/materials/flare/flare.mat src/materials/fog/fog.mat src/materials/fsr/fsr_easu.mat @@ -301,7 +304,6 @@ set(MATERIAL_SRCS src/materials/ssao/mipmapDepth.mat src/materials/ssao/sao.mat src/materials/ssao/saoBentNormals.mat - src/materials/vsmMipmap.mat ) if (NOT FILAMENT_DISABLE_GTAO) @@ -639,6 +641,12 @@ add_custom_command( APPEND ) +add_custom_command( + OUTPUT "${MATERIAL_DIR}/gaussian.filamat" + DEPENDS src/materials/evsm/gaussian.fs + APPEND +) + file(MAKE_DIRECTORY "${GENERATION_ROOT}/generated/data/") set(output_path_dfg_bin "${GENERATION_ROOT}/generated/data/dfg.bin") diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 1165c71928..2067a64177 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -29,6 +29,7 @@ #include "materials/bloom/bloom.h" #include "materials/colorGrading/colorGrading.h" #include "materials/dof/dof.h" +#include "materials/evsm/evsm.h" #include "materials/flare/flare.h" #include "materials/fog/fog.h" #include "materials/fsr/fsr.h" @@ -297,7 +298,6 @@ static const PostProcessManager::StaticMaterialInfo sMaterialList[] = { { {"arraySampler", false}, {"componentCount", 4} } }, { "separableGaussianBlur4L", MATERIAL(MATERIALS, SEPARABLEGAUSSIANBLUR), { {"arraySampler", true }, {"componentCount", 4} } }, - { "vsmMipmap", MATERIAL(MATERIALS, VSMMIPMAP) }, { "debugShadowCascades", MATERIAL(MATERIALS, DEBUGSHADOWCASCADES) }, { "resolveDepth", MATERIAL(MATERIALS, RESOLVEDEPTH) }, { "shadowmap", MATERIAL(MATERIALS, SHADOWMAP) }, @@ -369,6 +369,9 @@ void PostProcessManager::init() noexcept { for (auto const& info: sMaterialList) { registerPostProcessMaterial(info.name, info); } + for (auto const& info: getEvsmMaterialList()) { + registerPostProcessMaterial(info.name, info); + } for (auto const& info: getBloomMaterialList()) { registerPostProcessMaterial(info.name, info); } @@ -1249,7 +1252,8 @@ FrameGraphId PostProcessManager::generateGaussianMipmap(Frame FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& fg, FrameGraphId const input, FrameGraphId output, - bool const reinhard, size_t kernelWidth, const float sigma) noexcept { + bool const reinhard, size_t kernelWidth, const float sigma, + std::optional scissor) noexcept { auto computeGaussianCoefficients = [kernelWidth, sigma](float2* kernel, size_t const size) -> size_t { @@ -1387,6 +1391,10 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& { // horizontal pass auto mi = getMaterialInstance(mEngine, driver, separableGaussianBlur); + if (scissor.has_value()) { + mi->setScissor(scissor.value()); + } + setCommonParams(mi); mi->setParameter(sourceParameterName, hwIn, SamplerParams{ .filterMag = SamplerMagFilter::LINEAR, @@ -1405,6 +1413,9 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& { // vertical pass auto mi = getMaterialInstance(mEngine, driver, separableGaussianBlur); + if (scissor.has_value()) { + mi->setScissor(scissor.value()); + } setCommonParams(mi); UTILS_UNUSED_IN_RELEASE auto width = outDesc.width; UTILS_UNUSED_IN_RELEASE auto height = outDesc.height; @@ -3781,67 +3792,6 @@ FrameGraphId PostProcessManager::resolveDepthWithShader(Frame return ppResolve->output; } -FrameGraphId PostProcessManager::vsmMipmapPass(FrameGraph& fg, - FrameGraphId const input, uint8_t layer, size_t const level, - float4 clearColor) noexcept { - - struct VsmMipData { - FrameGraphId in; - }; - - auto const& depthMipmapPass = fg.addPass("VSM Generate Mipmap Pass", - [&](FrameGraph::Builder& builder, auto& data) { - StaticString const name = builder.getName(input); - data.in = builder.sample(input); - - auto out = builder.createSubresource(data.in, "Mip level", { - .level = uint8_t(level + 1), .layer = layer }); - - out = builder.write(out, FrameGraphTexture::Usage::COLOR_ATTACHMENT); - builder.declareRenderPass(name, { - .attachments = { .color = { out }}, - .clearColor = clearColor, - .clearFlags = TargetBufferFlags::COLOR - }); - }, - [=, this](FrameGraphResources const& resources, - auto const& data, DriverApi& driver) { - bindPostProcessDescriptorSet(driver); - bindPerRenderableDescriptorSet(driver); - - auto in = driver.createTextureView(resources.getTexture(data.in), level, 1); - auto out = resources.getRenderPassInfo(); - - auto const& inDesc = resources.getDescriptor(data.in); - auto width = inDesc.width; - assert_invariant(width == inDesc.height); - uint32_t const dim = std::max(1u, width >> (level + 1)); - - auto& material = getPostProcessMaterial("vsmMipmap"); - FMaterial const* const ma = material.getMaterial(mEngine, driver); - - auto const pipeline = getPipelineState(ma); - backend::Viewport const scissor = { 0, 0, dim, dim }; - - FMaterialInstance* const mi = getMaterialInstance(ma); - mi->setParameter("color", in, SamplerParams{ - .filterMag = SamplerMagFilter::LINEAR, - .filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST - }); - mi->setParameter("layer", uint32_t(layer)); - mi->setParameter("uvscale", 1.0f / float(dim)); - mi->commit(driver, getUboManager()); - mi->use(driver); - - renderFullScreenQuadWithScissor(out, pipeline, scissor, driver); - unbindAllDescriptorSets(driver); - - driver.destroyTexture(in); // `in` is just a view on `data.in` - }); - - return depthMipmapPass->in; -} - FrameGraphId PostProcessManager::debugShadowCascades(FrameGraph& fg, ShadowMapManager const& smm, FrameGraphId const input, diff --git a/filament/src/PostProcessManager.h b/filament/src/PostProcessManager.h index e0308d84d7..33d5f1b2c5 100644 --- a/filament/src/PostProcessManager.h +++ b/filament/src/PostProcessManager.h @@ -312,15 +312,11 @@ public: utils::StaticString outputBufferName, FrameGraphId input, FrameGraphTexture::Descriptor outDesc) noexcept; - // VSM shadow mipmap pass - FrameGraphId vsmMipmapPass(FrameGraph& fg, - FrameGraphId input, uint8_t layer, size_t level, - math::float4 clearColor) noexcept; - FrameGraphId gaussianBlurPass(FrameGraph& fg, FrameGraphId input, FrameGraphId output, - bool reinhard, size_t kernelWidth, float sigma) noexcept; + bool reinhard, size_t kernelWidth, float sigma, + std::optional scissor = {}) noexcept; FrameGraphId debugShadowCascades(FrameGraph& fg, ShadowMapManager const& smm, @@ -391,6 +387,8 @@ public: void bindPostProcessDescriptorSet(backend::DriverApi& driver) const noexcept; + void bindPerRenderableDescriptorSet(backend::DriverApi& driver) const noexcept; + backend::PipelineState getPipelineState(FMaterial const* ma, Variant::type_t variant) const noexcept; backend::PipelineState getPipelineState(FMaterial const* ma, @@ -425,10 +423,10 @@ public: void resetForRender(); -private: - static void unbindAllDescriptorSets(backend::DriverApi& driver) noexcept; - void bindPerRenderableDescriptorSet(backend::DriverApi& driver) const noexcept; + MaterialInstanceManager& getMaterialInstanceManager() noexcept { + return mMaterialInstanceManager; + } // Helper to get a MaterialInstance from a FMaterial // This currently just call FMaterial::getDefaultInstance(). @@ -437,12 +435,15 @@ private: } // Helper to get a MaterialInstance from a PostProcessMaterial. - FMaterialInstance* getMaterialInstance(FEngine& engine, backend::DriverApi& driver, PostProcessMaterial const& material, - PostProcessVariant variant = PostProcessVariant::OPAQUE) { + FMaterialInstance* getMaterialInstance(FEngine& engine, backend::DriverApi& driver, + PostProcessMaterial const& material, PostProcessVariant variant = PostProcessVariant::OPAQUE) { FMaterial const* ma = material.getMaterial(engine, driver, variant); return getMaterialInstance(ma); } + static void unbindAllDescriptorSets(backend::DriverApi& driver) noexcept; + +private: UboManager* getUboManager() const noexcept; backend::RenderPrimitiveHandle mFullScreenQuadRph; diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index da143de9f4..6826a084fa 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -30,6 +30,7 @@ #include "details/Camera.h" #include "details/DebugRegistry.h" +#include "details/MaterialInstance.h" #include "details/Texture.h" #include "details/View.h" @@ -42,6 +43,7 @@ #include #include +#include #include #include @@ -53,6 +55,7 @@ #include #include #include +#include #include #include @@ -63,14 +66,17 @@ #include #include #include +#include #include #include + namespace filament { using namespace backend; using namespace math; +using namespace utils; ShadowMapManager::ShadowMapManager(FEngine& engine) : mIsDepthClampSupported(engine.getDriverApi().isDepthClampSupported()), @@ -248,13 +254,13 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG struct ShadowPass { // 112 bytes mutable RenderPass::Executor executor; ShadowMap* shadowMap; - utils::Range range; + Range range; FScene::VisibleMaskType visibilityMask; }; // the actual shadow map atlas (currently a 2D texture array) FrameGraphId shadows; // a RenderPass per shadow map - utils::FixedCapacityVector passList; + FixedCapacityVector passList; }; auto& prepareShadowPass = fg.addPass("Prepare Shadow Pass", @@ -453,7 +459,6 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG fg.getBlackboard()["shadowmap"] = prepareShadowPass->shadows; struct ShadowPassData { - FrameGraphId tempBlurSrc{}; // temporary shadowmap when blurring FrameGraphId output; uint32_t rt{}; }; @@ -464,10 +469,7 @@ 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 msaaSamples = textureRequirements.msaaSamples; - const bool blur = entry.shadowMap->hasVisibleShadows() && - view.hasVSM() && options.vsm.blurWidth > 0.0f; auto last = first; // loop over each shadow pass to find its layer range @@ -475,8 +477,7 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG ++last; } - assert_invariant(mFeatureShadowAllocator || - std::distance(first, last) == 1); + assert_invariant(mFeatureShadowAllocator || std::distance(first, last) == 1); // And render all shadow pass of a given layer as a single render pass auto& shadowPass = fg.addPass("Shadow Pass", @@ -511,28 +512,6 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG // we need to clear the shadow map with the max EVSM moments renderTargetDesc.clearColor = textureRequirements.clearColor; renderTargetDesc.samples = msaaSamples; - - if (UTILS_UNLIKELY(blur)) { - // Temporary (resolved) texture used to render the shadowmap when blurring - // is needed -- it'll be used as the source of the blur. - data.tempBlurSrc = builder.createTexture("Temporary Shadowmap", { - .width = textureRequirements.size, - .height = textureRequirements.size, - .format = textureRequirements.format - }); - - data.tempBlurSrc = builder.write(data.tempBlurSrc, - FrameGraphTexture::Usage::COLOR_ATTACHMENT); - - data.rt = builder.declareRenderPass("Temp Shadow RT", { - .attachments = { - .color = { data.tempBlurSrc }, - .depth = depth }, - .clearColor = textureRequirements.clearColor, - .samples = msaaSamples, - .clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH - }); - } } else { // the shadowmap layer data.output = builder.write(data.output, FrameGraphTexture::Usage::DEPTH_ATTACHMENT); @@ -541,11 +520,7 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG } // finally, create the shadowmap render target -- one per layer. - auto rt = builder.declareRenderPass("Shadow RT", renderTargetDesc); - - // render either directly into the shadowmap, or to the temporary texture for - // blurring. - data.rt = blur ? data.rt : rt; + data.rt = builder.declareRenderPass("Shadow RT", renderTargetDesc); }, [=, &engine](FrameGraphResources const& resources, auto const& data, DriverApi& driver) { @@ -580,27 +555,21 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG DescriptorSet::unbind(driver, DescriptorSetBindingPoints::PER_MATERIAL); }); - first = last; - // now emit the blurring passes if needed - if (UTILS_UNLIKELY(blur)) { - auto& ppm = engine.getPostProcessManager(); - - // FIXME: this `options` is for the first shadowmap in the list, but it applies to - // the whole layer. Blurring should happen per shadowmap, not for the whole - // layer. - - // 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 = size_t(std::ceil((blurWidth - 5.0f) / 4.0f)); - kernelWidth = kernelWidth * 4 + 5; - ppm.gaussianBlurPass(fg, - shadowPass->tempBlurSrc, - shadowPass->output, - false, kernelWidth, sigma); + if (UTILS_UNLIKELY(view.hasVSM())) { + auto list = FixedCapacityVector::with_capacity(std::distance(first, last)); + for (auto curr = first; curr != last; curr++) { + auto const sm = curr->shadowMap; + const float blurWidth = sm->getShadowOptions().vsm.blurWidth; + if (sm->hasVisibleShadows() && blurWidth > 0.0f) { + list.push_back(sm); + } + } + if (!list.empty()) { + // horizontal passes... + auto const horizontal = gaussianBlurSeparatedPass(engine, fg, shadowPass->output, {}, list, {1, 0}); + // vertical passes... + gaussianBlurSeparatedPass(engine, fg, horizontal, shadowPass->output, std::move(list), {0, 1}); } } @@ -608,16 +577,192 @@ FrameGraphId ShadowMapManager::render(FEngine& engine, FrameG // 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); + // TODO: we could take a list of scissor maybe + vsmMipmapPass(engine, fg, prepareShadowPass->shadows, layer, level, textureRequirements.clearColor); } } + + // next batch of passes + first = last; } return prepareShadowPass->shadows; } +FrameGraphId ShadowMapManager::gaussianBlurSeparatedPass( + FEngine& engine, + FrameGraph& fg, + FrameGraphId const input, + FrameGraphId output, + FixedCapacityVector shadowMapList, + int2 const dir) { + + struct BlurPassData { + FrameGraphId in; + FrameGraphId out; + FixedCapacityVector list; + }; + + auto const& separatedBlurPasses = fg.addPass("Separated Gaussian Blur Pass", + [&](FrameGraph::Builder& builder, auto& data) { + auto const inDesc = builder.getDescriptor(input); + data.list = std::move(shadowMapList); + data.in = builder.sample(input); + if (!output) { + auto outDesc = inDesc; + outDesc.type = SamplerType::SAMPLER_2D_ARRAY; + outDesc.levels = 1; + outDesc.depth = 1; + output = builder.createTexture("Temporary Separated Blur Texture", outDesc); + } else { + // TODO: When we write back into the destination, we need to preserve its content + // because we don't know if we will write all of it or just parts (b/c it's + // an atlas). We could avoid this if we knew we didn't care about what's not + // written in this pass. This read() forces preservation of the content. + output = builder.read(output, FrameGraphTexture::Usage::COLOR_ATTACHMENT); + } + data.out = builder.declareRenderPass(output); + }, + [=, &engine](FrameGraphResources const& resources, auto const& data, DriverApi& driver) { + using FGTSD = FrameGraphTexture::SubResourceDescriptor; + FGTSD const& inSubDesc = resources.getSubResourceDescriptor(data.in); + auto hwOutRT = resources.getRenderPassInfo(0); + auto hwIn = resources.getTexture(data.in); + + auto& ppm = engine.getPostProcessManager(); + ppm.bindPostProcessDescriptorSet(driver); + ppm.bindPerRenderableDescriptorSet(driver); + + // get the material + auto const& separableGaussianBlur = ppm.getPostProcessMaterial("gaussian"); + auto const ma = separableGaussianBlur.getMaterial(engine, driver); + + // Generates half of a Gaussian kernel (center + one side) + auto generateGaussianWeights = [](std::array& weights, + int const radius, float const sigma) { + float sum = 0.0f; + for (int i = 0; i <= radius; ++i) { + weights[i] = std::exp(-(float(i * i) / (2.0f * sigma * sigma))); + sum += (i == 0) ? weights[i] : (2.0f * weights[i]); + } + float const invSum = 1.0f / sum; + for (int i = 0; i <= radius; ++i) { + weights[i] *= invSum; + } + }; + + auto miList = FixedCapacityVector::with_capacity(data.list.size()); + + // set-up all the MaterialInstance for each shadow map + for (ShadowMap const* const shadowMap : data.list) { + std::array kernel; + float const blurWidth = shadowMap->getShadowOptions().vsm.blurWidth; + float const sigma = blurWidth / 2.5f; + int const radius = std::min(int(std::ceil(blurWidth)), 31); + generateGaussianWeights(kernel, radius, sigma); + + backend::Viewport const scissor = shadowMap->getScissor(); + auto const mi = ppm.getMaterialInstanceManager().getMaterialInstance(ma); + mi->setScissor(scissor); + mi->setParameter("input", hwIn, SamplerParams{ + .filterMag = SamplerMagFilter::NEAREST, + .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST + }); + mi->setParameter("kernel", kernel.data(), radius + 1); + mi->setParameter("bounds", + dir.x * int2{ scissor.left, scissor.left + scissor.width - 1 } + + dir.y * int2{ scissor.bottom, scissor.bottom + scissor.height - 1 }); + mi->setParameter("dir", dir); + mi->setParameter("layer", int32_t(inSubDesc.layer)); + mi->setParameter("radius", radius); + mi->commit(driver, engine.getUboManager()); + miList.push_back(mi); + } + + // Do all the horizontal or vertical blur passes for this layer at once + driver.beginRenderPass(hwOutRT.target, hwOutRT.params); + for (auto const mi : miList) { + mi->use(driver); + driver.scissor(mi->getScissor()); + driver.draw(ppm.getPipelineState(ma), engine.getFullScreenRenderPrimitive(), 0, 3, 1); + } + driver.endRenderPass(); + + ppm.unbindAllDescriptorSets(driver); + }); + + return separatedBlurPasses->out; +} + +FrameGraphId ShadowMapManager::vsmMipmapPass( + FEngine& engine, + FrameGraph& fg, + FrameGraphId const input, uint8_t layer, size_t const level, + float4 clearColor) noexcept { + + struct VsmMipData { + FrameGraphId in; + }; + + auto const& depthMipmapPass = fg.addPass("EVSM Mipmap Pass", + [&](FrameGraph::Builder& builder, auto& data) { + data.in = builder.sample(input); + auto out = builder.createSubresource(data.in, "EVSM Mipmap level", { + .level = uint8_t(level + 1), .layer = layer }); + out = builder.write(out, FrameGraphTexture::Usage::COLOR_ATTACHMENT); + builder.declareRenderPass(builder.getName(input), { + .attachments = { .color = { out }}, + .clearColor = clearColor, + .clearFlags = TargetBufferFlags::COLOR + }); + }, + [=, &engine](FrameGraphResources const& resources, + auto const& data, DriverApi& driver) { + + auto in = driver.createTextureView(resources.getTexture(data.in), level, 1); + auto out = resources.getRenderPassInfo(); + + auto const& inDesc = resources.getDescriptor(data.in); + auto width = inDesc.width; + assert_invariant(width == inDesc.height); + uint32_t const dim = std::max(1u, width >> (level + 1)); + + auto& ppm = engine.getPostProcessManager(); + ppm.bindPostProcessDescriptorSet(driver); + ppm.bindPerRenderableDescriptorSet(driver); + + auto& material = ppm.getPostProcessMaterial("vsmMipmap"); + FMaterial const* const ma = material.getMaterial(engine, driver); + + auto const pipeline = ppm.getPipelineState(ma); + backend::Viewport const scissor = { 0, 0, dim, dim }; + + auto const mi = ppm.getMaterialInstanceManager().getMaterialInstance(ma); + mi->setParameter("color", in, SamplerParams{ + .filterMag = SamplerMagFilter::NEAREST, + .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST + }); + mi->setParameter("layer", uint32_t(layer)); + mi->commit(driver, engine.getUboManager()); + + + // Do all the horizontal or vertical blur passes for this layer at once + driver.beginRenderPass(out.target, out.params); + { + mi->use(driver); + driver.scissor(scissor); + driver.draw(pipeline, engine.getFullScreenRenderPrimitive(), 0, 3, 1); + } + driver.endRenderPass(); + + driver.destroyTexture(in); // `in` is just a view on `data.in` + ppm.unbindAllDescriptorSets(driver); + }); + + return depthMipmapPass->in; +} + ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEngine& engine, FView& view, CameraInfo cameraInfo, FScene::RenderableSoa& renderableData, FScene::LightSoa const& lightData, ShadowMap::SceneInfo sceneInfo) noexcept { @@ -647,7 +792,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng }; bool hasVisibleShadows = false; - utils::Slice const cascadedShadowMaps = getCascadedShadowMap(); + 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. @@ -857,7 +1002,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin void ShadowMapManager::cullSpotShadowMap(ShadowMap const& shadowMap, FEngine const& engine, FView const& view, - FScene::RenderableSoa& renderableData, utils::Range range, + FScene::RenderableSoa& renderableData, Range range, FScene::LightSoa const& lightData) noexcept { auto& lcm = engine.getLightManager(); @@ -949,7 +1094,7 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap, } void ShadowMapManager::cullPointShadowMap(ShadowMap const& shadowMap, FView const& view, - FScene::RenderableSoa& renderableData, utils::Range const range, + FScene::RenderableSoa& renderableData, Range const range, FScene::LightSoa const& lightData) noexcept { uint8_t const face = shadowMap.getFace(); @@ -999,7 +1144,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine lightData.data()); ShadowTechnique shadowTechnique{}; - utils::Slice const spotShadowMaps = getSpotShadowMaps(); + Slice const spotShadowMaps = getSpotShadowMaps(); if (!spotShadowMaps.empty()) { shadowTechnique |= ShadowTechnique::SHADOW_MAP; for (ShadowMap const& shadowMap : spotShadowMaps) { @@ -1196,11 +1341,11 @@ void ShadowMapManager::updateNearFarPlanes(mat4f* projection, p[3].z = (sf * F.w - sn * N.w) * 0.5f; } -utils::FixedCapacityVector +FixedCapacityVector ShadowMapManager::getDirectionalShadowCameras() const noexcept { if (!mInitialized) return {}; auto const csm = getCascadedShadowMap(); - auto result = utils::FixedCapacityVector::with_capacity(csm.size()); + auto result = FixedCapacityVector::with_capacity(csm.size()); for (ShadowMap const& sm : csm) { result.push_back(sm.hasVisibleShadows() ? sm.getDebugCamera() : nullptr); } diff --git a/filament/src/ShadowMapManager.h b/filament/src/ShadowMapManager.h index 712b24db4f..4b65c1ca0f 100644 --- a/filament/src/ShadowMapManager.h +++ b/filament/src/ShadowMapManager.h @@ -138,6 +138,21 @@ public: RenderPassBuilder const& passBuilder, FView& view, CameraInfo const& mainCameraInfo, math::float4 const& userTime) noexcept; + FrameGraphId gaussianBlurSeparatedPass( + FEngine& engine, + FrameGraph& fg, + FrameGraphId input, + FrameGraphId output, + utils::FixedCapacityVector shadowMapList, + math::int2 dir); + + FrameGraphId vsmMipmapPass( + FEngine& engine, + FrameGraph& fg, + FrameGraphId input, uint8_t layer, size_t level, + math::float4 clearColor) noexcept; + + // valid after calling update() above ShadowMappingUniforms getShadowMappingUniforms() const noexcept { return mShadowMappingUniforms; diff --git a/filament/src/fg/FrameGraphRenderPass.h b/filament/src/fg/FrameGraphRenderPass.h index e53cfda491..f6437e2498 100644 --- a/filament/src/fg/FrameGraphRenderPass.h +++ b/filament/src/fg/FrameGraphRenderPass.h @@ -62,7 +62,6 @@ struct FrameGraphRenderPass { uint8_t samples = 0; // # of samples (0 = unset, default) uint8_t layerCount = 1; // # of layer (# > 1 = multiview) backend::TargetBufferFlags clearFlags{}; - backend::TargetBufferFlags discardStart{}; }; struct ImportDescriptor { diff --git a/filament/src/materials/evsm/evsm.cpp b/filament/src/materials/evsm/evsm.cpp new file mode 100644 index 0000000000..cc477791f1 --- /dev/null +++ b/filament/src/materials/evsm/evsm.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "evsm.h" + +#include "generated/resources/evsm.h" + +#include + +#include + +#include + +#include + +namespace filament { +static const StaticMaterialInfo sMaterialList[] = { + {"vsmMipmap", MATERIAL(EVSM, VSMMIPMAP)}, + {"gaussian", MATERIAL(EVSM, GAUSSIAN)}, +}; + +utils::Slice getEvsmMaterialList() noexcept { + return { std::begin(sMaterialList), std::end(sMaterialList) }; +} + +} // namespace filament diff --git a/filament/src/materials/evsm/evsm.h b/filament/src/materials/evsm/evsm.h new file mode 100644 index 0000000000..13d5aea32f --- /dev/null +++ b/filament/src/materials/evsm/evsm.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#pragma once + +#include + +#include + +namespace filament { + +utils::Slice getEvsmMaterialList() noexcept; + +} // namespace filament diff --git a/filament/src/materials/evsm/gaussian.fs b/filament/src/materials/evsm/gaussian.fs new file mode 100644 index 0000000000..27b4cd269e --- /dev/null +++ b/filament/src/materials/evsm/gaussian.fs @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +void postProcess(inout PostProcessInputs postProcess) { + int layer = materialParams.layer; + int radius = materialParams.radius; + ivec2 dir = materialParams.dir; + highp int atlasMin = materialParams.bounds.x; + highp int atlasMax = materialParams.bounds.y; + + // 1. Get the exact integer coordinate of the current pixel + highp ivec2 centerCoord = ivec2(gl_FragCoord.xy); + + // 2. Fetch the center pixel and pre-scale it (Safe FP16 FMA) + highp ivec2 clampedCenter = clamp(centerCoord, atlasMin, atlasMax); + highp vec4 result = texelFetch(materialParams_input, ivec3(clampedCenter, layer), 0) * materialParams.kernel[0]; + + // 3. Accumulate the positive and negative offsets + // Using a constant loop bound allows the shader compiler to perfectly unroll this. + for (int i = 1; i <= radius; ++i) { + ivec2 offset = dir * i; + + // Clamp the fetch coordinates strictly to the Atlas sub-rectangle boundary + highp ivec2 posCoord = clamp(centerCoord + offset, atlasMin, atlasMax); + highp ivec2 negCoord = clamp(centerCoord - offset, atlasMin, atlasMax); + + // Pre-scaled accumulation to strictly prevent FP16 +Inf overflow + highp float w = materialParams.kernel[i]; + result += texelFetch(materialParams_input, ivec3(posCoord, layer), 0) * w; + result += texelFetch(materialParams_input, ivec3(negCoord, layer), 0) * w; + } + + postProcess.color = result; +} diff --git a/filament/src/materials/evsm/gaussian.mat b/filament/src/materials/evsm/gaussian.mat new file mode 100644 index 0000000000..6224662a79 --- /dev/null +++ b/filament/src/materials/evsm/gaussian.mat @@ -0,0 +1,52 @@ +material { + name : gaussian, + parameters : [ + { + type : sampler2dArray, + name : input, + precision: high + }, + { + type : float[32], + name : kernel, + precision: high + }, + { + type : int2, + name : bounds, + precision: high + }, + { + type : int2, + name : dir + }, + { + type : int, + name : layer + }, + { + type : int, + name : radius + } + ], + outputs : [ + { + name : color, + target : color, + type : float4, + precision : high + } + ], + constants : [ + ], + variables : [ + ], + domain : postprocess, + depthWrite : false, + depthCulling : false, + culling: none +} + +fragment { + #include "gaussian.fs" +} diff --git a/filament/src/materials/vsmMipmap.mat b/filament/src/materials/evsm/vsmMipmap.mat similarity index 96% rename from filament/src/materials/vsmMipmap.mat rename to filament/src/materials/evsm/vsmMipmap.mat index 822f942ded..b7f3198c9c 100644 --- a/filament/src/materials/vsmMipmap.mat +++ b/filament/src/materials/evsm/vsmMipmap.mat @@ -9,10 +9,6 @@ material { { type : int, name : layer - }, - { - type : float, - name : uvscale } ], outputs : [