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:
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
39
filament/src/materials/evsm/evsm.cpp
Normal file
39
filament/src/materials/evsm/evsm.cpp
Normal 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
|
||||
28
filament/src/materials/evsm/evsm.h
Normal file
28
filament/src/materials/evsm/evsm.h
Normal 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
|
||||
47
filament/src/materials/evsm/gaussian.fs
Normal file
47
filament/src/materials/evsm/gaussian.fs
Normal 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;
|
||||
}
|
||||
52
filament/src/materials/evsm/gaussian.mat
Normal file
52
filament/src/materials/evsm/gaussian.mat
Normal 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"
|
||||
}
|
||||
@@ -9,10 +9,6 @@ material {
|
||||
{
|
||||
type : int,
|
||||
name : layer
|
||||
},
|
||||
{
|
||||
type : float,
|
||||
name : uvscale
|
||||
}
|
||||
],
|
||||
outputs : [
|
||||
Reference in New Issue
Block a user