EVSM improvements (#9758)

- refactoring/clecanup to make some changes easier
- VSM mipmap generation was mistakenly disable when blur radius was 0
- analytic variance was disabled because the math only worked for VSM. Fixed the math.
- better handling of large blurs when using fp32
- implement EVSM equivalent of receiver plane normal bias
- use correct EVSM clearing color
- mipmapping with point lights works much better (no seams)
- min variance is computed automatically
- custom high precision mipmaping shader for VSM
This commit is contained in:
Mathias Agopian
2026-03-02 16:46:07 -08:00
committed by GitHub
parent cf66813f41
commit 6f0d47f275
19 changed files with 350 additions and 250 deletions

View File

@@ -2121,7 +2121,7 @@ public class View {
*/
public boolean highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
public float minVarianceScale = 0.5f;
/**

View File

@@ -372,7 +372,7 @@ public:
} vsm;
/**
* Light bulb radius used for soft shadows. Currently this is only used when DPCF or PCSS is
* Light bulb radius used for soft shadows. Currently, this is only used when DPCF or PCSS is
* enabled. (2cm by default).
*/
float shadowBulbRadius = 0.02f;

View File

@@ -735,7 +735,7 @@ struct VsmShadowOptions {
bool highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
float minVarianceScale = 0.5f;

View File

@@ -3809,15 +3809,13 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg
auto const& inDesc = resources.getDescriptor(data.in);
auto width = inDesc.width;
assert_invariant(width == inDesc.height);
int const dim = width >> (level + 1);
uint32_t const dim = std::max(1u, width >> (level + 1));
auto& material = getPostProcessMaterial("vsmMipmap");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
// When generating shadow map mip levels, we want to preserve the 1 texel border.
// (note clearing never respects the scissor in Filament)
auto const pipeline = getPipelineState(ma);
backend::Viewport const scissor = { 1u, 1u, dim - 2u, dim - 2u };
backend::Viewport const scissor = { 0, 0, dim, dim };
FMaterialInstance* const mi = getMaterialInstance(ma);
mi->setParameter("color", in, SamplerParams{

View File

@@ -62,6 +62,8 @@ ShadowMap::ShadowMap(FEngine& engine) noexcept
: mPerShadowMapUniforms(engine),
mShadowType(ShadowType::DIRECTIONAL),
mHasVisibleShadows(false),
mVsm(false),
mReservedBit(false),
mFace(0) {
Entity entities[2];
engine.getEntityManager().create(2, entities);
@@ -85,7 +87,7 @@ void ShadowMap::terminate(FEngine& engine) {
ShadowMap::~ShadowMap() = default;
void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType,
void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType, bool const vsm,
uint16_t const shadowIndex, uint8_t const face,
LightManager::ShadowOptions const* options) {
mLightIndex = lightIndex;
@@ -93,6 +95,7 @@ void ShadowMap::initialize(size_t const lightIndex, ShadowType const shadowType,
mOptions = options;
mShadowType = shadowType;
mFace = face;
mVsm = vsm;
}
mat4f ShadowMap::getDirectionalLightViewMatrix(float3 direction, float3 up,
@@ -1181,9 +1184,8 @@ float2 ShadowMap::texelSizeWorldSpace(mat4f const& S, uint16_t const shadowDimen
return s;
}
template<typename Casters, typename Receivers>
void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers,
Casters casters, Receivers receivers) noexcept {
template<typename Visitor>
void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers, Visitor visitor) noexcept {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
using State = FRenderableManager::Visibility;
@@ -1196,14 +1198,11 @@ void ShadowMap::visitScene(const FScene& scene, uint32_t const visibleLayers,
size_t const c = soa.size();
for (size_t i = 0; i < c; i++) {
if (layers[i] & visibleLayers) {
const Aabb aabb{ worldAABBCenter[i] - worldAABBExtent[i],
worldAABBCenter[i] + worldAABBExtent[i] };
if (visibility[i].castShadows) {
casters(aabb, visibleMasks[i]);
}
if (visibility[i].receiveShadows) {
receivers(aabb, visibleMasks[i]);
}
Aabb const aabb{
worldAABBCenter[i] - worldAABBExtent[i],
worldAABBCenter[i] + worldAABBExtent[i]
};
visitor(aabb, visibleMasks[i], visibility[i]);
}
}
}
@@ -1222,13 +1221,15 @@ ShadowMap::SceneInfo::SceneInfo(
wsShadowCastersVolume = {};
wsShadowReceiversVolume = {};
visitScene(scene, visibleLayers,
[&](Aabb caster, Culler::result_type) {
wsShadowCastersVolume.min = min(wsShadowCastersVolume.min, caster.min);
wsShadowCastersVolume.max = max(wsShadowCastersVolume.max, caster.max);
},
[&](Aabb receiver, Culler::result_type) {
wsShadowReceiversVolume.min = min(wsShadowReceiversVolume.min, receiver.min);
wsShadowReceiversVolume.max = max(wsShadowReceiversVolume.max, receiver.max);
[this](Aabb const& aabb, Culler::result_type, FRenderableManager::Visibility const visibility) {
if (visibility.castShadows) {
wsShadowCastersVolume.min = min(wsShadowCastersVolume.min, aabb.min);
wsShadowCastersVolume.max = max(wsShadowCastersVolume.max, aabb.max);
}
if (visibility.receiveShadows) {
wsShadowReceiversVolume.min = min(wsShadowReceiversVolume.min, aabb.min);
wsShadowReceiversVolume.max = max(wsShadowReceiversVolume.max, aabb.max);
}
}
);
}
@@ -1242,18 +1243,20 @@ void ShadowMap::updateSceneInfoDirectional(mat4f const& Mv, FScene const& scene,
sceneInfo.lsCastersNearFar = { std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max() };
sceneInfo.lsReceiversNearFar = { std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max() };
visitScene(scene, sceneInfo.visibleLayers,
[&](Aabb caster, Culler::result_type) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster);
sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z);
sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z);
},
[&](Aabb receiver, Culler::result_type const vis) {
// account only for objects that are visible by the camera
auto mask = 1u << VISIBLE_RENDERABLE_BIT;
if ((vis & mask) == mask) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, receiver);
sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z);
sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z);
[&](Aabb const& aabb, Culler::result_type const vis, FRenderableManager::Visibility const visibility) {
if (visibility.castShadows) {
auto const r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb);
sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z);
sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z);
}
if (visibility.castShadows) {
// account only for objects that are visible by the camera
constexpr auto mask = 1u << VISIBLE_RENDERABLE_BIT;
if ((vis & mask) == mask) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb);
sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z);
sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z);
}
}
}
);
@@ -1268,15 +1271,15 @@ void ShadowMap::updateSceneInfoSpot(mat4f const& Mv, FScene const& scene,
sceneInfo.lsCastersNearFar = { std::numeric_limits<float>::lowest(), std::numeric_limits<float>::max() };
// account only for objects that are visible by both the camera and the light
visitScene(scene, sceneInfo.visibleLayers,
[&](Aabb caster, Culler::result_type const vis) {
auto mask = VISIBLE_DYN_SHADOW_RENDERABLE;
if ((vis & mask) == mask) {
auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster);
sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near
sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far
[&](Aabb const& aabb, Culler::result_type const vis, FRenderableManager::Visibility const visibility) {
if (visibility.castShadows) {
constexpr auto mask = VISIBLE_DYN_SHADOW_RENDERABLE;
if ((vis & mask) == mask) {
auto const r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, aabb);
sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near
sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far
}
}
},
[&](Aabb receiver, Culler::result_type) {
}
);
}
@@ -1286,47 +1289,56 @@ void ShadowMap::setAllocation(uint8_t const layer, backend::Viewport viewport) n
}
backend::Viewport ShadowMap::getViewport() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the texture.
// This happens only for directional lights when "focus shadow casters" is used,
// or when shadowFar is smaller than the camera far.
// For spot- and point-lights we also use a 1-texel border, so that bilinear filtering
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp).
// This is used for calculating the light space; in particular, if the shadowmap is in a 2D atlas, this
// returns the valid area of the shadowmap. By definition all values must be integer.
// We set a viewport with a 1-texel border for when we index outside the texture, which should only happen for
// directional lights when "focus shadow casters" is used, or when shadowFar is smaller than the camera far.
// For directional lights, the border is filed with "fully lit".
//
// For point-lights, we also use a 1-texel border, but for a different reason; the border is filled with data
// so that bilinear (PCF) filtering works properly.
//
// Spot-light a treated like point lights (the border will be filled with "fully-lit" anyways)
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
}
backend::Viewport ShadowMap::getScissor() const noexcept {
// We set a viewport with a 1-texel border for when we index outside the texture.
// This happens only for directional lights when "focus shadow casters" is used,
// or when shadowFar is smaller than the camera far.
// For spot- and point-lights we also use a 1-texel border, so that bilinear filtering
// can work properly if the shadowmap is in an atlas (and we can't rely on h/w clamp), so we
// don't scissor the border, so it gets filled with correct neighboring texels.
// This is used while rendering the shadowmap.
const uint32_t dim = mOptions->mapSize;
const uint16_t border = 1u;
switch (mShadowType) {
case ShadowType::DIRECTIONAL:
return { mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border };
switch (mShadowType) { // NOLINT(*-multiway-paths-covered)
case ShadowType::DIRECTIONAL: {
// Don't render anything into the border; it's already filled with "fully lit".
return {mOffset.x + border, mOffset.y + border, dim - 2u * border, dim - 2u * border};
}
case ShadowType::SPOT:
case ShadowType::POINT:
return { mOffset.x, mOffset.y, dim, dim };
case ShadowType::POINT: {
// Render into the border (which will get the adjacent faces texels)
return {mOffset.x, mOffset.y, dim, dim};
}
}
return {};
}
float4 ShadowMap::getClampToEdgeCoords(ShadowMapInfo const& shadowMapInfo) const noexcept {
float border; // shadowmap border in texels
switch (mShadowType) {
// This is used to clamp the texture coordinates when sampling the shadowmap
float border = 0; // shadowmap border in texels
switch (mShadowType) { // NOLINT(*-multiway-paths-covered)
case ShadowType::DIRECTIONAL:
// For directional lights, we need to allow the sampling to reach the border, it
// happens when "focus shadow casters" is used for instance.
// For directional lights, we need to allow the sampling to reach the center of border texels,
// but no further, so we don't read into the adjacent texture in the 2D atlas (taking bilinear filtering
// into account).
border = 0.5f;
break;
case ShadowType::SPOT:
case ShadowType::POINT:
// For spot and point light, this is equal to the viewport. i.e. the valid
// texels are inside the viewport (w/ 1-texel border), the border will be used
// for bilinear filtering.
// For point-light, the border is only needed for bilinear filtering (of the other faces)
border = 1.0f;
break;
}
@@ -1355,8 +1367,8 @@ void ShadowMap::prepareCamera(Transaction const& transaction,
}
void ShadowMap::prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept {
ShadowMapDescriptorSet::prepareViewport(transaction, viewport);
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept {
ShadowMapDescriptorSet::prepareViewport(transaction, physicalViewport, logicalViewport);
}
void ShadowMap::prepareTime(Transaction const& transaction,
@@ -1370,8 +1382,8 @@ void ShadowMap::prepareMaterialGlobals(Transaction const& transaction,
}
void ShadowMap::prepareShadowMapping(Transaction const& transaction,
bool const highPrecision) noexcept {
ShadowMapDescriptorSet::prepareShadowMapping(transaction, highPrecision);
float const vsmExponent, float const vsmMaxMoment) noexcept {
ShadowMapDescriptorSet::prepareShadowMapping(transaction, vsmExponent, vsmMaxMoment);
}
ShadowMapDescriptorSet::Transaction ShadowMap::open(DriverApi& driver) noexcept {

View File

@@ -128,7 +128,7 @@ public:
static math::mat4f getPointLightViewMatrix(backend::TextureCubemapFace face,
math::float3 position) noexcept;
void initialize(size_t lightIndex, ShadowType shadowType, uint16_t shadowIndex, uint8_t face,
void initialize(size_t lightIndex, ShadowType shadowType, bool vsm, uint16_t shadowIndex, uint8_t face,
LightManager::ShadowOptions const* options);
struct ShaderParameters {
@@ -173,7 +173,10 @@ public:
static void updateSceneInfoSpot(const math::mat4f& Mv, FScene const& scene,
SceneInfo& sceneInfo);
LightManager::ShadowOptions const* getShadowOptions() const noexcept { return mOptions; }
LightManager::ShadowOptions const& getShadowOptions() const noexcept {
assert_invariant(mOptions);
return *mOptions;
}
size_t getLightIndex() const { return mLightIndex; }
uint16_t getShadowIndex() const { return mShadowIndex; }
void setAllocation(uint8_t layer, backend::Viewport viewport) noexcept;
@@ -193,13 +196,13 @@ public:
static void prepareCamera(Transaction const& transaction,
FEngine const& engine, const CameraInfo& cameraInfo) noexcept;
static void prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept;
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept;
static void prepareTime(Transaction const& transaction,
FEngine const& engine, math::float4 const& userTime) noexcept;
static void prepareMaterialGlobals(Transaction const& transaction,
std::array<math::float4, 4> const& materialGlobals) noexcept;
static void prepareShadowMapping(Transaction const& transaction,
bool highPrecision) noexcept;
float vsmExponent, float vsmMaxMoment) noexcept;
static ShadowMapDescriptorSet::Transaction open(backend::DriverApi& driver) noexcept;
void commit(Transaction& transaction, FEngine& engine, backend::DriverApi& driver) const noexcept;
void bind(backend::DriverApi& driver) const noexcept;
@@ -273,9 +276,8 @@ private:
static inline math::float4 computeBoundingSphere(
math::float3 const* vertices, size_t count) noexcept;
template<typename Casters, typename Receivers>
static void visitScene(FScene const& scene, uint32_t visibleLayers,
Casters casters, Receivers receivers) noexcept;
template<typename Visitor>
static void visitScene(FScene const& scene, uint32_t visibleLayers, Visitor visitor) noexcept;
static inline Aabb compute2DBounds(const math::mat4f& lightView,
math::float3 const* wsVertices, size_t count) noexcept;
@@ -349,9 +351,11 @@ private:
uint8_t mLayer = 0; // our layer in the shadowMap texture // 1
ShadowType mShadowType : 2; // :2
bool mHasVisibleShadows : 1; // :1
bool mVsm : 1; // :1
UTILS_UNUSED bool mReservedBit : 1; // :1
uint8_t mFace : 3; // :3
math::ushort2 mOffset{}; // 4
UTILS_UNUSED uint8_t reserved[4]; // 4
UTILS_UNUSED uint8_t reserved[4] = {}; // 4
};
} // namespace filament

View File

@@ -151,12 +151,14 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::update(
mDirectionalShadowMapCount = builder.mDirectionalShadowMapCount;
mSpotShadowMapCount = builder.mSpotShadowMapCount;
const bool vsm = view.hasVSM();
for (auto const& entry : builder.mShadowMaps) {
auto& shadowMap = getShadowMap(entry.shadowIndex);
shadowMap.initialize(
entry.lightIndex,
entry.shadowType,
vsm,
entry.shadowIndex,
entry.face,
entry.options);
@@ -230,15 +232,12 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
FView& view, CameraInfo const& mainCameraInfo,
float4 const& userTime) noexcept {
const float moment2 = std::numeric_limits<half>::max();
const float moment1 = std::sqrt(moment2);
const float4 vsmClearColor{ moment1, moment2, -moment1, moment2 };
FScene* scene = view.getScene();
assert_invariant(scene);
// make a copy here, because it's a very small structure
const TextureAtlasRequirements textureRequirements = mTextureAtlasRequirements;
VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions();
TextureAtlasRequirements const textureRequirements = mTextureAtlasRequirements;
assert_invariant(textureRequirements.layers <= CONFIG_MAX_SHADOW_LAYERS);
// -------------------------------------------------------------------------------------------
@@ -258,8 +257,6 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
utils::FixedCapacityVector<ShadowPass> passList;
};
VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions();
auto& prepareShadowPass = fg.addPass<PrepareShadowPassData>("Prepare Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
data.passList.reserve(getMaxShadowMapCount());
@@ -392,16 +389,18 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
auto transaction = ShadowMap::open(driver);
ShadowMap::prepareCamera(transaction, engine, cameraInfo);
ShadowMap::prepareViewport(transaction, shadowMap.getViewport());
ShadowMap::prepareViewport(transaction,
{ 0, 0, textureRequirements.size, textureRequirements.size },
shadowMap.getViewport());
ShadowMap::prepareTime(transaction, engine, userTime);
ShadowMap::prepareMaterialGlobals(transaction, view.getMaterialGlobals());
ShadowMap::prepareShadowMapping(transaction,
vsmShadowOptions.highPrecision);
getWrapExponentEVSM(vsmShadowOptions, shadowMap.getShadowOptions()),
getMaxMomentEVSM(vsmShadowOptions));
shadowMap.commit(transaction, engine, driver);
// updatePrimitivesLod must be run before RenderPass::appendCommands.
FView::updatePrimitivesLod(scene->getRenderableData(),
engine, cameraInfo, entry.range);
FView::updatePrimitivesLod(scene->getRenderableData(), engine, cameraInfo, entry.range);
// generate and sort the commands for rendering the shadow map
@@ -430,10 +429,10 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
entry.executor = pass.getExecutor();
if (!view.hasVSM()) {
auto const* options = shadowMap.getShadowOptions();
auto const& options = shadowMap.getShadowOptions();
PolygonOffset const polygonOffset = { // handle reversed Z
.slope = -options->polygonOffsetSlope,
.constant = -options->polygonOffsetConstant
.slope = -options.polygonOffsetSlope,
.constant = -options.polygonOffsetConstant
};
entry.executor.overridePolygonOffset(&polygonOffset);
}
@@ -465,10 +464,10 @@ FrameGraphId<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& options = entry.shadowMap->getShadowOptions();
const auto msaaSamples = textureRequirements.msaaSamples;
const bool blur = entry.shadowMap->hasVisibleShadows() &&
view.hasVSM() && options->vsm.blurWidth > 0.0f;
view.hasVSM() && options.vsm.blurWidth > 0.0f;
auto last = first;
// loop over each shadow pass to find its layer range
@@ -508,10 +507,9 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
renderTargetDesc.attachments.color[0] = data.output;
renderTargetDesc.attachments.depth = depth;
renderTargetDesc.clearFlags =
TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
renderTargetDesc.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
// we need to clear the shadow map with the max EVSM moments
renderTargetDesc.clearColor = vsmClearColor;
renderTargetDesc.clearColor = textureRequirements.clearColor;
renderTargetDesc.samples = msaaSamples;
if (UTILS_UNLIKELY(blur)) {
@@ -530,16 +528,14 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
.attachments = {
.color = { data.tempBlurSrc },
.depth = depth },
.clearColor = vsmClearColor,
.clearColor = textureRequirements.clearColor,
.samples = msaaSamples,
.clearFlags = TargetBufferFlags::COLOR
| TargetBufferFlags::DEPTH
.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH
});
}
} else {
// the shadowmap layer
data.output = builder.write(data.output,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
data.output = builder.write(data.output, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
renderTargetDesc.attachments.depth = data.output;
renderTargetDesc.clearFlags = TargetBufferFlags::DEPTH;
}
@@ -594,27 +590,27 @@ FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameG
// the whole layer. Blurring should happen per shadowmap, not for the whole
// layer.
const float blurWidth = options->vsm.blurWidth;
// FIXME: this Gaussian blur is not precise enough for EVSM
const float blurWidth = options.vsm.blurWidth;
if (blurWidth > 0.0f) {
const float sigma = (blurWidth + 1.0f) / 6.0f;
size_t kernelWidth = std::ceil((blurWidth - 5.0f) / 4.0f);
size_t kernelWidth = size_t(std::ceil((blurWidth - 5.0f) / 4.0f));
kernelWidth = kernelWidth * 4 + 5;
ppm.gaussianBlurPass(fg,
shadowPass->tempBlurSrc,
shadowPass->output,
false, kernelWidth, sigma);
}
}
// FIXME: mipmapping here is broken because it'll access texels from adjacent
// shadow maps.
// If the shadow texture has more than one level, mipmapping was requested, either directly
// or indirectly via anisotropic filtering.
// So generate the mipmaps for each layer
if (textureRequirements.levels > 1) {
for (size_t level = 0; level < textureRequirements.levels - 1; level++) {
ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level, vsmClearColor);
}
// If the shadow texture has more than one level, mipmapping was requested, either directly
// or indirectly via anisotropic filtering.
// So generate the mipmaps for each layer
if (UTILS_UNLIKELY(textureRequirements.levels > 1)) {
auto& ppm = engine.getPostProcessManager();
for (size_t level = 0; level < textureRequirements.levels - 1; level++) {
ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level, textureRequirements.clearColor);
}
}
}
@@ -626,7 +622,8 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
FView& view, CameraInfo cameraInfo, FScene::RenderableSoa& renderableData,
FScene::LightSoa const& lightData, ShadowMap::SceneInfo sceneInfo) noexcept {
FScene* scene = view.getScene();
FScene const* const scene = view.getScene();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
auto& lcm = engine.getLightManager();
FLightManager::Instance const directionalLight = lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
@@ -650,7 +647,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
};
bool hasVisibleShadows = false;
utils::Slice<ShadowMap> cascadedShadowMaps = getCascadedShadowMap();
utils::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.
@@ -750,6 +747,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = wsTexelSize * normalBias;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / length(wsTexelSize);
@@ -801,10 +799,8 @@ void ShadowMapManager::updateSpotVisibilityMasks(
const bool visSpotShadowRenderable = v.castShadows && inVisibleLayer &&
(!v.culling || (mask & VISIBLE_DYN_SHADOW_RENDERABLE));
using Type = Culler::result_type;
visibleMask[i] &= ~Type(VISIBLE_DYN_SHADOW_RENDERABLE);
visibleMask[i] |= Type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
visibleMask[i] &= ~Culler::result_type(VISIBLE_DYN_SHADOW_RENDERABLE);
visibleMask[i] |= Culler::result_type(visSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
}
}
@@ -813,13 +809,14 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
FScene::LightSoa const& lightData, ShadowMap::SceneInfo const& sceneInfo) noexcept {
const size_t lightIndex = shadowMap.getLightIndex();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
// update the shadow map frustum/camera
const ShadowMap::ShadowMapInfo shadowMapInfo{
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize - 2u),
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize - 2u),
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
@@ -834,7 +831,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
const size_t shadowIndex = shadowMap.getShadowIndex();
const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
const float normalBias = shadowMapInfo.vsm ? 0.0f : options.normalBias;
auto& s = mShadowUb.edit();
const double n = shadowMap.getCamera().getNear();
@@ -845,9 +842,10 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, FEngine& engin
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ;
s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n));
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius
/ length(wsTexelSizeAtOneMeter);
}
@@ -905,13 +903,14 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
const uint8_t face = shadowMap.getFace();
const size_t lightIndex = shadowMap.getLightIndex();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
FLightManager::ShadowOptions const& options = shadowMap.getShadowOptions();
auto const& vsmShadowOptions = view.getVsmShadowOptions();
// update the shadow map frustum/camera
const ShadowMap::ShadowMapInfo shadowMapInfo{
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize), // point-lights don't have a border
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize), // point-lights don't have a border
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN ||
engine.getBackend() == Backend::WEBGPU,
@@ -926,7 +925,7 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
const size_t shadowIndex = shadowMap.getShadowIndex();
const float2 wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
const float normalBias = shadowMapInfo.vsm ? 0.0f : options.normalBias;
auto& s = mShadowUb.edit();
const double n = shadowMap.getCamera().getNear();
@@ -937,9 +936,10 @@ void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ;
s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n));
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].vsmExponent = getWrapExponentEVSM(vsmShadowOptions, options);
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius
/ length(wsTexelSizeAtOneMeter);
}
}
@@ -995,7 +995,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
lightData.data<FScene::SHADOW_INFO>());
ShadowTechnique shadowTechnique{};
utils::Slice<const ShadowMap> spotShadowMaps = getSpotShadowMaps();
utils::Slice<const ShadowMap> const spotShadowMaps = getSpotShadowMaps();
if (!spotShadowMaps.empty()) {
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
for (ShadowMap const& shadowMap : spotShadowMaps) {
@@ -1035,14 +1035,14 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
for (ShadowMap const& shadowMap : getCascadedShadowMap()) {
// Shadow map size should be the same for all cascades.
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
maxDimension = std::max(maxDimension, options.mapSize);
elvsm = elvsm || options.vsm.elvsm;
}
for (ShadowMap const& shadowMap : getSpotShadowMaps()) {
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
maxDimension = std::max(maxDimension, options.mapSize);
elvsm = elvsm || options.vsm.elvsm;
}
uint8_t layersNeeded = 0;
@@ -1052,7 +1052,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
ShadowMap* pShadowMap) mutable {
// Allocate shadowmap from our Atlas Allocator
auto const& options = pShadowMap->getShadowOptions();
auto allocation = allocator.allocate(options->mapSize);
auto allocation = allocator.allocate(options.mapSize);
assert_invariant(allocation.isValid());
assert_invariant(!allocation.viewport.empty());
pShadowMap->setAllocation(allocation.layer, allocation.viewport);
@@ -1088,6 +1088,7 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
msaaSamples = 1;
}
float4 clearColor{};
TextureFormat format = TextureFormat::DEPTH16;
if (view.hasVSM()) {
if (vsmShadowOptions.highPrecision) {
@@ -1095,6 +1096,9 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
} else {
format = elvsm ? TextureFormat::RGBA16F : TextureFormat::RG16F;
}
const float maxMoment2 = getMaxMomentEVSM(vsmShadowOptions);
const float maxMoment1 = std::sqrt(maxMoment2);
clearColor = { maxMoment1, maxMoment2, 0, 0 };
}
mSoftShadowOptions = view.getSoftShadowOptions();
@@ -1116,7 +1120,8 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
layersNeeded,
mipLevels,
msaaSamples,
format
format,
clearColor
};
}

