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`.
This commit is contained in:
Mathias Agopian
2026-03-30 20:55:02 -07:00
committed by GitHub
parent ae48711ae2
commit ffcccb336e
11 changed files with 423 additions and 143 deletions

View File

@@ -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")

View File

@@ -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<FrameGraphTexture> PostProcessManager::generateGaussianMipmap(Frame
FrameGraphId<FrameGraphTexture> PostProcessManager::gaussianBlurPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> const input,
FrameGraphId<FrameGraphTexture> output,
bool const reinhard, size_t kernelWidth, const float sigma) noexcept {
bool const reinhard, size_t kernelWidth, const float sigma,
std::optional<backend::Viewport> scissor) noexcept {
auto computeGaussianCoefficients =
[kernelWidth, sigma](float2* kernel, size_t const size) -> size_t {
@@ -1387,6 +1391,10 @@ FrameGraphId<FrameGraphTexture> 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<FrameGraphTexture> 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<FrameGraphTexture> PostProcessManager::resolveDepthWithShader(Frame
return ppResolve->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> const input, uint8_t layer, size_t const level,
float4 clearColor) noexcept {
struct VsmMipData {
FrameGraphId<FrameGraphTexture> in;
};
auto const& depthMipmapPass = fg.addPass<VsmMipData>("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<FrameGraphTexture> PostProcessManager::debugShadowCascades(FrameGraph& fg,
ShadowMapManager const& smm,
FrameGraphId<FrameGraphTexture> const input,

View File

@@ -312,15 +312,11 @@ public:
utils::StaticString outputBufferName, FrameGraphId<FrameGraphTexture> input,
FrameGraphTexture::Descriptor outDesc) noexcept;
// VSM shadow mipmap pass
FrameGraphId<FrameGraphTexture> vsmMipmapPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, uint8_t layer, size_t level,
math::float4 clearColor) noexcept;
FrameGraphId<FrameGraphTexture> gaussianBlurPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
FrameGraphId<FrameGraphTexture> output,
bool reinhard, size_t kernelWidth, float sigma) noexcept;
bool reinhard, size_t kernelWidth, float sigma,
std::optional<backend::Viewport> scissor = {}) noexcept;
FrameGraphId<FrameGraphTexture> 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;

View File

@@ -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 <backend/DriverApiForward.h>
#include <backend/DriverEnums.h>
#include <backend/PipelineState.h>
#include <utils/compiler.h>
#include <utils/debug.h>
@@ -53,6 +55,7 @@
#include <math/half.h>
#include <math/mat4.h>
#include <math/vec4.h>
#include <math/vec2.h>
#include <math/scalar.h>
#include <algorithm>
@@ -63,14 +66,17 @@
#include <limits>
#include <new>
#include <memory>
#include <utility>
#include <stdint.h>
#include <stddef.h>
namespace filament {
using namespace backend;
using namespace math;
using namespace utils;
ShadowMapManager::ShadowMapManager(FEngine& engine)
: mIsDepthClampSupported(engine.getDriverApi().isDepthClampSupported()),
@@ -248,13 +254,13 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
struct ShadowPass { // 112 bytes
mutable RenderPass::Executor executor;
ShadowMap* shadowMap;
utils::Range<uint32_t> range;
Range<uint32_t> range;
FScene::VisibleMaskType visibilityMask;
};
// the actual shadow map atlas (currently a 2D texture array)
FrameGraphId<FrameGraphTexture> shadows;
// a RenderPass per shadow map
utils::FixedCapacityVector<ShadowPass> passList;
FixedCapacityVector<ShadowPass> passList;
};
auto& prepareShadowPass = fg.addPass<PrepareShadowPassData>("Prepare Shadow Pass",
@@ -453,7 +459,6 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
fg.getBlackboard()["shadowmap"] = prepareShadowPass->shadows;
struct ShadowPassData {
FrameGraphId<FrameGraphTexture> tempBlurSrc{}; // temporary shadowmap when blurring
FrameGraphId<FrameGraphTexture> output;
uint32_t rt{};
};
@@ -464,10 +469,7 @@ FrameGraphId<FrameGraphTexture> 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<FrameGraphTexture> 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<ShadowPassData>("Shadow Pass",
@@ -511,28 +512,6 @@ FrameGraphId<FrameGraphTexture> 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<FrameGraphTexture> 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<FrameGraphTexture> 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<ShadowMap const*>::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<FrameGraphTexture> 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<FrameGraphTexture> ShadowMapManager::gaussianBlurSeparatedPass(
FEngine& engine,
FrameGraph& fg,
FrameGraphId<FrameGraphTexture> const input,
FrameGraphId<FrameGraphTexture> output,
FixedCapacityVector<ShadowMap const*> shadowMapList,
int2 const dir) {
struct BlurPassData {
FrameGraphId<FrameGraphTexture> in;
FrameGraphId<FrameGraphTexture> out;
FixedCapacityVector<ShadowMap const*> list;
};
auto const& separatedBlurPasses = fg.addPass<BlurPassData>("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<float, 32>& 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<FMaterialInstance*>::with_capacity(data.list.size());
// set-up all the MaterialInstance for each shadow map
for (ShadowMap const* const shadowMap : data.list) {
std::array<float, 32> 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<FrameGraphTexture> ShadowMapManager::vsmMipmapPass(
FEngine& engine,
FrameGraph& fg,
FrameGraphId<FrameGraphTexture> const input, uint8_t layer, size_t const level,
float4 clearColor) noexcept {
struct VsmMipData {
FrameGraphId<FrameGraphTexture> in;
};
auto const& depthMipmapPass = fg.addPass<VsmMipData>("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<ShadowMap> const cascadedShadowMaps = getCascadedShadowMap();
Slice<ShadowMap> 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<uint32_t> range,
FScene::RenderableSoa& renderableData, Range<uint32_t> 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<uint32_t> const range,
FScene::RenderableSoa& renderableData, Range<uint32_t> const range,
FScene::LightSoa const& lightData) noexcept {
uint8_t const face = shadowMap.getFace();
@@ -999,7 +1144,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
lightData.data<FScene::SHADOW_INFO>());
ShadowTechnique shadowTechnique{};
utils::Slice<const ShadowMap> const spotShadowMaps = getSpotShadowMaps();
Slice<const ShadowMap> 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<Camera const*>
FixedCapacityVector<Camera const*>
ShadowMapManager::getDirectionalShadowCameras() const noexcept {
if (!mInitialized) return {};
auto const csm = getCascadedShadowMap();
auto result = utils::FixedCapacityVector<Camera const*>::with_capacity(csm.size());
auto result = FixedCapacityVector<Camera const*>::with_capacity(csm.size());
for (ShadowMap const& sm : csm) {
result.push_back(sm.hasVisibleShadows() ? sm.getDebugCamera() : nullptr);
}

View File

@@ -138,6 +138,21 @@ public:
RenderPassBuilder const& passBuilder,
FView& view, CameraInfo const& mainCameraInfo, math::float4 const& userTime) noexcept;
FrameGraphId<FrameGraphTexture> gaussianBlurSeparatedPass(
FEngine& engine,
FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
FrameGraphId<FrameGraphTexture> output,
utils::FixedCapacityVector<ShadowMap const*> shadowMapList,
math::int2 dir);
FrameGraphId<FrameGraphTexture> vsmMipmapPass(
FEngine& engine,
FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, uint8_t layer, size_t level,
math::float4 clearColor) noexcept;
// valid after calling update() above
ShadowMappingUniforms getShadowMappingUniforms() const noexcept {
return mShadowMappingUniforms;

View File

@@ -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 {

View File

@@ -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 <materials/StaticMaterialInfo.h>
#include <utils/Slice.h>
#include <iterator>
#include <stddef.h>
namespace filament {
static const StaticMaterialInfo sMaterialList[] = {
{"vsmMipmap", MATERIAL(EVSM, VSMMIPMAP)},
{"gaussian", MATERIAL(EVSM, GAUSSIAN)},
};
utils::Slice<const StaticMaterialInfo> getEvsmMaterialList() noexcept {
return { std::begin(sMaterialList), std::end(sMaterialList) };
}
} // namespace filament

View File

@@ -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 <materials/StaticMaterialInfo.h>
#include <utils/Slice.h>
namespace filament {
utils::Slice<const StaticMaterialInfo> getEvsmMaterialList() noexcept;
} // namespace filament

View File

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

View File

@@ -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"
}

View File

@@ -9,10 +9,6 @@ material {
{
type : int,
name : layer
},
{
type : float,
name : uvscale
}
],
outputs : [