Files
filament/filament/src/ShadowMapManager.cpp
T
Mathias Agopian 130825422e Shadowmap texture vizualizer
The vizualizer is implemented inside filament itself and activated
using the debug registry. This intended for filament development use.
2024-01-24 11:28:11 -08:00

970 lines
44 KiB
C++

/*
* Copyright (C) 2020 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 "ShadowMapManager.h"
#include "RenderPass.h"
#include "ShadowMap.h"
#include "details/Texture.h"
#include "details/View.h"
#include <fg/FrameGraph.h>
#include <utils/debug.h>
#include <utils/FixedCapacityVector.h>
namespace filament {
using namespace backend;
using namespace math;
ShadowMapManager::ShadowMapManager(FEngine& engine)
: mEngine(engine) {
// initialize our ShadowMap array in-place
UTILS_NOUNROLL
for (auto& entry : mShadowMapCache) {
new (&entry) ShadowMap(engine);
}
mShadowUbh = engine.getDriverApi().createBufferObject(mShadowUb.getSize(),
BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC);
FDebugRegistry& debugRegistry = engine.getDebugRegistry();
debugRegistry.registerProperty("d.shadowmap.visualize_cascades",
&engine.debug.shadowmap.visualize_cascades);
debugRegistry.registerProperty("d.shadowmap.tightly_bound_scene",
&engine.debug.shadowmap.tightly_bound_scene);
}
ShadowMapManager::~ShadowMapManager() {
// destroy the ShadowMap array in-place
UTILS_NOUNROLL
for (auto& entry : mShadowMapCache) {
std::destroy_at(std::launder(reinterpret_cast<ShadowMap*>(&entry)));
}
}
void ShadowMapManager::terminate(FEngine& engine) {
DriverApi& driver = engine.getDriverApi();
driver.destroyBufferObject(mShadowUbh);
UTILS_NOUNROLL
for (auto& entry : mShadowMapCache) {
std::launder(reinterpret_cast<ShadowMap*>(&entry))->terminate(engine);
}
}
ShadowMapManager::ShadowTechnique ShadowMapManager::update(FEngine& engine, FView& view,
CameraInfo const& cameraInfo,
FScene::RenderableSoa& renderableData, FScene::LightSoa const& lightData) noexcept {
ShadowTechnique shadowTechnique = {};
calculateTextureRequirements(engine, view, lightData);
// Compute scene-dependent values shared across all shadow maps
ShadowMap::SceneInfo const info{ *view.getScene(), view.getVisibleLayers(), cameraInfo.view };
shadowTechnique |= updateCascadeShadowMaps(
engine, view, cameraInfo, renderableData, lightData, info);
shadowTechnique |= updateSpotShadowMaps(
engine, lightData);
mSceneInfo = info;
return shadowTechnique;
}
void ShadowMapManager::reset() noexcept {
mDirectionalShadowMapCount = 0;
mSpotShadowMapCount = 0;
}
void ShadowMapManager::setDirectionalShadowMap(size_t lightIndex,
LightManager::ShadowOptions const* options) noexcept {
assert_invariant(options->shadowCascades <= CONFIG_MAX_SHADOW_CASCADES);
// this updates getCascadedShadowMap()
mDirectionalShadowMapCount = options->shadowCascades;
utils::Slice<ShadowMap> cascadedShadowMap = getCascadedShadowMap();
for (size_t c = 0; c < options->shadowCascades; c++) {
ShadowMap& shadowMap = cascadedShadowMap[c];
shadowMap.initialize(lightIndex, ShadowType::DIRECTIONAL, c, 0, options);
}
}
void ShadowMapManager::addShadowMap(size_t lightIndex, bool spotlight,
LightManager::ShadowOptions const* options) noexcept {
if (spotlight) {
const size_t c = mSpotShadowMapCount++;
const size_t i = c + CONFIG_MAX_SHADOW_CASCADES;
assert_invariant(i < CONFIG_MAX_SHADOWMAPS);
auto& shadowMap = getShadowMap(i);
shadowMap.initialize(lightIndex, ShadowType::SPOT, i, 0, options);
} else {
// point-light, generate 6 independent shadowmaps
for (size_t face = 0; face < 6; face++) {
const size_t c = mSpotShadowMapCount++;
const size_t i = c + CONFIG_MAX_SHADOW_CASCADES;
assert_invariant(i < CONFIG_MAX_SHADOWMAPS);
auto& shadowMap = getShadowMap(i);
shadowMap.initialize(lightIndex, ShadowType::POINT, i, face, options);
}
}
}
FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameGraph& fg,
RenderPass const& pass, 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;
assert_invariant(textureRequirements.layers <= CONFIG_MAX_SHADOW_LAYERS);
// -------------------------------------------------------------------------------------------
// Prepare Shadow Pass
// -------------------------------------------------------------------------------------------
struct PrepareShadowPassData {
struct ShadowPass {
mutable RenderPass::Executor executor;
ShadowMap* shadowMap;
utils::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;
};
VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions();
auto& prepareShadowPass = fg.addPass<PrepareShadowPassData>("Prepare Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
data.passList.reserve(CONFIG_MAX_SHADOWMAPS);
data.shadows = builder.createTexture("Shadowmap", {
.width = textureRequirements.size, .height = textureRequirements.size,
.depth = textureRequirements.layers,
.levels = textureRequirements.levels,
.type = SamplerType::SAMPLER_2D_ARRAY,
.format = textureRequirements.format
});
// these loops create a list of the shadow maps that might need to be rendered
auto& passList = data.passList;
// Directional, cascaded shadow maps
auto const directionalShadowCastersRange = view.getVisibleDirectionalShadowCasters();
if (!directionalShadowCastersRange.empty()) {
for (auto& shadowMap : getCascadedShadowMap()) {
// for the directional light, we already know if it has visible shadows.
if (shadowMap.hasVisibleShadows()) {
passList.push_back({
{}, &shadowMap, directionalShadowCastersRange,
VISIBLE_DIR_SHADOW_RENDERABLE });
}
}
}
// Point lights and Spotlight shadow maps
auto const spotShadowCastersRange = view.getVisibleSpotShadowCasters();
if (!spotShadowCastersRange.empty()) {
for (auto& shadowMap : getSpotShadowMaps()) {
assert_invariant(!shadowMap.isDirectionalShadow());
passList.push_back({
{}, &shadowMap, spotShadowCastersRange,
VISIBLE_DYN_SHADOW_RENDERABLE });
}
}
assert_invariant(passList.size() <= textureRequirements.layers);
// This pass must be declared as having a side effect because it never gets a
// "read" from one of its resource (only writes), so the FrameGraph culls it.
builder.sideEffect();
},
[this, &engine, &view, vsmShadowOptions,
scene, mainCameraInfo, userTime, passTemplate = pass](
FrameGraphResources const&, auto const& data, DriverApi& driver) {
// Note: we could almost parallel_for the loop below, the problem currently is
// that updatePrimitivesLod() updates temporary global state.
// prepareSpotShadowMap() also update the visibility of renderable. These two
// pieces of state are needed only until shadowMap.render() returns.
// Conceptually, we could store this out-of-band.
// Generate a RenderPass for each shadow map
for (auto const& entry : data.passList) {
ShadowMap& shadowMap = *entry.shadowMap;
// for spot shadow map, we need to do the culling
switch (shadowMap.getShadowType()) {
case ShadowType::DIRECTIONAL:
break;
case ShadowType::SPOT:
prepareSpotShadowMap(shadowMap, engine, view, mainCameraInfo,
scene->getRenderableData(), entry.range,
scene->getLightData(), mSceneInfo);
break;
case ShadowType::POINT:
preparePointShadowMap(shadowMap, engine, view, mainCameraInfo,
scene->getRenderableData(), entry.range,
scene->getLightData(), mSceneInfo);
break;
}
if (shadowMap.hasVisibleShadows()) {
// Note: this loop can generate a lot of commands that come out of the
// "per frame command arena". The allocation persists until the
// end of the frame.
// One way to possibly mitigate this, would be to always use the
// same command buffer for all shadow map, but then we'd generate
// a lot of unneeded draw calls.
// To do this efficiently, we'd need a way to cull draw calls already
// recorded in the command buffer, per shadow map.
// cameraInfo only valid after calling update
const CameraInfo cameraInfo{ shadowMap.getCamera(), mainCameraInfo };
auto transaction = ShadowMap::open(driver);
ShadowMap::prepareCamera(transaction, engine, cameraInfo);
ShadowMap::prepareViewport(transaction, shadowMap.getViewport());
ShadowMap::prepareTime(transaction, engine, userTime);
ShadowMap::prepareShadowMapping(transaction,
vsmShadowOptions.highPrecision);
shadowMap.commit(transaction, driver);
// updatePrimitivesLod must be run before RenderPass::appendCommands.
view.updatePrimitivesLod(engine,
cameraInfo, scene->getRenderableData(), entry.range);
// generate and sort the commands for rendering the shadow map
RenderPass pass(passTemplate);
pass.setCamera(cameraInfo);
pass.setVisibilityMask(entry.visibilityMask);
pass.setGeometry(scene->getRenderableData(),
entry.range, scene->getRenderableUBO());
pass.appendCommands(engine, RenderPass::SHADOW);
pass.sortCommands(engine);
entry.executor = pass.getExecutor();
if (!view.hasVSM()) {
auto const* options = shadowMap.getShadowOptions();
const PolygonOffset polygonOffset = { // handle reversed Z
.slope = -options->polygonOffsetSlope,
.constant = -options->polygonOffsetConstant
};
entry.executor.overridePolygonOffset(&polygonOffset);
}
}
}
// Finally update our UBO in one batch
if (mShadowUb.isDirty()) {
driver.updateBufferObject(mShadowUbh,
mShadowUb.toBufferDescriptor(driver), 0);
}
});
// -------------------------------------------------------------------------------------------
// Shadow Passes
// -------------------------------------------------------------------------------------------
fg.getBlackboard()["shadowmap"] = prepareShadowPass->shadows;
struct ShadowPassData {
FrameGraphId<FrameGraphTexture> tempBlurSrc{}; // temporary shadowmap when blurring
FrameGraphId<FrameGraphTexture> output;
uint32_t rt{};
};
auto const& passList = prepareShadowPass.getData().passList;
for (auto const& entry: passList) {
if (!entry.shadowMap->hasVisibleShadows()) {
continue;
}
const uint8_t layer = entry.shadowMap->getLayer();
const auto* options = entry.shadowMap->getShadowOptions();
const auto msaaSamples = textureRequirements.msaaSamples;
auto& shadowPass = fg.addPass<ShadowPassData>("Shadow Pass",
[&](FrameGraph::Builder& builder, auto& data) {
const bool blur = view.hasVSM() && options->vsm.blurWidth > 0.0f;
FrameGraphRenderPass::Descriptor renderTargetDesc{};
data.output = builder.createSubresource(prepareShadowPass->shadows,
"Shadowmap Layer", { .layer = layer });
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).
// When rendering VSM shadow maps, we still need a depth texture for sorting.
// We specify the sample count here because we don't need automatic resolve.
auto depth = builder.createTexture("Temporary VSM Depth Texture", {
.width = textureRequirements.size, .height = textureRequirements.size,
.samples = msaaSamples,
.format = TextureFormat::DEPTH16,
});
// 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
});
depth = builder.write(depth,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
data.output = builder.write(data.output,
FrameGraphTexture::Usage::COLOR_ATTACHMENT);
renderTargetDesc.attachments.color[0] = data.output;
renderTargetDesc.attachments.depth = depth;
renderTargetDesc.clearFlags =
TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH;
// we need to clear the shadow map with the max EVSM moments
renderTargetDesc.clearColor = vsmClearColor;
renderTargetDesc.samples = msaaSamples;
if (blur) {
data.tempBlurSrc = builder.write(data.tempBlurSrc,
FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.rt = builder.declareRenderPass("Temp Shadow RT", {
.attachments = {
.color = { data.tempBlurSrc },
.depth = depth },
.clearColor = vsmClearColor,
.samples = msaaSamples,
.clearFlags = TargetBufferFlags::COLOR
| TargetBufferFlags::DEPTH
});
}
} else {
// the shadowmap layer
data.output = builder.write(data.output,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
renderTargetDesc.attachments.depth = data.output;
renderTargetDesc.clearFlags = TargetBufferFlags::DEPTH;
}
// 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;
},
[=, &engine, &entry](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
// Note: we capture entry by reference here. That's actually okay because
// `entry` lives in `PrepareShadowPassData` which is guaranteed to still
// be alive when we execute here (all passes stay alive until the FrameGraph
// is destroyed).
// It wouldn't work to capture by copy because entry.executor wouldn't be
// initialized, as this happens in an `execute` block.
auto rt = resources.getRenderPassInfo(data.rt);
engine.flush();
driver.beginRenderPass(rt.target, rt.params);
entry.shadowMap->bind(driver);
entry.executor.overrideScissor(entry.shadowMap->getScissor());
entry.executor.execute(engine, "Shadow Pass");
driver.endRenderPass();
});
// now emit the blurring passes if needed
if (view.hasVSM()) {
auto& ppm = engine.getPostProcessManager();
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);
kernelWidth = kernelWidth * 4 + 5;
ppm.gaussianBlurPass(fg,
shadowPass->tempBlurSrc,
shadowPass->output,
false, kernelWidth, sigma);
}
// 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++) {
const bool finalize = level == textureRequirements.levels - 2;
ppm.vsmMipmapPass(fg, prepareShadowPass->shadows, layer, level,
vsmClearColor, finalize);
}
}
}
}
return prepareShadowPass->shadows;
}
ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEngine& engine,
FView& view, CameraInfo cameraInfo, FScene::RenderableSoa& renderableData,
FScene::LightSoa const& lightData, ShadowMap::SceneInfo sceneInfo) noexcept {
FScene* scene = view.getScene();
auto& lcm = engine.getLightManager();
FLightManager::Instance const directionalLight = lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
FLightManager::ShadowOptions const& options = lcm.getShadowOptions(directionalLight);
FLightManager::ShadowParams const& params = lcm.getShadowParams(directionalLight);
// Adjust the camera's projection for the light's shadowFar
cameraInfo.zf = params.options.shadowFar > 0.0f ? params.options.shadowFar : cameraInfo.zf;
if (UTILS_UNLIKELY(params.options.shadowFar > 0.0f)) {
cameraInfo.zf = params.options.shadowFar;
float const n = cameraInfo.zn;
float const f = cameraInfo.zf;
/*
* Updating a projection matrix near and far planes:
*
* We assume that the near and far plane equations are of the form:
* N = { 0, 0, 1, n }
* F = { 0, 0, -1, -f }
*
* We also assume that the lower-left 2x2 of the projection is all 0:
* P = A 0 C 0
* 0 B D 0
* 0 0 E F
* 0 0 G H
*
* It result that we need to calculate E and F while leaving all other parameter unchanged.
*
* We know that:
* with N, F the near/far normalized plane equation parameters
* sn, sf arbitrary scale factors (they don't affect the planes)
* m is the transpose of projection (see Frustum.cpp)
*
* sn * N == -m[3] - m[2]
* sf * F == -m[3] + m[2]
*
* sn * N + sf * F == -2 * m[3]
* sn * N - sf * F == -2 * m[2]
*
* sn * N.z + sf * F.z == -2 * m[3].z
* sn * N.w + sf * F.w == -2 * m[3].w
* sn * N.z - sf * F.z == -2 * m[2].z
* sn * N.w - sf * F.w == -2 * m[2].w
*
* sn * N.z + sf * F.z == -2 * p[2].w
* sn * N.w + sf * F.w == -2 * p[3].w
* sn * N.z - sf * F.z == -2 * p[2].z
* sn * N.w - sf * F.w == -2 * p[3].z
*
* We now need to solve for { p[2].z, p[3].z, sn, sf } :
*
* sn == -2 * (p[2].w * F.w - p[3].w * F.z) / (N.z * F.w - N.w * F.z)
* sf == -2 * (p[2].w * N.w - p[3].w * N.z) / (F.z * N.w - F.w * N.z)
* p[2].z == (sf * F.z - sn * N.z) / 2
* p[3].z == (sf * F.w - sn * N.w) / 2
*/
auto& p = cameraInfo.cullingProjection;
float4 const N = { 0, 0, 1, n }; // near plane equation
float4 const F = { 0, 0, -1, -f }; // far plane equation
// near plane equation scale factor
float const sn = -2.0f * (p[2].w * F.w - p[3].w * F.z) / (N.z * F.w - N.w * F.z);
// far plane equation scale factor
float const sf = -2.0f * (p[2].w * N.w - p[3].w * N.z) / (F.z * N.w - F.w * N.z);
// New values for the projection
p[2].z = (sf * F.z - sn * N.z) * 0.5f;
p[3].z = (sf * F.w - sn * N.w) * 0.5f;
}
const ShadowMap::ShadowMapInfo shadowMapInfo{
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options.mapSize),
.shadowDimension = uint16_t(options.mapSize - 2u),
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN,
.vsm = view.hasVSM()
};
bool hasVisibleShadows = false;
utils::Slice<ShadowMap> 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.
ShadowMap& shadowMap = cascadedShadowMaps[0];
const auto direction = lightData.elementAt<FScene::SHADOW_DIRECTION>(0);
// We compute the directional light's model matrix using the origin's as the light position.
// The choice of the light's origin initially doesn't matter for a directional light.
// This will be adjusted later because of how we compute the depth metric for VSM.
const mat4f MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction,
normalize(cameraInfo.worldTransform[0].xyz));
// Compute scene-dependent values shared across all cascades
ShadowMap::updateSceneInfoDirectional(MvAtOrigin, *scene, sceneInfo);
shadowMap.updateDirectional(mEngine,
lightData, 0, cameraInfo, shadowMapInfo, sceneInfo);
hasVisibleShadows = shadowMap.hasVisibleShadows();
if (hasVisibleShadows) {
Frustum const& frustum = shadowMap.getCamera().getCullingFrustum();
FView::cullRenderables(engine.getJobSystem(), renderableData, frustum,
VISIBLE_DIR_SHADOW_RENDERABLE_BIT);
}
}
ShadowTechnique shadowTechnique{};
uint32_t directionalShadowsMask = 0;
uint32_t cascadeHasVisibleShadows = 0;
if (hasVisibleShadows) {
// Adjust the near and far planes to tightly bound the scene.
float vsNear = -cameraInfo.zn;
float vsFar = -cameraInfo.zf;
if (engine.debug.shadowmap.tightly_bound_scene && !params.options.stable) {
vsNear = std::min(vsNear, sceneInfo.vsNearFar.x);
vsFar = std::max(vsFar, sceneInfo.vsNearFar.y);
}
const size_t cascadeCount = cascadedShadowMaps.size();
// We divide the camera frustum into N cascades. This gives us N + 1 split positions.
// The first split position is the near plane; the last split position is the far plane.
std::array<float, CascadeSplits::SPLIT_COUNT> splitPercentages{};
splitPercentages[cascadeCount] = 1.0f;
for (size_t i = 1; i < cascadeCount; i++) {
splitPercentages[i] = options.cascadeSplitPositions[i - 1];
}
const CascadeSplits splits({
.proj = cameraInfo.cullingProjection,
.near = vsNear,
.far = vsFar,
.cascadeCount = cascadeCount,
.splitPositions = splitPercentages
});
// The split positions uniform is a float4. To save space, we chop off the first split position
// (which is the near plane, and doesn't need to be communicated to the shaders).
static_assert(CONFIG_MAX_SHADOW_CASCADES <= 5,
"At most, a float4 can fit 4 split positions for 5 shadow cascades");
float4 wsSplitPositionUniform{ -std::numeric_limits<float>::infinity() };
std::copy(splits.beginWs() + 1, splits.endWs(), &wsSplitPositionUniform[0]);
float csSplitPosition[CONFIG_MAX_SHADOW_CASCADES + 1];
std::copy(splits.beginCs(), splits.endCs(), csSplitPosition);
mShadowMappingUniforms.cascadeSplits = wsSplitPositionUniform;
// When computing the required bias we need a half-texel size, so we multiply by 0.5 here.
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : 0.5f * lcm.getShadowNormalBias(0);
for (size_t i = 0, c = cascadedShadowMaps.size(); i < c; i++) {
// Compute the frustum for the directional light.
ShadowMap& shadowMap = cascadedShadowMaps[i];
assert_invariant(shadowMap.getLightIndex() == 0);
sceneInfo.csNearFar = { csSplitPosition[i], csSplitPosition[i + 1] };
auto shaderParameters = shadowMap.updateDirectional(mEngine,
lightData, 0, cameraInfo, shadowMapInfo, sceneInfo);
if (shadowMap.hasVisibleShadows()) {
const size_t shadowIndex = shadowMap.getShadowIndex();
assert_invariant(shadowIndex == i);
// 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 = shaderParameters.texelSizeAtOneMeterWs;
auto& s = mShadowUb.edit();
s.shadows[shadowIndex].layer = shadowMap.getLayer();
s.shadows[shadowIndex].lightFromWorldMatrix = shaderParameters.lightSpace;
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSize;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSize;
s.shadows[shadowIndex].elvsm = options.vsm.elvsm;
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options.shadowBulbRadius / wsTexelSize;
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
cascadeHasVisibleShadows |= 0x1u << i;
}
}
}
// screen-space contact shadows for the directional light
float const screenSpaceShadowDistance = options.maxShadowDistance;
if (options.screenSpaceContactShadows) {
shadowTechnique |= ShadowTechnique::SCREEN_SPACE;
}
directionalShadowsMask |= std::min(uint8_t(255u), options.stepCount) << 8u;
if (any(shadowTechnique & ShadowTechnique::SHADOW_MAP)) {
directionalShadowsMask |= 0x1u;
}
if (any(shadowTechnique & ShadowTechnique::SCREEN_SPACE)) {
directionalShadowsMask |= 0x2u;
}
uint32_t cascades = 0;
cascades |= uint32_t(cascadedShadowMaps.size());
cascades |= cascadeHasVisibleShadows << 8u;
mShadowMappingUniforms.directionalShadows = directionalShadowsMask;
mShadowMappingUniforms.ssContactShadowDistance = screenSpaceShadowDistance;
mShadowMappingUniforms.cascades = cascades;
return shadowTechnique;
}
void ShadowMapManager::updateSpotVisibilityMasks(
uint8_t visibleLayers,
uint8_t const* UTILS_RESTRICT layers,
FRenderableManager::Visibility const* UTILS_RESTRICT visibility,
Culler::result_type* UTILS_RESTRICT visibleMask, size_t count) {
// __restrict__ seems to only be taken into account as function parameters. This is very
// important here, otherwise, this loop doesn't get vectorized.
// This is vectorized 16x.
count = (count + 0xFu) & ~0xFu; // capacity guaranteed to be multiple of 16
for (size_t i = 0; i < count; ++i) {
const Culler::result_type mask = visibleMask[i];
const FRenderableManager::Visibility v = visibility[i];
const bool inVisibleLayer = layers[i] & visibleLayers;
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);
}
}
void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap,
FEngine& engine, FView& view, CameraInfo const& mainCameraInfo,
FScene::RenderableSoa& renderableData, utils::Range<uint32_t> range,
FScene::LightSoa& lightData, ShadowMap::SceneInfo const& sceneInfo) noexcept {
auto& lcm = engine.getLightManager();
const size_t lightIndex = shadowMap.getLightIndex();
const FLightManager::Instance li = lightData.elementAt<FScene::LIGHT_INSTANCE>(lightIndex);
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
// compute the frustum for this light
// for spotlights, we cull shadow casters first because we already know the frustum,
// this will help us find better near/far plane later
const auto position = lightData.elementAt<FScene::POSITION_RADIUS>(lightIndex).xyz;
const auto direction = lightData.elementAt<FScene::DIRECTION>(lightIndex);
const auto radius = lightData.elementAt<FScene::POSITION_RADIUS>(lightIndex).w;
const auto outerConeAngle = lcm.getSpotLightOuterCone(li);
// compute shadow map frustum for culling
const mat4f Mv = ShadowMap::getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position);
const mat4f Mp = mat4f::perspective(outerConeAngle * f::RAD_TO_DEG * 2.0f, 1.0f, 0.01f, radius);
const mat4f MpMv = math::highPrecisionMultiply(Mp, Mv);
const Frustum frustum(MpMv);
// Cull shadow casters
float3 const* worldAABBCenter = renderableData.data<FScene::WORLD_AABB_CENTER>();
float3 const* worldAABBExtent = renderableData.data<FScene::WORLD_AABB_EXTENT>();
FScene::VisibleMaskType* visibleArray = renderableData.data<FScene::VISIBLE_MASK>();
Culler::intersects(
visibleArray + range.first,
frustum,
worldAABBCenter + range.first,
worldAABBExtent + range.first,
range.size(),
VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
// update their visibility mask
uint8_t const* layers = renderableData.data<FScene::LAYERS>();
auto const* visibility = renderableData.data<FScene::VISIBILITY_STATE>();
updateSpotVisibilityMasks(
view.getVisibleLayers(),
layers + range.first,
visibility + range.first,
visibleArray + range.first,
range.size());
// update the shadow map frustum/camera
const ShadowMap::ShadowMapInfo shadowMapInfo{
.atlasDimension = mTextureAtlasRequirements.size,
.textureDimension = uint16_t(options->mapSize),
.shadowDimension = uint16_t(options->mapSize - 2u),
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN,
.vsm = view.hasVSM()
};
auto shaderParameters = shadowMap.updateSpot(mEngine,
lightData, lightIndex, mainCameraInfo, shadowMapInfo, *view.getScene(), sceneInfo);
// and if we need to generate it, update all the UBO data
if (shadowMap.hasVisibleShadows()) {
const size_t shadowIndex = shadowMap.getShadowIndex();
const float wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
auto& s = mShadowUb.edit();
const double n = shadowMap.getCamera().getNear();
const double f = shadowMap.getCamera().getCullingFar();
s.shadows[shadowIndex].layer = shadowMap.getLayer();
s.shadows[shadowIndex].lightFromWorldMatrix = shaderParameters.lightSpace;
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n));
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
/ wsTexelSizeAtOneMeter;
}
}
void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
FEngine& engine, FView& view, CameraInfo const& mainCameraInfo,
FScene::RenderableSoa& renderableData, utils::Range<uint32_t> range,
FScene::LightSoa& lightData,
ShadowMap::SceneInfo const& sceneInfo) noexcept {
const uint8_t face = shadowMap.getFace();
const size_t lightIndex = shadowMap.getLightIndex();
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
// compute the frustum for this light
// for spotlights, we cull shadow casters first because we already know the frustum,
// this will help us find better near/far plane later
const auto position = lightData.elementAt<FScene::POSITION_RADIUS>(lightIndex).xyz;
const auto radius = lightData.elementAt<FScene::POSITION_RADIUS>(lightIndex).w;
// compute shadow map frustum for culling
const mat4f Mv = ShadowMap::getPointLightViewMatrix(TextureCubemapFace(face), position);
const mat4f Mp = mat4f::perspective(90.0f, 1.0f, 0.01f, radius);
const Frustum frustum{ math::highPrecisionMultiply(Mp, Mv) };
// Cull shadow casters
float3 const* worldAABBCenter = renderableData.data<FScene::WORLD_AABB_CENTER>();
float3 const* worldAABBExtent = renderableData.data<FScene::WORLD_AABB_EXTENT>();
FScene::VisibleMaskType* visibleArray = renderableData.data<FScene::VISIBLE_MASK>();
Culler::intersects(
visibleArray + range.first,
frustum,
worldAABBCenter + range.first,
worldAABBExtent + range.first,
range.size(),
VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
// update their visibility mask
uint8_t const* layers = renderableData.data<FScene::LAYERS>();
auto const* visibility = renderableData.data<FScene::VISIBILITY_STATE>();
updateSpotVisibilityMasks(
view.getVisibleLayers(),
layers + range.first,
visibility + range.first,
visibleArray + range.first,
range.size());
// 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
.textureSpaceFlipped = engine.getBackend() == Backend::METAL ||
engine.getBackend() == Backend::VULKAN,
.vsm = view.hasVSM()
};
auto shaderParameters = shadowMap.updatePoint(mEngine, lightData, lightIndex,
mainCameraInfo, shadowMapInfo, *view.getScene(), face);
// and if we need to generate it, update all the UBO data
if (shadowMap.hasVisibleShadows()) {
const size_t shadowIndex = shadowMap.getShadowIndex();
const float wsTexelSizeAtOneMeter = shaderParameters.texelSizeAtOneMeterWs;
// note: normalBias is set to zero for VSM
const float normalBias = shadowMapInfo.vsm ? 0.0f : options->normalBias;
auto& s = mShadowUb.edit();
const double n = shadowMap.getCamera().getNear();
const double f = shadowMap.getCamera().getCullingFar();
s.shadows[shadowIndex].layer = shadowMap.getLayer();
s.shadows[shadowIndex].lightFromWorldMatrix = shaderParameters.lightSpace;
s.shadows[shadowIndex].scissorNormalized = shaderParameters.scissorNormalized;
s.shadows[shadowIndex].normalBias = normalBias * wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].lightFromWorldZ = shaderParameters.lightFromWorldZ;
s.shadows[shadowIndex].texelSizeAtOneMeter = wsTexelSizeAtOneMeter;
s.shadows[shadowIndex].nearOverFarMinusNear = float(n / (f - n));
s.shadows[shadowIndex].elvsm = options->vsm.elvsm;
s.shadows[shadowIndex].bulbRadiusLs =
mSoftShadowOptions.penumbraScale * options->shadowBulbRadius
/ wsTexelSizeAtOneMeter;
}
}
ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine& engine,
FScene::LightSoa const& lightData) noexcept {
// The const_cast here is a little ugly, but conceptually lightData should be const,
// it's just that we're using it to store some temporary data. With SoA we can't have
// a `mutable` element, so that's a workaround.
FScene::ShadowInfo* const shadowInfo = const_cast<FScene::ShadowInfo*>(
lightData.data<FScene::SHADOW_INFO>());
ShadowTechnique shadowTechnique{};
utils::Slice<ShadowMap> const spotShadowMaps = getSpotShadowMaps();
if (!spotShadowMaps.empty()) {
shadowTechnique |= ShadowTechnique::SHADOW_MAP;
for (ShadowMap const& shadowMap : spotShadowMaps) {
const size_t lightIndex = shadowMap.getLightIndex();
// Gather the per-light (not per shadow map) information. For point lights we will
// "see" 6 shadowmaps (one per face), we must use the first face one, the shader
// knows how to find the entry for other faces (they're guaranteed to be sequential).
if (shadowMap.getFace() == 0) {
shadowInfo[lightIndex].castsShadows = true; // FIXME: is that set correctly?
shadowInfo[lightIndex].index = shadowMap.getShadowIndex();
}
}
}
// screen-space contact shadows for point/spotlights
auto& lcm = engine.getLightManager();
auto *pLightInstances = lightData.data<FScene::LIGHT_INSTANCE>();
for (size_t i = 0, c = lightData.size(); i < c; i++) {
// screen-space contact shadows
LightManager::ShadowOptions const& shadowOptions = lcm.getShadowOptions(pLightInstances[i]);
if (shadowOptions.screenSpaceContactShadows) {
shadowTechnique |= ShadowTechnique::SCREEN_SPACE;
shadowInfo[i].contactShadows = true;
// TODO: distance/steps are always taken from the directional light currently
}
}
return shadowTechnique;
}
void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view,
FScene::LightSoa const&) noexcept {
// Lay out the shadow maps. For now, we take the largest requested dimension and allocate a
// texture of that size. Each cascade / shadow map gets its own layer in the array texture.
// The directional shadow cascades start on layer 0, followed by spotlights.
uint8_t layer = 0;
uint32_t maxDimension = 0;
bool elvsm = false;
for (ShadowMap& 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;
shadowMap.setLayer(layer++);
}
for (ShadowMap& shadowMap : getSpotShadowMaps()) {
auto const& options = shadowMap.getShadowOptions();
maxDimension = std::max(maxDimension, options->mapSize);
elvsm = elvsm || options->vsm.elvsm;
shadowMap.setLayer(layer++);
}
const uint8_t layersNeeded = layer;
// 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);
uint8_t msaaSamples = vsmShadowOptions.msaaSamples;
if (engine.getDriverApi().isWorkaroundNeeded(Workaround::DISABLE_BLIT_INTO_TEXTURE_ARRAY)) {
msaaSamples = 1;
}
TextureFormat format = TextureFormat::DEPTH16;
if (view.hasVSM()) {
if (vsmShadowOptions.highPrecision) {
if (elvsm) {
format = TextureFormat::RGBA32F;
} else {
format = TextureFormat::RG32F;
}
} else {
if (elvsm) {
format = TextureFormat::RGBA16F;
} else {
format = TextureFormat::RG16F;
}
}
}
mSoftShadowOptions = view.getSoftShadowOptions();
uint8_t mipLevels = 1u;
if (useMipmapping) {
// Limit the lowest mipmap level to 256x256.
// This avoids artifacts on high derivative tangent surfaces.
int const lowMipmapLevel = 7; // log2(256) - 1
mipLevels = std::max(1, FTexture::maxLevelCount(maxDimension) - lowMipmapLevel);
}
// publish the debugging data
engine.debug.shadowmap.display_shadow_texture_layer_count = layersNeeded;
engine.debug.shadowmap.display_shadow_texture_level_count = mipLevels;
mTextureAtlasRequirements = {
(uint16_t)maxDimension,
layersNeeded,
mipLevels,
msaaSamples,
format
};
}
ShadowMapManager::CascadeSplits::CascadeSplits(Params const& params) noexcept
: mSplitCount(params.cascadeCount + 1) {
for (size_t s = 0; s < mSplitCount; s++) {
mSplitsWs[s] = params.near + (params.far - params.near) * params.splitPositions[s];
mSplitsCs[s] = mat4f::project(params.proj, float3(0.0f, 0.0f, mSplitsWs[s])).z;
}
}
} // namespace filament