View File

@@ -47,9 +47,12 @@
#include <utils/Slice.h>
#include <math/mat4.h>
#include <math/half.h>
#include <math/vec4.h>
#include <algorithm>
#include <array>
#include <limits>
#include <memory>
#include <new>
#include <type_traits>
@@ -145,6 +148,28 @@ public:
// for debugging only
utils::FixedCapacityVector<Camera const*> getDirectionalShadowCameras() const noexcept;
static float getMaxMomentEVSM(VsmShadowOptions const& vsmShadowOptions) noexcept {
return vsmShadowOptions.highPrecision ?
std::numeric_limits<float>::max() :
float(std::numeric_limits<math::half>::max());
}
static float getMaxWrapExponentEVSM(VsmShadowOptions const& vsmShadowOptions) noexcept {
constexpr float low = 5.2f; // ~ std::log(std::numeric_limits<math::half>::max()) * 0.5f;
constexpr float high = 40.0f; // ~ std::log(std::numeric_limits<float>::max()) * 0.5f;
return vsmShadowOptions.highPrecision ? high : low;
}
static float getWrapExponentEVSM(
VsmShadowOptions const& vsmShadowOptions,
LightManager::ShadowOptions const& options) noexcept {
constexpr float ABSOLUTE_FILTER_LIMIT = 42.0f;
float const targetExponent = getMaxWrapExponentEVSM(vsmShadowOptions);
float const effectiveFilterRadius = std::max(1.0f, options.vsm.blurWidth);
float const filterCeiling = ABSOLUTE_FILTER_LIMIT / effectiveFilterRadius;
return std::min(targetExponent, filterCeiling);
}
private:
explicit ShadowMapManager(FEngine& engine);
@@ -216,6 +241,7 @@ private:
uint8_t levels = 0;
uint8_t msaaSamples = 1;
backend::TextureFormat format = backend::TextureFormat::DEPTH16;
math::float4 clearColor{};
} mTextureAtlasRequirements;
SoftShadowOptions mSoftShadowOptions;

