Added support for DPCF

This commit is contained in:
Mathias Agopian
2021-12-03 01:20:50 -06:00
committed by Mathias Agopian
parent aac3ac579c
commit 087f29c6c9
27 changed files with 335 additions and 92 deletions
+2
View File
@@ -5,6 +5,8 @@ A new header is inserted each time a *tag* is created.
## v1.15.1 (currently main branch)
- engine: add support for DPCF (PCF shadows with contact hardening)
## v1.15.0
- engine: Fix spotlights normal bias calculation [⚠️ **Material breakage**].
@@ -79,7 +79,7 @@ Java_com_google_android_filament_LightManager_nBuilderShadowOptions(JNIEnv* env,
jfloat shadowFarHint, jboolean stable,
jfloat polygonOffsetConstant, jfloat polygonOffsetSlope,
jboolean screenSpaceContactShadows, jint stepCount,
jfloat maxShadowDistance, jint vsmMsaaSamples, jfloat blurWidth) {
jfloat maxShadowDistance, jint vsmMsaaSamples, jfloat blurWidth, jfloat shadowBulbRadius) {
LightManager::Builder *builder = (LightManager::Builder *) nativeBuilder;
LightManager::ShadowOptions shadowOptions {
.mapSize = (uint32_t)mapSize,
@@ -98,7 +98,8 @@ Java_com_google_android_filament_LightManager_nBuilderShadowOptions(JNIEnv* env,
.vsm = {
.msaaSamples = (uint8_t) vsmMsaaSamples,
.blurWidth = blurWidth
}
},
.shadowBulbRadius = shadowBulbRadius
};
jfloat *nativeSplits = env->GetFloatArrayElements(splitPositions, NULL);
const jsize splitCount = std::min((jsize) 3, env->GetArrayLength(splitPositions));
@@ -351,6 +351,12 @@ public class LightManager {
* The maximum value is 125.
*/
public float blurWidth = 0.0f;
/**
* Light bulb radius used for soft shadows. Currently this is only used when DPCF is
* enabled. (2cm by default).
*/
public float shadowBulbRadius = 0.02f;
}
public static class ShadowCascades {
@@ -494,7 +500,7 @@ public class LightManager {
options.polygonOffsetConstant, options.polygonOffsetSlope,
options.screenSpaceContactShadows,
options.stepCount, options.maxShadowDistance, options.vsmMsaaSamples,
options.blurWidth);
options.blurWidth, options.shadowBulbRadius);
return this;
}
@@ -1152,7 +1158,7 @@ public class LightManager {
private static native void nDestroyBuilder(long nativeBuilder);
private static native boolean nBuilderBuild(long nativeBuilder, long nativeEngine, int entity);
private static native void nBuilderCastShadows(long nativeBuilder, boolean enable);
private static native void nBuilderShadowOptions(long nativeBuilder, int mapSize, int cascades, float[] splitPositions, float constantBias, float normalBias, float shadowFar, float shadowNearHint, float shadowFarhint, boolean stable, float polygonOffsetConstant, float polygonOffsetSlope, boolean screenSpaceContactShadows, int stepCount, float maxShadowDistance, int vsmMsaaSamples, float blurWidth);
private static native void nBuilderShadowOptions(long nativeBuilder, int mapSize, int cascades, float[] splitPositions, float constantBias, float normalBias, float shadowFar, float shadowNearHint, float shadowFarhint, boolean stable, float polygonOffsetConstant, float polygonOffsetSlope, boolean screenSpaceContactShadows, int stepCount, float maxShadowDistance, int vsmMsaaSamples, float blurWidth, float shadowBulbRadius);
private static native void nBuilderCastLight(long nativeBuilder, boolean enabled);
private static native void nBuilderPosition(long nativeBuilder, float x, float y, float z);
private static native void nBuilderDirection(long nativeBuilder, float x, float y, float z);
@@ -736,7 +736,12 @@ public class View {
/**
* Variance shadows.
*/
VSM
VSM,
/**
* Percentage-closer filtered shadows, with contact hardening simulation.
*/
DPCF
}
/**
+6
View File
@@ -334,6 +334,12 @@ public:
*/
float blurWidth = 0.0f;
} vsm;
/**
* Light bulb radius used for soft shadows. Currently this is only used when DPCF is
* enabled. (2cm by default).
*/
float shadowBulbRadius = 0.02f;
};
struct ShadowCascades {
+2 -1
View File
@@ -339,7 +339,8 @@ enum class Dithering : uint8_t {
*/
enum class ShadowType : uint8_t {
PCF, //!< percentage-closer filtered shadows (default)
VSM //!< variance shadows
VSM, //!< variance shadows
DPCF //!< PCF with contact hardening simulation
};
/**
+1 -1
View File
@@ -339,7 +339,7 @@ bool Froxelizer::update() noexcept {
/*
* Now compute the bounding sphere of each froxel, which is needed for spot-lights
* Now compute the bounding sphere of each froxel, which is needed for spotlights
* We intersect 3 planes of the frustum to find each 8 corners.
* Currently the bounding sphere is computed from the bounding-box, which is probably,
* not the best.
+11 -2
View File
@@ -260,6 +260,7 @@ void PerViewUniforms::prepareShadowMapping(ShadowMappingUniforms const& shadowMa
s.lightFromWorldMatrix = shadowMappingUniforms.lightFromWorldMatrix;
s.cascadeSplits = shadowMappingUniforms.cascadeSplits;
s.shadowBulbRadiusLs = shadowMappingUniforms.shadowBulbRadiusLs;
s.shadowBias = shadowMappingUniforms.shadowBias;
s.ssContactShadowDistance = shadowMappingUniforms.ssContactShadowDistance;
s.directionalShadows = shadowMappingUniforms.directionalShadows;
@@ -277,6 +278,8 @@ void PerViewUniforms::prepareShadowVSM(Handle<HwTexture> texture, VsmShadowOptio
.filterMin = filterMin,
.anisotropyLog2 = options.anisotropy,
}});
auto& s = mPerViewUb.edit();
s.shadowSamplingType = SHADOW_SAMPLING_RUNTIME_VSM;
}
void PerViewUniforms::prepareShadowPCF(Handle<HwTexture> texture) noexcept {
@@ -284,11 +287,17 @@ void PerViewUniforms::prepareShadowPCF(Handle<HwTexture> texture) noexcept {
texture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR,
.compareMode = SamplerCompareMode::COMPARE_TO_TEXTURE, // ignored for VSM
.compareFunc = SamplerCompareFunc::GE // ignored for VSM
.compareMode = SamplerCompareMode::COMPARE_TO_TEXTURE,
.compareFunc = SamplerCompareFunc::GE
}});
}
void PerViewUniforms::prepareShadowDPCF(Handle<HwTexture> texture) noexcept {
mPerViewSb.setSampler(PerViewSib::SHADOW_MAP, { texture, { }});
auto& s = mPerViewUb.edit();
s.shadowSamplingType = SHADOW_SAMPLING_RUNTIME_DPCF;
}
void PerViewUniforms::commit(backend::DriverApi& driver) noexcept {
if (mPerViewUb.isDirty()) {
driver.updateBufferObject(mPerViewUbh, mPerViewUb.toBufferDescriptor(driver), 0);
+5
View File
@@ -51,6 +51,10 @@ class PerViewUniforms {
using LightManagerInstance = utils::EntityInstance<LightManager>;
using TextureHandle = backend::Handle<backend::HwTexture>;
static constexpr uint32_t const SHADOW_SAMPLING_RUNTIME_PCF = 0u;
static constexpr uint32_t const SHADOW_SAMPLING_RUNTIME_VSM = 1u;
static constexpr uint32_t const SHADOW_SAMPLING_RUNTIME_DPCF = 2u;
public:
explicit PerViewUniforms(FEngine& engine) noexcept;
@@ -79,6 +83,7 @@ public:
// maybe these should have their own UBO, they're needed only when GENERATING the shadowmaps
void prepareShadowVSM(TextureHandle texture, VsmShadowOptions const& options) noexcept;
void prepareShadowPCF(TextureHandle texture) noexcept;
void prepareShadowDPCF(TextureHandle texture) noexcept;
// update local data into GPU UBO
void commit(backend::DriverApi& driver) noexcept;
+2 -1
View File
@@ -286,7 +286,8 @@ void RenderPass::generateCommandsImpl(uint32_t extraFlags,
materialVariant.setDirectionalLighting(renderFlags & HAS_DIRECTIONAL_LIGHT);
materialVariant.setDynamicLighting(renderFlags & HAS_DYNAMIC_LIGHTING);
materialVariant.setFog(renderFlags & HAS_FOG);
materialVariant.setVsm((renderFlags & HAS_VSM) && hasShadowing);
// DPCF uses the same color pass shader as VSM (but not for depth)
materialVariant.setVsm(((renderFlags & HAS_VSM) || (renderFlags & HAS_DPCF)) && hasShadowing);
materialVariant.setShadowReceiver(false); // this is set per Renderable
Command cmdColor;
+1
View File
@@ -240,6 +240,7 @@ public:
static constexpr RenderFlags HAS_FOG = 0x10;
static constexpr RenderFlags HAS_VSM = 0x20;
static constexpr RenderFlags HAS_PICKING = 0x40;
static constexpr RenderFlags HAS_DPCF = 0x80;
// Arena used for commands
using Arena = utils::Arena<
+5 -1
View File
@@ -292,7 +292,11 @@ void FRenderer::renderJob(ArenaScope& arena, FView& view) {
if (view.isFrontFaceWindingInverted()) baseRenderFlags |= RenderPass::HAS_INVERSE_FRONT_FACES;
RenderPass::RenderFlags colorRenderFlags = baseRenderFlags;
if (view.hasVsm()) colorRenderFlags |= RenderPass::HAS_VSM;
switch (view.getShadowType()) {
case ShadowType::PCF: break;
case ShadowType::VSM: colorRenderFlags |= RenderPass::HAS_VSM; break;
case ShadowType::DPCF: colorRenderFlags |= RenderPass::HAS_DPCF; break;
}
RenderPass::RenderFlags structureRenderFlags = baseRenderFlags;
if (view.hasPicking()) structureRenderFlags |= RenderPass::HAS_PICKING;
+8 -7
View File
@@ -343,9 +343,10 @@ void ShadowMap::updateDirectional(const FScene::LightSoa& lightData, size_t inde
const mat4 MbMt = getTextureCoordsMapping();
const mat4f St = mat4f(MbMt * S);
// note: in texelSizeWorldSpace() below, we could use Mb * Mt * F * W because
// L * Mp * Mv is a rigid transform (for directional lights)
if (USE_LISPSM) {
// note: in texelSizeWorldSpace() below, we can use Mb * Mt * F * W because
// L * Mp * Mv is a rigid transform for directional lights, and doesn't matter.
// if Wp[3][1] is 0, then LISPSM was cancelled.
if (USE_LISPSM && Wp[3][1] != 0.0f) {
mTexelSizeAtOneMeterWs = texelSizeWorldSpace(Wp, mat4f(MbMt * F));
} else {
// We know we're using an ortho projection
@@ -945,8 +946,8 @@ float ShadowMap::texelSizeWorldSpace(const mat3f& worldToShadowTexture) const no
// orthographic projections. We just need to inverse worldToShadowTexture,
// which is guaranteed to be orthographic.
// The two first columns give us how a texel maps in world-space.
const float ures = 1.0f / mShadowMapInfo.shadowDimension;
const float vres = 1.0f / mShadowMapInfo.shadowDimension;
const float ures = 1.0f / float(mShadowMapInfo.shadowDimension);
const float vres = 1.0f / float(mShadowMapInfo.shadowDimension);
const mat3f shadowTextureToWorld(inverse(worldToShadowTexture));
const float3 Jx = shadowTextureToWorld[0];
const float3 Jy = shadowTextureToWorld[1];
@@ -969,8 +970,8 @@ float ShadowMap::texelSizeWorldSpace(const mat4f& Wp, const mat4f& MbMtF) const
// It might be better to do this computation in the vertex shader.
float3 p = {0.5, 0.5, 0.0};
const float ures = 1.0f / mShadowMapInfo.shadowDimension;
const float vres = 1.0f / mShadowMapInfo.shadowDimension;
const float ures = 1.0f / float(mShadowMapInfo.shadowDimension);
const float vres = 1.0f / float(mShadowMapInfo.shadowDimension);
const float dres = 1.0f / 65536.0f;
constexpr bool JACOBIAN_ESTIMATE = false;
+19 -14
View File
@@ -163,7 +163,7 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, backend::DriverAp
.depth = textureRequirements.layers,
.levels = textureRequirements.levels,
.type = SamplerType::SAMPLER_2D_ARRAY,
.format = view.hasVsm() ? vsmTextureFormat : mTextureFormat
.format = view.hasVSM() ? vsmTextureFormat : mTextureFormat
});
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) { });
@@ -191,14 +191,14 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, backend::DriverAp
auto& shadowPass = fg.addPass<ShadowPassData>("Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
const bool blur = view.hasVsm() && options->vsm.blurWidth > 0.0f;
const bool blur = view.hasVSM() && options->vsm.blurWidth > 0.0f;
FrameGraphRenderPass::Descriptor renderTargetDesc{};
auto attachment = builder.createSubresource(prepareShadowPass->shadows,
"Shadowmap Layer", { .layer = layer });
if (view.hasVsm()) {
if (view.hasVSM()) {
// Each shadow pass has its own sample count, but textures are created with
// a default count of 1 because we're using "magic resolve" (sample count is
// set on the render target).
@@ -270,7 +270,7 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, backend::DriverAp
shadowMap.render(*scene, entry.range, entry.visibilityMask, cameraInfo, &entryPass);
const auto& executor = entryPass.getExecutor();
const bool blur = view.hasVsm() && options->vsm.blurWidth > 0.0f;
const bool blur = view.hasVSM() && options->vsm.blurWidth > 0.0f;
view.prepareCamera(cameraInfo);
@@ -305,7 +305,7 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, backend::DriverAp
// now emit the blurring passes
if (view.hasVsm()) {
if (view.hasVSM()) {
const float blurWidth = options->vsm.blurWidth;
if (blurWidth > 0.0f) {
const float sigma = (blurWidth + 1.0f) / 6.0f;
@@ -349,10 +349,10 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize - 2u),
.vsm = view.hasVsm(),
.vsm = view.hasVSM(),
.polygonOffset = { // handle reversed Z
.slope = view.hasVsm() ? 0.0f : -params.options.polygonOffsetSlope,
.constant = view.hasVsm() ? 0.0f : -params.options.polygonOffsetConstant
.slope = view.hasVSM() ? 0.0f : -params.options.polygonOffsetSlope,
.constant = view.hasVSM() ? 0.0f : -params.options.polygonOffsetConstant
}
};
@@ -375,7 +375,8 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng
// Texel size is constant for directional light (although that's not true when LISPSM
// is used, but in that case we're pretending it is).
const float wsTexelSize = shadowMap.getTexelSizAtOneMeterWs();
mShadowMappingUniforms.shadowBias = float3{ 0, normalBias * wsTexelSize, 0 };
mShadowMappingUniforms.shadowBias = normalBias * wsTexelSize;
mShadowMappingUniforms.shadowBulbRadiusLs = options.shadowBulbRadius / wsTexelSize;
}
// Adjust the near and far planes to tightly bound the scene.
@@ -498,10 +499,10 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize - 2u),
.spotIndex = uint16_t(i),
.vsm = view.hasVsm(),
.vsm = view.hasVSM(),
.polygonOffset = { // handle reversed Z
.slope = view.hasVsm() ? 0.0f : -params.options.polygonOffsetSlope,
.constant = view.hasVsm() ? 0.0f : -params.options.polygonOffsetConstant
.slope = view.hasVSM() ? 0.0f : -params.options.polygonOffsetSlope,
.constant = view.hasVSM() ? 0.0f : -params.options.polygonOffsetConstant
}
};
@@ -537,11 +538,15 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
auto& s = shadowUb.edit();
const float n = shadowMap.getCamera().getNear();
const float f = shadowMap.getCamera().getCullingFar();
s.shadows[i].lightFromWorldMatrix = shadowMap.getLightSpaceMatrix();
s.shadows[i].direction = direction;
s.shadows[i].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[i].lightFromWorldZ = shadowMap.getLightFromWorldZ();
s.shadows[i].texelSizeAtOneMeter = wsTexelSizeAtOneMeter;
s.shadows[i].bulbRadiusLs = options->shadowBulbRadius / wsTexelSizeAtOneMeter;
s.shadows[i].nearOverFarMinusNear = n / (f - n);
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
}
@@ -586,8 +591,8 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view
// Generate mipmaps for VSM when anisotropy is enabled or when requested
auto const& vsmShadowOptions = view.getVsmShadowOptions();
const bool useMipmapping = view.hasVsm() &&
((vsmShadowOptions.anisotropy > 0) || vsmShadowOptions.mipmapping);
const bool useMipmapping = view.hasVSM() &&
((vsmShadowOptions.anisotropy > 0) || vsmShadowOptions.mipmapping);
uint8_t mipLevels = 1u;
if (useMipmapping) {
+2 -1
View File
@@ -52,7 +52,8 @@ class RenderPass;
struct ShadowMappingUniforms {
std::array<math::mat4f, CONFIG_MAX_SHADOW_CASCADES> lightFromWorldMatrix;
math::float4 cascadeSplits;
math::float3 shadowBias;
float shadowBulbRadiusLs;
float shadowBias;
float ssContactShadowDistance;
uint32_t directionalShadows;
uint32_t cascades;
+13 -7
View File
@@ -305,11 +305,11 @@ void FView::prepareShadowing(FEngine& engine, DriverApi& driver,
mShadowMapManager.setShadowCascades(0, &shadowOptions);
}
// Find all shadow-casting spot lights.
// Find all shadow-casting spotlights.
size_t shadowCastingSpotCount = 0;
// We allow a max of CONFIG_MAX_SHADOW_CASTING_SPOTS spot light shadows. Any additional
// shadow-casting spot lights are ignored.
// shadow-casting spotlights are ignored.
for (size_t l = FScene::DIRECTIONAL_LIGHTS_COUNT; l < lightData.size(); l++) {
// when we get here all the lights should be visible
@@ -456,7 +456,7 @@ void FView::prepare(FEngine& engine, DriverApi& driver, ArenaScope& arena,
* Gather all information needed to render this scene. Apply the world origin to all
* objects in the scene.
*/
scene->prepare(worldOriginScene, hasVsm());
scene->prepare(worldOriginScene, hasVSM());
/*
* Light culling: runs in parallel with Renderable culling (below)
@@ -666,10 +666,16 @@ void FView::prepareStructure(Handle<HwTexture> structure) const noexcept {
}
void FView::prepareShadow(Handle<HwTexture> texture) const noexcept {
if (hasVsm()) {
mPerViewUniforms.prepareShadowVSM(texture, mVsmShadowOptions);
} else {
mPerViewUniforms.prepareShadowPCF(texture);
switch (mShadowType) {
case filament::ShadowType::PCF:
mPerViewUniforms.prepareShadowPCF(texture);
break;
case filament::ShadowType::VSM:
mPerViewUniforms.prepareShadowVSM(texture, mVsmShadowOptions);
break;
case filament::ShadowType::DPCF:
mPerViewUniforms.prepareShadowDPCF(texture);
break;
}
}
+2 -2
View File
@@ -90,7 +90,7 @@ public:
CANDELA // intensity specified in candela (only applicable to punctual lights)
};
struct ShadowParams {
struct ShadowParams { // TODO: get rid of this struct
LightManager::ShadowOptions options;
};
@@ -239,7 +239,7 @@ private:
DIRECTION, // direction in local-space (i.e. pre-transform)
COLOR, // color
SHADOW_PARAMS, // state needed for shadowing
SPOT_PARAMS, // state needed for spot lights
SPOT_PARAMS, // state needed for spotlights
SUN_ANGULAR_RADIUS, // state for the directional light sun
SUN_HALO_SIZE, // state for the directional light sun
SUN_HALO_FALLOFF, // state for the directional light sun
+2 -1
View File
@@ -186,7 +186,8 @@ public:
bool hasShadowing() const noexcept { return mHasShadowing; }
bool needsShadowMap() const noexcept { return mNeedsShadowMap; }
bool hasFog() const noexcept { return mFogOptions.enabled && mFogOptions.density > 0.0f; }
bool hasVsm() const noexcept { return mShadowType == ShadowType::VSM; }
bool hasVSM() const noexcept { return mShadowType == ShadowType::VSM; }
bool hasDPCF() const noexcept { return mShadowType == ShadowType::DPCF; }
bool hasPicking() const noexcept { return mActivePickingQueriesList != nullptr; }
void renderShadowMaps(FrameGraph& fg, FEngine& engine, FEngine::DriverApi& driver,
@@ -70,7 +70,9 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
math::float3 lightDirection;
uint32_t fParamsX; // stride-x
math::float3 shadowBias; // unused, normal bias, unused
float shadowBulbRadiusLs; // light radius in light-space
float shadowBias; // normal bias
float reserved;
float oneOverFroxelDimensionY;
math::float4 zParams; // froxel Z parameters
@@ -126,7 +128,7 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
float vsmExponent;
float vsmDepthScale;
float vsmLightBleedReduction;
float vsmReserved0;
uint32_t shadowSamplingType; // 0: vsm, 1: dpcf
float lodBias;
float oneOverFarMinusNear; // 1 / (f-n), always positive
@@ -142,7 +144,7 @@ static_assert(sizeof(PerViewUib) == sizeof(math::float4) * 128,
"PerViewUib should be exactly 2KiB");
// PerRenderableUib must have an alignment of 256 to be compatible with all versions of GLES.
struct alignas(256) PerRenderableUib {
struct alignas(256) PerRenderableUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
static constexpr utils::StaticString _name{ "ObjectUniforms" };
math::mat4f worldFromModelMatrix;
math::mat3f worldFromModelNormalMatrix; // this gets expanded to 48 bytes during the copy to the UBO
@@ -161,7 +163,7 @@ struct alignas(256) PerRenderableUib {
};
static_assert(sizeof(PerRenderableUib) % 256 == 0, "sizeof(Transform) should be a multiple of 256");
struct LightsUib {
struct LightsUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
static constexpr utils::StaticString _name{ "LightsUniforms" };
math::float4 positionFalloff; // { float3(pos), 1/falloff^2 }
math::float3 direction; // dir
@@ -183,7 +185,7 @@ struct LightsUib {
static_assert(sizeof(LightsUib) == 64, "the actual UBO is an array of 256 mat4");
// UBO for punctual (spot light) shadows.
struct ShadowUib {
struct ShadowUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
static constexpr utils::StaticString _name{ "ShadowUniforms" };
struct alignas(16) ShadowData {
math::mat4f lightFromWorldMatrix;
@@ -192,19 +194,22 @@ struct ShadowUib {
math::float4 lightFromWorldZ;
float texelSizeAtOneMeter;
float bulbRadiusLs;
float nearOverFarMinusNear;
};
ShadowData shadows[CONFIG_MAX_SHADOW_CASTING_SPOTS];
};
static_assert(sizeof(ShadowUib) <= 16384, "ShadowUib exceed max UBO size");
// UBO froxel record buffer.
struct FroxelRecordUib {
struct FroxelRecordUib { // NOLINT(cppcoreguidelines-pro-type-member-init)
static constexpr utils::StaticString _name{ "FroxelRecordUniforms" };
math::uint4 records[1024];
};
static_assert(sizeof(FroxelRecordUib) == 16384, "FroxelRecordUib should be exactly 16KiB");
// This is not the UBO proper, but just an element of a bone array.
struct PerRenderableUibBone {
struct PerRenderableUibBone { // NOLINT(cppcoreguidelines-pro-type-member-init)
static constexpr utils::StaticString _name{ "BonesUniforms" };
math::quatf q = { 1, 0, 0, 0 };
math::float4 t = {};
+4 -2
View File
@@ -56,7 +56,9 @@ UniformInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
.add("lightDirection", 1, UniformInterfaceBlock::Type::FLOAT3)
.add("fParamsX", 1, UniformInterfaceBlock::Type::UINT)
// shadow
.add("shadowBias", 1, UniformInterfaceBlock::Type::FLOAT3)
.add("shadowBulbRadiusLs", 1, UniformInterfaceBlock::Type::FLOAT)
.add("shadowBias", 1, UniformInterfaceBlock::Type::FLOAT)
.add("reserved", 1, UniformInterfaceBlock::Type::FLOAT)
.add("oneOverFroxelDimensionY", 1, UniformInterfaceBlock::Type::FLOAT)
// froxels
.add("zParams", 1, UniformInterfaceBlock::Type::FLOAT4)
@@ -107,7 +109,7 @@ UniformInterfaceBlock const& UibGenerator::getPerViewUib() noexcept {
.add("vsmExponent", 1, UniformInterfaceBlock::Type::FLOAT)
.add("vsmDepthScale", 1, UniformInterfaceBlock::Type::FLOAT)
.add("vsmLightBleedReduction", 1, UniformInterfaceBlock::Type::FLOAT)
.add("vsmReserved0", 1, UniformInterfaceBlock::Type::FLOAT)
.add("shadowSamplingType", 1, UniformInterfaceBlock::Type::UINT)
.add("lodBias", 1, UniformInterfaceBlock::Type::FLOAT)
.add("oneOverFarMinusNear", 1, UniformInterfaceBlock::Type::FLOAT, Precision::HIGH)
+2
View File
@@ -212,6 +212,7 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Ditherin
static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, ShadowType* out) {
if (0 == compare(tokens[i], jsonChunk, "PCF")) { *out = ShadowType::PCF; }
else if (0 == compare(tokens[i], jsonChunk, "VSM")) { *out = ShadowType::VSM; }
else if (0 == compare(tokens[i], jsonChunk, "DPCF")) { *out = ShadowType::DPCF; }
else {
slog.w << "Invalid ShadowType: '" << STR(tokens[i], jsonChunk) << "'" << io::endl;
}
@@ -1130,6 +1131,7 @@ static std::ostream& operator<<(std::ostream& out, ShadowType in) {
switch (in) {
case ShadowType::PCF: return out << "\"PCF\"";
case ShadowType::VSM: return out << "\"VSM\"";
case ShadowType::DPCF: return out << "\"DPCF\"";
}
return out << "\"INVALID\"";
}
+20 -17
View File
@@ -767,26 +767,29 @@ void SimpleViewer::updateUserInterface() {
light.shadowOptions.mapSize = mapSize;
bool enableVsm = mSettings.view.shadowType == ShadowType::VSM;
ImGui::Checkbox("Enable VSM", &enableVsm);
mSettings.view.shadowType = enableVsm ? ShadowType::VSM : ShadowType::PCF;
int shadowType = (int)mSettings.view.shadowType;
ImGui::Combo("Shadow type", &shadowType, "PCF\0VSM\0DPCF\0\0");
mSettings.view.shadowType = (ShadowType)shadowType;
char label[32];
snprintf(label, 32, "%d", 1 << mVsmMsaaSamplesLog2);
ImGui::SliderInt("VSM MSAA samples", &mVsmMsaaSamplesLog2, 0, 3, label);
light.shadowOptions.vsm.msaaSamples = static_cast<uint8_t>(1u << mVsmMsaaSamplesLog2);
if (mSettings.view.shadowType == ShadowType::VSM) {
char label[32];
snprintf(label, 32, "%d", 1 << mVsmMsaaSamplesLog2);
ImGui::SliderInt("VSM MSAA samples", &mVsmMsaaSamplesLog2, 0, 3, label);
light.shadowOptions.vsm.msaaSamples =
static_cast<uint8_t>(1u << mVsmMsaaSamplesLog2);
int vsmAnisotropy = mSettings.view.vsmShadowOptions.anisotropy;
snprintf(label, 32, "%d", 1 << vsmAnisotropy);
ImGui::SliderInt("VSM anisotropy", &vsmAnisotropy, 0, 3, label);
mSettings.view.vsmShadowOptions.anisotropy = vsmAnisotropy;
ImGui::Checkbox("VSM mipmapping", &mSettings.view.vsmShadowOptions.mipmapping);
ImGui::SliderFloat("VSM blur", &light.shadowOptions.vsm.blurWidth, 0.0f, 125.0f);
int vsmAnisotropy = mSettings.view.vsmShadowOptions.anisotropy;
snprintf(label, 32, "%d", 1 << vsmAnisotropy);
ImGui::SliderInt("VSM anisotropy", &vsmAnisotropy, 0, 3, label);
mSettings.view.vsmShadowOptions.anisotropy = vsmAnisotropy;
ImGui::Checkbox("VSM mipmapping", &mSettings.view.vsmShadowOptions.mipmapping);
ImGui::SliderFloat("VSM blur", &light.shadowOptions.vsm.blurWidth, 0.0f, 125.0f);
// These are not very useful in practice (defaults are good), but we keep them here for debugging
//ImGui::SliderFloat("VSM exponent", &mSettings.view.vsmShadowOptions.exponent, 0.0, 6.0f);
//ImGui::SliderFloat("VSM Light bleed", &mSettings.view.vsmShadowOptions.lightBleedReduction, 0.0, 1.0f);
//ImGui::SliderFloat("VSM min variance scale", &mSettings.view.vsmShadowOptions.minVarianceScale, 0.0, 10.0f);
// These are not very useful in practice (defaults are good), but we keep them here for debugging
//ImGui::SliderFloat("VSM exponent", &mSettings.view.vsmShadowOptions.exponent, 0.0, 6.0f);
//ImGui::SliderFloat("VSM Light bleed", &mSettings.view.vsmShadowOptions.lightBleedReduction, 0.0, 1.0f);
//ImGui::SliderFloat("VSM min variance scale", &mSettings.view.vsmShadowOptions.minVarianceScale, 0.0, 10.0f);
}
int shadowCascades = light.shadowOptions.shadowCascades;
ImGui::SliderInt("Cascades", &shadowCascades, 1, 4);
+2
View File
@@ -31,4 +31,6 @@ struct ShadowData {
float normalBias;
highp vec4 lightFromWorldZ;
float texelSizeAtOneMeter;
float bulbRadiusLs;
float nearOverFarMinusNear;
};
+4 -5
View File
@@ -99,15 +99,14 @@ highp vec3 getNormalizedViewportCoord2() {
}
#if defined(HAS_SHADOWING) && defined(HAS_DYNAMIC_LIGHTING)
highp vec4 getSpotLightSpacePosition(uint index) {
highp vec4 getSpotLightSpacePosition(uint index, highp float zLight) {
highp mat4 lightFromWorldMatrix = shadowUniforms.shadows[index].lightFromWorldMatrix;
highp vec3 dir = shadowUniforms.shadows[index].direction;
// for spotlights, the bias depends on z
highp float z = dot(shadowUniforms.shadows[index].lightFromWorldZ, vertex_worldPosition);
float bias = shadowUniforms.shadows[index].normalBias * z;
float bias = shadowUniforms.shadows[index].normalBias * zLight;
return computeLightSpacePosition(vertex_worldPosition.xyz,
return computeLightSpacePosition(getWorldPosition(),
vertex_worldNormal, dir, bias, lightFromWorldMatrix);
}
#endif
@@ -140,7 +139,7 @@ highp vec4 getCascadeLightSpacePosition(uint cascade) {
}
return computeLightSpacePosition(getWorldPosition(), getWorldNormalVector(),
frameUniforms.lightDirection, frameUniforms.shadowBias.y,
frameUniforms.lightDirection, frameUniforms.shadowBias,
frameUniforms.lightFromWorldMatrix[cascade]);
}
+1 -1
View File
@@ -143,7 +143,7 @@ Light getLight(const uint index) {
highp uint channels = floatBitsToUint(data[3][3]);
// poition-to-light vector
highp vec3 worldPosition = vertex_worldPosition.xyz;
highp vec3 worldPosition = getWorldPosition();
highp vec3 posToLight = positionFalloff.xyz - worldPosition;
// and populate the Light structure
+2 -2
View File
@@ -114,7 +114,7 @@ void main() {
#if defined(HAS_SHADOWING) && defined(HAS_DIRECTIONAL_LIGHTING)
vertex_lightSpacePosition = computeLightSpacePosition(
vertex_worldPosition.xyz, vertex_worldNormal,
frameUniforms.lightDirection, frameUniforms.shadowBias.y, getLightFromWorldMatrix());
frameUniforms.lightDirection, frameUniforms.shadowBias, getLightFromWorldMatrix());
#endif
#endif // !defined(USE_OPTIMIZED_DEPTH_VERTEX_SHADER)
@@ -145,7 +145,7 @@ void main() {
// (see ShadowMap.cpp).
// Use vertex_worldPosition.w which is otherwise not used to store the interpolated
// light-space depth.
highp float z = (frameUniforms.viewFromWorldMatrix * vec4(material.worldPosition.xyz, 1.0)).z;
highp float z = (getViewFromWorldMatrix() * getWorldPosition(material)).z;
// rescale [near, far] to [0, 1]
highp float depth = -z * frameUniforms.oneOverFarMinusNear - frameUniforms.nearOverFarMinusNear;
+190 -15
View File
@@ -1,3 +1,12 @@
//------------------------------------------------------------------------------
// Shadow Sampling Types
//------------------------------------------------------------------------------
// Keep this in sync with PerViewUniforms.h
#define SHADOW_SAMPLING_RUNTIME_PCF 0u
#define SHADOW_SAMPLING_RUNTIME_VSM 1u
#define SHADOW_SAMPLING_RUNTIME_DPCF 2u
//------------------------------------------------------------------------------
// PCF Shadow Sampling
//------------------------------------------------------------------------------
@@ -12,13 +21,17 @@ float sampleDepth(const mediump sampler2DArrayShadow map, const uint layer,
// use hardware assisted PCF
float ShadowSample_PCF(const mediump sampler2DArrayShadow map,
const uint layer, const highp vec3 position) {
const uint layer, const highp vec4 shadowPosition) {
highp vec3 position = shadowPosition.xyz * (1.0 / shadowPosition.w);
// note: shadowPosition.z is in the [1, 0] range (reversed Z)
return sampleDepth(map, layer, position.xy, position.z);
}
// use manual PCF
float ShadowSample_PCF(const mediump sampler2DArray shadowMap,
const uint layer, const highp vec3 position) {
const uint layer, const highp vec4 shadowPosition) {
highp vec3 position = shadowPosition.xyz * (1.0 / shadowPosition.w);
// note: shadowPosition.z is in the [1, 0] range (reversed Z)
highp vec2 size = vec2(textureSize(shadowMap, 0));
highp vec2 st = position.xy * size - 0.5;
vec4 d;
@@ -31,11 +44,154 @@ float ShadowSample_PCF(const mediump sampler2DArray shadowMap,
d[2] = texelFetchOffset(shadowMap, tc, 0, ivec2(1, 0)).r;
d[3] = texelFetchOffset(shadowMap, tc, 0, ivec2(0, 0)).r;
#endif
vec4 pcf = step(0.0, d - position.zzzz);
vec4 pcf = step(0.0, position.zzzz - d);
highp vec2 grad = fract(st);
return mix(mix(pcf.w, pcf.z, grad.x), mix(pcf.x, pcf.y, grad.x), grad.y);
}
//------------------------------------------------------------------------------
// DPCF sampling
//------------------------------------------------------------------------------
const uint DPCF_SHADOW_TAP_COUNT = 12u; // 12 max
const vec2 poissonDisk[12] = vec2[12](
vec2(-0.326,-0.406), vec2(-0.840,-0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621),
vec2( 0.962,-0.195), vec2( 0.473,-0.480), vec2( 0.519, 0.767), vec2( 0.185,-0.893),
vec2( 0.507, 0.064), vec2( 0.896, 0.412), vec2(-0.322,-0.933), vec2(-0.792,-0.598)
);
float random(const highp vec2 w) {
const vec3 m = vec3(0.06711056, 0.00583715, 52.9829189);
return fract(m.z * fract(dot(w, m.xy)));
}
float hardenedKernel(float x) {
// this is basically a stronger smoothstep()
x = 2.0 * x - 1.0;
float s = sign(x);
x = 1.0 - s * x;
x = x * x * x;
x = s - x * s;
return 0.5 * x + 0.5;
}
highp vec2 computeReceiverPlaneDepthBias(const highp vec3 position) {
// see: GDC '06: Shadow Mapping: GPU-based Tips and Techniques
// Chain rule to compute dz/du and dz/dv
// |dz/du| |du/dx du/dy|^-T |dz/dx|
// |dz/dv| = |dv/dx dv/dy| * |dz/dy|
highp vec3 duvz_dx = dFdx(position);
highp vec3 duvz_dy = dFdy(position);
highp vec2 dz_duv = inverse(transpose(mat2(duvz_dx.xy, duvz_dy.xy))) * vec2(duvz_dx.z, duvz_dy.z);
return dz_duv;
}
mat2 getRandomRotationMatrix(highp vec2 fragCoord) {
// rotate the poisson disk randomly
fragCoord += vec2(frameUniforms.temporalNoise); // 0 when TAA is not used
float randomAngle = random(fragCoord) * (2.0 * PI);
vec2 randomBase = vec2(cos(randomAngle), sin(randomAngle));
mat2 R = mat2(randomBase.x, randomBase.y, -randomBase.y, randomBase.x);
return R;
}
float getPenumbra(const bool DIRECTIONAL, const uint index, const highp float zLight) {
float penumbra;
// This conditional is resolved at compile time
if (DIRECTIONAL) {
penumbra = frameUniforms.shadowBulbRadiusLs;
} else {
// the penumbra radius depends on the light-space z for spotlights
penumbra = shadowUniforms.shadows[index].bulbRadiusLs / zLight;
}
return penumbra;
}
float getPenumbraRatio(const bool DIRECTIONAL, const uint index,
float z_receiver, float z_blocker) {
// z_receiver/z_blocker are not linear depths (i.e. they're not distances)
// Penumbra ratio for PCSS is given by: pr = (d_receiver - d_blocker) / d_blocker
float penumbraRatio;
if (DIRECTIONAL) {
// TODO: take lispsm into account
// For directional lights, the depths are linear but depend on the position (because of LiSPSM).
// With: z_linear = f + z * (n - f)
// We get: (r-b)/b ==> (f/(n-f) + r_linear) / (f/(n-f) + b_linear) - 1
// Assuming f>>n and ignoring LISPSM, we get:
penumbraRatio = (z_blocker - z_receiver) / (1.0 - z_blocker);
} else {
// For spotlights, the depths are congruent to 1/z, specifically:
// z_linear = (n * f) / (n + z * (f - n))
// replacing in (r - b) / b gives:
float nearOverFarMinusNear = shadowUniforms.shadows[index].nearOverFarMinusNear;
penumbraRatio = (nearOverFarMinusNear + z_blocker) / (nearOverFarMinusNear + z_receiver) - 1.0;
}
return penumbraRatio;
}
/*
* DPCF, PCF with contact hardenning simulation.
* see "Shadow of Cold War", A scalable approach to shadowing -- by Kevin Myers
*/
float ShadowSample_DPCF(const bool DIRECTIONAL,
const mediump sampler2DArray map, const uint layer, const uint index,
const highp vec4 shadowPosition, const highp float zLight) {
highp vec3 position = shadowPosition.xyz * (1.0 / shadowPosition.w);
highp vec2 texelSize = vec2(1.0) / vec2(textureSize(map, 0));
// We need to use the shadow receiver plane depth bias to combat shadow acne due to the
// large kernel.
highp vec2 dz_duv = computeReceiverPlaneDepthBias(position);
float penumbra = getPenumbra(DIRECTIONAL, index, zLight);
// rotate the poisson disk randomly
mat2 R = getRandomRotationMatrix(gl_FragCoord.xy);
float occludedCount = 0.0;
float z_occSum = 0.0;
for (uint i = 0u; i < DPCF_SHADOW_TAP_COUNT; i++) {
highp vec2 duv = R * poissonDisk[i] * (texelSize * penumbra);
float z_occ = textureLod(map, vec3(position.xy + duv, layer), 0.0).r;
// note: z_occ and z_rec are not necessarily linear here, comparing them is always okay for
// the regular PCF, but the "distance" is meaningless unless they are actually linear
// (e.g.: for the directional light).
// Either way, if we assume that all the samples are close to each other we can take their
// average regardless, and the average depth value of the occluders
// becomes: z_occSum / occludedCount.
// receiver plane depth bias
float z_bias = dot(dz_duv, duv);
float dz = z_occ - position.z; // dz>0 when blocker is between receiver and light
float occluded = step(z_bias, dz);
occludedCount += occluded;
z_occSum += z_occ * occluded;
}
// early exit if there is no occluders at all, also avoids a divide-by-zero below.
if (z_occSum == 0.0) {
return 1.0;
}
float penumbraRatio = getPenumbraRatio(DIRECTIONAL, index, position.z, z_occSum / occludedCount);
// The main way we're diverging from PCSS is that we're not going to sample again, instead
// we're going to reuse the blocker search samples and we're going to use the penumbra ratio
// as a parameter to lerp between a hardened PCF kernel and the search PCF kernel.
// We need a parameter to blend between the the "hardened" kernel and the "soft" kernel,
// to this end clamp the penumbra ratio between 0 (blocker is close to the receiver) and
// 1 (blocker is close to the light).
penumbraRatio = saturate(penumbraRatio);
// regular PCF weight (i.e. average of samples in shadow)
float percentageOccluded = occludedCount * (1.0 / float(DPCF_SHADOW_TAP_COUNT));
// now we just need to lerp between hardened PCF and regular PCF based on alpha
percentageOccluded = mix(hardenedKernel(percentageOccluded), percentageOccluded, penumbraRatio);
return 1.0 - percentageOccluded;
}
//------------------------------------------------------------------------------
// Screen-space Contact Shadows
//------------------------------------------------------------------------------
@@ -147,7 +303,13 @@ float evaluateShadowVSM(const highp vec2 moments, const highp float depth) {
}
float ShadowSample_VSM(const mediump sampler2DArray shadowMap,
const uint layer, const highp vec3 position) {
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);
// Read the shadow map with all available filtering
highp vec4 moments = texture(shadowMap, vec3(position.xy, layer));
highp float depth = position.z;
@@ -186,21 +348,21 @@ float shadow(const bool DIRECTIONAL,
#endif
} else {
#if defined(HAS_DYNAMIC_LIGHTING)
shadowPosition = getSpotLightSpacePosition(index);
highp float zLight = dot(shadowUniforms.shadows[index].lightFromWorldZ, vec4(getWorldPosition(), 1.0));
shadowPosition = getSpotLightSpacePosition(index, zLight);
#endif
}
highp vec3 position = shadowPosition.xyz * (1.0 / shadowPosition.w);
// note: shadowPosition.z is in the [1, 0] range (reversed Z)
return ShadowSample_PCF(shadowMap, layer, position);
return ShadowSample_PCF(shadowMap, layer, shadowPosition);
}
// VSM sampling
// VSM or DPCF sampling
float shadow(const bool DIRECTIONAL,
const mediump sampler2DArray shadowMap,
const uint layer, const uint index, const uint cascade) {
highp vec4 shadowPosition;
highp float zLight = 0.0;
// This conditional is resolved at compile time
if (DIRECTIONAL) {
@@ -209,13 +371,26 @@ float shadow(const bool DIRECTIONAL,
#endif
} else {
#if defined(HAS_DYNAMIC_LIGHTING)
shadowPosition = getSpotLightSpacePosition(index);
zLight = dot(shadowUniforms.shadows[index].lightFromWorldZ, vec4(getWorldPosition(), 1.0));
shadowPosition = getSpotLightSpacePosition(index, zLight);
#endif
}
// 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);
return ShadowSample_VSM(shadowMap, layer, position);
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_VSM) {
return ShadowSample_VSM(shadowMap, layer, shadowPosition);
}
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_DPCF) {
return ShadowSample_DPCF(DIRECTIONAL, shadowMap, layer, index, shadowPosition, zLight);
}
if (frameUniforms.shadowSamplingType == SHADOW_SAMPLING_RUNTIME_PCF) {
// 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, layer, shadowPosition);
}
// should not happen
return 0.0;
}