View File

@@ -93,6 +93,10 @@ public:
mHasScissor = true;
}
void setScissor(backend::Viewport const& viewport) noexcept {
setScissor(viewport.left, viewport.bottom, viewport.width, viewport.height);
}
void unsetScissor() noexcept {
constexpr uint32_t maxvalu = std::numeric_limits<int32_t>::max();
mScissorRect = { 0, 0, maxvalu, maxvalu };

View File

@@ -76,6 +76,7 @@
#include <cmath>
#include <chrono>
#include <functional>
#include <limits>
#include <memory>
#include <new>
#include <ratio>
@@ -1131,8 +1132,6 @@ void FView::prepareShadowMapping() const noexcept {
uniforms = mShadowMapManager->getShadowMappingUniforms();
}
constexpr float low = 5.54f; // ~ std::log(std::numeric_limits<math::half>::max()) * 0.5f;
constexpr float high = 42.0f; // ~ std::log(std::numeric_limits<float>::max()) * 0.5f;
constexpr uint32_t SHADOW_SAMPLING_RUNTIME_PCF = 0u;
constexpr uint32_t SHADOW_SAMPLING_RUNTIME_EVSM = 1u;
constexpr uint32_t SHADOW_SAMPLING_RUNTIME_DPCF = 2u;
@@ -1148,8 +1147,8 @@ void FView::prepareShadowMapping() const noexcept {
break;
case ShadowType::VSM:
s.shadowSamplingType = SHADOW_SAMPLING_RUNTIME_EVSM;
s.vsmExponent = mVsmShadowOptions.highPrecision ? high : low;
s.vsmDepthScale = mVsmShadowOptions.minVarianceScale * 0.01f * s.vsmExponent;
s.vsmExponent = 0; // this is only used when rendering the shadowmap, not when using it
s.vsmMaxMoment = ShadowMapManager::getMaxMomentEVSM(mVsmShadowOptions);
s.vsmLightBleedReduction = mVsmShadowOptions.lightBleedReduction;
break;
case ShadowType::DPCF:

View File

@@ -28,9 +28,11 @@
#include <utils/debug.h>
#include <math/half.h>
#include <math/vec4.h>
#include <array>
#include <limits>
namespace filament {
@@ -74,8 +76,8 @@ void ShadowMapDescriptorSet::prepareLodBias(Transaction const& transaction, floa
}
void ShadowMapDescriptorSet::prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept {
PerViewDescriptorSetUtils::prepareViewport(edit(transaction), viewport, viewport);
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept {
PerViewDescriptorSetUtils::prepareViewport(edit(transaction), physicalViewport, logicalViewport);
// TODO: offset calculation is now different
}
@@ -90,11 +92,10 @@ void ShadowMapDescriptorSet::prepareMaterialGlobals(Transaction const& transacti
}
void ShadowMapDescriptorSet::prepareShadowMapping(Transaction const& transaction,
bool const highPrecision) noexcept {
float const vsmExponent, float const vsmMaxMoment) noexcept {
auto& s = edit(transaction);
constexpr float low = 5.54f; // ~ std::log(std::numeric_limits<math::half>::max()) * 0.5f;
constexpr float high = 42.0f; // ~ std::log(std::numeric_limits<float>::max()) * 0.5f;
s.vsmExponent = highPrecision ? high : low;
s.vsmExponent = vsmExponent;
s.vsmMaxMoment = vsmMaxMoment;
}
ShadowMapDescriptorSet::Transaction ShadowMapDescriptorSet::open(DriverApi& driver) noexcept {

View File

@@ -66,7 +66,7 @@ public:
float bias) noexcept;
static void prepareViewport(Transaction const& transaction,
backend::Viewport const& viewport) noexcept;
backend::Viewport const& physicalViewport, backend::Viewport const& logicalViewport) noexcept;
static void prepareTime(Transaction const& transaction,
FEngine const& engine, math::float4 const& userTime) noexcept;
@@ -75,7 +75,7 @@ public:
std::array<math::float4, 4> const& materialGlobals) noexcept;
static void prepareShadowMapping(Transaction const& transaction,
bool highPrecision) noexcept;
float vsmExponent, float vsmMaxMoment) noexcept;
static Transaction open(backend::DriverApi& driver) noexcept;

View File

@@ -19,7 +19,8 @@ material {
{
name : color,
target : color,
type : float4
type : float4,
precision : high
}
],
domain : postprocess,
@@ -30,8 +31,29 @@ material {
fragment {
void postProcess(inout PostProcessInputs postProcess) {
highp vec2 uv = gl_FragCoord.xy * materialParams.uvscale;
postProcess.color = textureLod(materialParams_color,
vec3(uv, materialParams.layer), 0.0);
// For EVSM mipmapping, we use a custom box filter (instead of the GPU's bilinear filter), because GPUs often
// use a lower precision for texture interpolation and EVSM moments are very sensitive to that.
highp ivec2 destCoord = ivec2(gl_FragCoord.xy);
highp ivec3 srcCoord = ivec3(destCoord * 2, materialParams.layer);
#if defined(TARGET_WEBGPU_ENVIRONMENT)
// it looks like WebGPU doesn't support texelFetchOffset
highp vec4 m0 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(0, 0), srcCoord.z), 0);
highp vec4 m1 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(1, 0), srcCoord.z), 0);
highp vec4 m2 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(0, 1), srcCoord.z), 0);
highp vec4 m3 = texelFetch(materialParams_color, ivec3(srcCoord.xy + ivec2(1, 1), srcCoord.z), 0);
#else
highp vec4 m0 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(0, 0));
highp vec4 m1 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(1, 0));
highp vec4 m2 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(0, 1));
highp vec4 m3 = texelFetchOffset(materialParams_color, srcCoord, 0, ivec2(1, 1));
#endif
// Make sure to not overflow the moments when averaging them
postProcess.color = (m0 * 0.25) +
(m1 * 0.25) +
(m2 * 0.25) +
(m3 * 0.25);
}
}

View File

@@ -166,7 +166,7 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
// VSM shadows [variant: VSM]
// --------------------------------------------------------------------------------------------
float vsmExponent;
float vsmDepthScale;
float vsmMaxMoment;
float vsmLightBleedReduction;
uint32_t shadowSamplingType; // 0: vsm, 1: dpcf
@@ -302,9 +302,9 @@ struct ShadowUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
float bulbRadiusLs; // 4
float nearOverFarMinusNear; // 4
math::float2 normalBias; // 4
bool elvsm; // 4
uint32_t layer; // 4
uint32_t reserved1; // 4
bool elvsm; // 4 // could be 1 bit
uint32_t layer; // 4 // could be 8 bits
float vsmExponent; // 4 // could be fp16
uint32_t reserved2; // 4
};
ShadowData shadows[CONFIG_MAX_SHADOWMAPS];

View File

@@ -175,7 +175,7 @@ BufferInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
// VSM shadows [variant: VSM]
// ------------------------------------------------------------------------------------
{ "vsmExponent", 0, Type::FLOAT },
{ "vsmDepthScale", 0, Type::FLOAT },
{ "vsmMaxMoment", 0, Type::FLOAT, Precision::HIGH },
{ "vsmLightBleedReduction", 0, Type::FLOAT },
{ "shadowSamplingType", 0, Type::UINT },

View File

@@ -20,7 +20,7 @@ layout(location = 0) out highp uvec2 outPicking;
// note: VARIANT_HAS_VSM and VARIANT_HAS_PICKING are mutually exclusive
//------------------------------------------------------------------------------
highp vec2 computeDepthMomentsVSM(const highp float depth);
highp vec4 computeDepthMomentsVSM(const highp float depth);
void main() {
filament_lodBias = frameUniforms.lodBias;
@@ -53,9 +53,7 @@ void main() {
// we always compute the "negative" side of ELVSM because the cost is small, and this allows
// EVSM/ELVSM choice to be done on the CPU side more easily.
highp float depth = vertex_worldPosition.w;
depth = exp(frameUniforms.vsmExponent * depth);
fragColor.xy = computeDepthMomentsVSM(depth);
fragColor.zw = computeDepthMomentsVSM(-1.0 / depth); // requires at least RGBA16F
fragColor = computeDepthMomentsVSM(depth);
#elif defined(VARIANT_HAS_PICKING)
#if FILAMENT_EFFECTIVE_VERSION == 100
outPicking.a = mod(float(object_uniforms_objectId / 65536), 256.0) / 255.0;
@@ -74,22 +72,30 @@ void main() {
#endif
}
highp vec2 computeDepthMomentsVSM(const highp float depth) {
// computes the moments
#if MATERIAL_FEATURE_LEVEL > 0
highp vec4 computeDepthMomentsVSM(const highp float depth) {
// See GPU Gems 3
// https://developer.nvidia.com/gpugems/gpugems3/part-ii-light-and-shadows/chapter-8-summed-area-variance-shadow-maps
highp vec2 moments;
// computes the first two moments
float c = frameUniforms.vsmExponent;
highp float MAX_MOMENT = frameUniforms.vsmMaxMoment;
// the first moment is just the depth (average)
moments.x = depth;
// wrap depth for EVSM
highp float z = exp(c * depth);
// compute the 2nd moment over the pixel extents.
moments.y = depth * depth;
// compute EVSM moments
highp vec2 m1 = vec2(z, -1.0 / z);
highp vec2 m2 = m1 * m1;
// the local linear approximation is not correct with a warped depth
//highp float dx = dFdx(depth);
//highp float dy = dFdy(depth);
//moments.y += 0.25 * (dx * dx + dy * dy);
// compute analytic variance (2nd moment), taking into account the change in depth accross the texel
highp float dzdx = dFdx(depth);
highp float dzdy = dFdy(depth);
highp float linearVariance = 0.25 * (dzdx * dzdx + dzdy * dzdy);
highp vec2 analyticVariance = c * c * m2 * linearVariance;
m2 = min(m2 + analyticVariance, MAX_MOMENT);
return moments;
return vec4(m1.x, m2.x, m1.y, m2.y);
}
#endif

View File

@@ -335,6 +335,91 @@ float ShadowSample_PCSS(const bool DIRECTIONAL,
return 1.0 - percentageOccluded;
}
//------------------------------------------------------------------------------
// VSM
//------------------------------------------------------------------------------
float chebyshevUpperBound(const highp vec2 moments, const highp float depth,
const highp float minVariance, const highp float lbrAmount) {
// Fast path: if the receiver is fully in front of the caster
if (depth <= moments.x) {
return 1.0;
}
// Calculate variance with our dynamically injected floor
highp float variance = max(moments.y - (moments.x * moments.x), minVariance);
// Standard Chebyshev inequality
highp float d = depth - moments.x;
highp float p_max = variance / (variance + d * d);
// Apply Light Bleeding Reduction (LBR)
return saturate((p_max - lbrAmount) / (1.0 - lbrAmount));
}
float evaluateEVSM(const bool ELVSM, float c,
const highp vec4 moments, const highp float zReceiver,
const highp vec2 dzduv, const highp vec2 texelSize) {
const highp float EPSILON_MULTIPLIER = 0.002; // could be 0.00001 in fp32
float lbrAmount = frameUniforms.vsmLightBleedReduction;
// Scale the UV-space gradient down to a single shadow map texel footprint.
highp vec2 texel_dzduv = dzduv * texelSize;
// squared magnitude of the linear depth gradient across a single shadow map texel footprint
highp float dz2 = dot(texel_dzduv, texel_dzduv);
// remap depth to [-1, 1]
highp float depth = zReceiver * 2.0 - 1.0;
// positive wrap
highp float pw = exp(c * depth);
highp float epsilon = EPSILON_MULTIPLIER * (pw * pw);
// Dynamic variance for the positive side (derivative of wraped depth w.r.t. light-space depth via Chain Rule)
highp float dpwdz = 2.0 * c * pw;
highp float pMinVariance = epsilon + 0.25 * (dpwdz * dpwdz) * dz2;
float p = chebyshevUpperBound(moments.xy, pw, pMinVariance, lbrAmount);
// negative wrap
if (ELVSM) {
highp float nw = -1.0 / pw;
highp float epsilon = EPSILON_MULTIPLIER * (nw * nw);
// Dynamic variance for the negative side (derivative of wraped depth w.r.t. light-space depth via Chain Rule)
highp float dnwdz = 2.0 * c * nw;
highp float nMinVariance = epsilon + 0.25 * (dnwdz * dnwdz) * dz2;
float n = chebyshevUpperBound(moments.zw, nw, nMinVariance, lbrAmount);
p = min(p, n);
}
return p;
}
float ShadowSample_VSM(const bool DIRECTIONAL, const highp sampler2DArray shadowMap,
const highp vec4 scissorNormalized,
const uint layer, const int index,
const highp vec4 shadowPosition, const highp float zLight) {
bool ELVSM = shadowUniforms.shadows[index].elvsm;
float c = shadowUniforms.shadows[index].vsmExponent;
highp vec2 texelSize = vec2(1.0) / vec2(textureSize(shadowMap, 0)); // TODO: put this in a uniform
// note: shadowPosition.z is in linear light-space normalized to [0, 1]
// see: ShadowMap::computeVsmLightSpaceMatrix() in ShadowMap.cpp
// see: computeLightSpacePosition() in common_shadowing.fs
highp vec3 position = vec3(shadowPosition.xy * (1.0 / shadowPosition.w), shadowPosition.z);
// plane receiver bias to reduce shadow acnee on the received plane (before clamp)
highp vec2 dzduv = computeReceiverPlaneDepthBias(position);
// clamp uv to border
position.xy = clamp(position.xy, scissorNormalized.xy, scissorNormalized.zw);
// Read the shadow map with all available filtering
highp vec4 moments = texture(shadowMap, vec3(position.xy, layer));
return evaluateEVSM(ELVSM, c, moments, position.z, dzduv, texelSize);
}
//------------------------------------------------------------------------------
// Screen-space Contact Shadows
//------------------------------------------------------------------------------
@@ -409,70 +494,6 @@ float screenSpaceContactShadow(vec3 lightDirection) {
return occlusion;
}
//------------------------------------------------------------------------------
// VSM
//------------------------------------------------------------------------------
float linstep(const float min, const float max, const float v) {
// we could use smoothstep() too
return clamp((v - min) / (max - min), 0.0, 1.0);
}
float reduceLightBleed(const float pMax, const float amount) {
// Remove the [0, amount] tail and linearly rescale (amount, 1].
return linstep(amount, 1.0, pMax);
}
float chebyshevUpperBound(const highp vec2 moments, const highp float mean,
const highp float minVariance, const float lightBleedReduction) {
// Donnelly and Lauritzen 2006, "Variance Shadow Maps"
highp float variance = moments.y - (moments.x * moments.x);
variance = max(variance, minVariance);
highp float d = mean - moments.x;
float pMax = variance / (variance + d * d);
pMax = reduceLightBleed(pMax, lightBleedReduction);
return mean <= moments.x ? 1.0 : pMax;
}
float evaluateShadowVSM(const highp vec2 moments, const highp float depth) {
highp float depthScale = frameUniforms.vsmDepthScale * depth;
highp float minVariance = depthScale * depthScale;
return chebyshevUpperBound(moments, depth, minVariance, frameUniforms.vsmLightBleedReduction);
}
float ShadowSample_VSM(const bool ELVSM, const highp sampler2DArray shadowMap,
const highp vec4 scissorNormalized,
const uint layer, const highp vec4 shadowPosition) {
// note: shadowPosition.z is in linear light-space normalized to [0, 1]
// see: ShadowMap::computeVsmLightSpaceMatrix() in ShadowMap.cpp
// see: computeLightSpacePosition() in common_shadowing.fs
highp vec3 position = vec3(shadowPosition.xy * (1.0 / shadowPosition.w), shadowPosition.z);
// Note: we don't need to clamp to `scissorNormalized` in the VSM case because this is only
// needed when the shadow casters and receivers are different, which is never the case with VSM
// (see ShadowMap.cpp).
// Read the shadow map with all available filtering
highp vec4 moments = texture(shadowMap, vec3(position.xy, layer));
highp float depth = position.z;
// EVSM depth warping
depth = depth * 2.0 - 1.0;
depth = frameUniforms.vsmExponent * depth;
depth = exp(depth);
float p = evaluateShadowVSM(moments.xy, depth);
if (ELVSM) {
p = min(p, evaluateShadowVSM(moments.zw, -1.0 / depth));
}
return p;
}
//------------------------------------------------------------------------------
// Shadow sampling dispatch
//------------------------------------------------------------------------------
@@ -529,6 +550,9 @@ float shadow(const bool DIRECTIONAL,
} else if (CONFIG_SHADOW_SAMPLING_METHOD == SHADOW_SAMPLING_PCF_LOW) {
return ShadowSample_PCF_Low(shadowMap, scissorNormalized, layer, shadowPosition);
}
// should not happen
return 0.0;
}
// Shadow requiring a sampler2D sampler (VSM, DPCF and PCSS)
@@ -539,9 +563,8 @@ float shadow(const bool DIRECTIONAL,
uint layer = shadowUniforms.shadows[index].layer;
// This conditional is resolved at compile time
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_EVSM) {
bool elvsm = shadowUniforms.shadows[index].elvsm;
return ShadowSample_VSM(elvsm, shadowMap, scissorNormalized, layer,
shadowPosition);
return ShadowSample_VSM(DIRECTIONAL, shadowMap, scissorNormalized, layer, index,
shadowPosition, zLight);
}
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_DPCF) {
@@ -558,8 +581,8 @@ float shadow(const bool DIRECTIONAL,
// This is here mostly for debugging at this point.
// Note: In this codepath, the normal bias is not applied because we're in the VSM variant.
// (see: get{Cascade|Spot}LightSpacePosition)
return ShadowSample_PCF(shadowMap, scissorNormalized,
layer, shadowPosition);
return ShadowSample_PCF(shadowMap, scissorNormalized, layer,
shadowPosition);
}
// should not happen

View File

@@ -10,7 +10,7 @@ struct ShadowData {
highp vec2 normalBias;
bool elvsm;
mediump uint layer;
mediump uint reserved1;
mediump float vsmExponent;
mediump uint reserved2;
};
#endif

View File

@@ -1747,7 +1747,7 @@ export interface View$VsmShadowOptions {
*/
highPrecision?: boolean;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
minVarianceScale?: number;
/**