1191 lines
53 KiB
C++
1191 lines
53 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 "AtlasAllocator.h"
|
|
#include "RenderPass.h"
|
|
#include "ShadowMap.h"
|
|
|
|
#include <filament/Frustum.h>
|
|
#include <filament/LightManager.h>
|
|
#include <filament/Options.h>
|
|
|
|
#include <iterator>
|
|
#include <private/filament/EngineEnums.h>
|
|
|
|
#include "components/RenderableManager.h"
|
|
|
|
#include "details/Camera.h"
|
|
#include "details/DebugRegistry.h"
|
|
#include "details/Texture.h"
|
|
#include "details/View.h"
|
|
|
|
#include "fg/FrameGraph.h"
|
|
#include "fg/FrameGraphId.h"
|
|
#include "fg/FrameGraphRenderPass.h"
|
|
#include "fg/FrameGraphTexture.h"
|
|
|
|
#include <backend/DriverApiForward.h>
|
|
#include <backend/DriverEnums.h>
|
|
|
|
#include <utils/compiler.h>
|
|
#include <utils/debug.h>
|
|
#include <utils/FixedCapacityVector.h>
|
|
#include <utils/BitmaskEnum.h>
|
|
#include <utils/Range.h>
|
|
#include <utils/Slice.h>
|
|
|
|
#include <math/half.h>
|
|
#include <math/mat4.h>
|
|
#include <math/vec4.h>
|
|
#include <math/scalar.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <new>
|
|
#include <memory>
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
|
|
namespace filament {
|
|
|
|
using namespace backend;
|
|
using namespace math;
|
|
|
|
ShadowMapManager::ShadowMapManager(FEngine& engine)
|
|
: mIsDepthClampSupported(engine.getDriverApi().isDepthClampSupported()) {
|
|
FDebugRegistry& debugRegistry = engine.getDebugRegistry();
|
|
debugRegistry.registerProperty("d.shadowmap.visualize_cascades",
|
|
&engine.debug.shadowmap.visualize_cascades);
|
|
debugRegistry.registerProperty("d.shadowmap.disable_light_frustum_align",
|
|
&engine.debug.shadowmap.disable_light_frustum_align);
|
|
debugRegistry.registerProperty("d.shadowmap.depth_clamp",
|
|
&engine.debug.shadowmap.depth_clamp);
|
|
|
|
mFeatureShadowAllocator = engine.features.engine.shadows.use_shadow_atlas;
|
|
}
|
|
|
|
ShadowMapManager::~ShadowMapManager() {
|
|
// destroy the ShadowMap array in-place
|
|
if (UTILS_UNLIKELY(mInitialized)) {
|
|
UTILS_NOUNROLL
|
|
for (auto& entry: mShadowMapCache) {
|
|
std::destroy_at(std::launder(reinterpret_cast<ShadowMap*>(&entry)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ShadowMapManager::createIfNeeded(FEngine& engine,
|
|
std::unique_ptr<ShadowMapManager>& inOutShadowMapManager) {
|
|
if (UTILS_UNLIKELY(!inOutShadowMapManager)) {
|
|
inOutShadowMapManager.reset(new ShadowMapManager(engine));
|
|
}
|
|
}
|
|
|
|
void ShadowMapManager::terminate(FEngine& engine,
|
|
std::unique_ptr<ShadowMapManager>& shadowMapManager) {
|
|
if (shadowMapManager) {
|
|
shadowMapManager->terminate(engine);
|
|
}
|
|
}
|
|
|
|
size_t ShadowMapManager::getMaxShadowMapCount() const noexcept {
|
|
return mFeatureShadowAllocator ? CONFIG_MAX_SHADOWMAPS : CONFIG_MAX_SHADOW_LAYERS;
|
|
}
|
|
|
|
void ShadowMapManager::terminate(FEngine& engine) {
|
|
if (UTILS_UNLIKELY(mInitialized)) {
|
|
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(
|
|
Builder const& builder,
|
|
FEngine& engine, FView& view,
|
|
CameraInfo const& cameraInfo,
|
|
FScene::RenderableSoa& renderableData, FScene::LightSoa const& lightData) noexcept {
|
|
|
|
if (!builder.mDirectionalShadowMapCount && !builder.mSpotShadowMapCount) {
|
|
// no shadows were recorder
|
|
return ShadowTechnique::NONE;
|
|
}
|
|
|
|
// initialize the shadowmap array the first time
|
|
if (UTILS_UNLIKELY(!mInitialized)) {
|
|
mInitialized = true;
|
|
// initialize our ShadowMap array in-place
|
|
mShadowUbh = engine.getDriverApi().createBufferObject(mShadowUb.getSize(),
|
|
BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC);
|
|
UTILS_NOUNROLL
|
|
for (auto& entry: mShadowMapCache) {
|
|
new(&entry) ShadowMap(engine);
|
|
}
|
|
}
|
|
|
|
mDirectionalShadowMapCount = builder.mDirectionalShadowMapCount;
|
|
mSpotShadowMapCount = builder.mSpotShadowMapCount;
|
|
|
|
for (auto const& entry : builder.mShadowMaps) {
|
|
auto& shadowMap = getShadowMap(entry.shadowIndex);
|
|
shadowMap.initialize(
|
|
entry.lightIndex,
|
|
entry.shadowType,
|
|
entry.shadowIndex,
|
|
entry.face,
|
|
entry.options);
|
|
}
|
|
|
|
ShadowTechnique shadowTechnique = {};
|
|
|
|
calculateTextureRequirements(engine, view, lightData);
|
|
|
|
// Compute scene-dependent values shared across all shadow maps
|
|
ShadowMap::SceneInfo const info{ *view.getScene(), view.getVisibleLayers() };
|
|
|
|
shadowTechnique |= updateCascadeShadowMaps(
|
|
engine, view, cameraInfo, renderableData, lightData, info);
|
|
|
|
shadowTechnique |= updateSpotShadowMaps(
|
|
engine, lightData);
|
|
|
|
mSceneInfo = info;
|
|
|
|
return shadowTechnique;
|
|
}
|
|
|
|
ShadowMapManager::Builder& ShadowMapManager::Builder::directionalShadowMap(size_t const lightIndex,
|
|
LightManager::ShadowOptions const* options) noexcept {
|
|
assert_invariant(options->shadowCascades <= CONFIG_MAX_SHADOW_CASCADES);
|
|
// this updates getCascadedShadowMap()
|
|
mDirectionalShadowMapCount = options->shadowCascades;
|
|
for (size_t c = 0; c < options->shadowCascades; c++) {
|
|
mShadowMaps.push_back({
|
|
.lightIndex = lightIndex,
|
|
.shadowType = ShadowType::DIRECTIONAL,
|
|
.shadowIndex = uint8_t(c),
|
|
.face = 0,
|
|
.options = options });
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
ShadowMapManager::Builder& ShadowMapManager::Builder::shadowMap(size_t const lightIndex, bool const 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);
|
|
mShadowMaps.push_back({
|
|
.lightIndex = lightIndex,
|
|
.shadowType = ShadowType::SPOT,
|
|
.shadowIndex = uint8_t(i),
|
|
.face = 0,
|
|
.options = 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);
|
|
mShadowMaps.push_back({
|
|
.lightIndex = lightIndex,
|
|
.shadowType = ShadowType::POINT,
|
|
.shadowIndex = uint8_t(i),
|
|
.face = uint8_t(face),
|
|
.options = options });
|
|
}
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
FrameGraphId<FrameGraphTexture> ShadowMapManager::render(FEngine& engine, FrameGraph& fg,
|
|
RenderPassBuilder const& passBuilder,
|
|
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 { // 112 bytes
|
|
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(getMaxShadowMapCount());
|
|
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
|
|
// if the view has shadowing enabled, the SRE variant could be used, so we must
|
|
// generate the shadow maps
|
|
if (view.needsDirectionalShadowMaps()) {
|
|
auto const directionalShadowCastersRange = view.getVisibleDirectionalShadowCasters();
|
|
for (auto& shadowMap : getCascadedShadowMap()) {
|
|
// For the directional light, we already know if it has visible shadows.
|
|
// if hasVisibleShadows() is false, we're guaranteed the shader won't
|
|
// sample this shadow map (see PerViewUib::cascades and
|
|
// ShadowMapManager::updateCascadeShadowMaps)
|
|
if (shadowMap.hasVisibleShadows()) {
|
|
passList.push_back({
|
|
{}, &shadowMap, directionalShadowCastersRange,
|
|
VISIBLE_DIR_SHADOW_RENDERABLE });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Point lights and Spotlight shadow maps
|
|
// if the view has shadowing enabled, the SRE variant could be used, so we must
|
|
// generate the shadow maps
|
|
if (view.needsPointShadowMaps()) {
|
|
auto const spotShadowCastersRange = view.getVisibleSpotShadowCasters();
|
|
for (auto& shadowMap : getSpotShadowMaps()) {
|
|
assert_invariant(!shadowMap.isDirectionalShadow());
|
|
|
|
switch (shadowMap.getShadowType()) {
|
|
case ShadowType::DIRECTIONAL:
|
|
// we should never be here
|
|
break;
|
|
case ShadowType::SPOT:
|
|
prepareSpotShadowMap(shadowMap, engine, view, mainCameraInfo,
|
|
scene->getLightData(), mSceneInfo);
|
|
break;
|
|
case ShadowType::POINT:
|
|
preparePointShadowMap(shadowMap, engine, view, mainCameraInfo,
|
|
scene->getLightData());
|
|
break;
|
|
}
|
|
|
|
// For spot/point lights, shadowMap.hasVisibleShadows() doesn't guarantee
|
|
// the shader won't access the shadow map, so we must generate it.
|
|
passList.push_back({
|
|
{}, &shadowMap, spotShadowCastersRange,
|
|
VISIBLE_DYN_SHADOW_RENDERABLE });
|
|
}
|
|
}
|
|
|
|
assert_invariant(mFeatureShadowAllocator ||
|
|
passList.size() <= textureRequirements.layers);
|
|
|
|
if (mFeatureShadowAllocator) {
|
|
// sort shadow passes by layer so that we can update all the shadow maps of
|
|
// a layer in one render pass.
|
|
std::sort(passList.begin(), passList.end(), [](
|
|
PrepareShadowPassData::ShadowPass const& lhs,
|
|
PrepareShadowPassData::ShadowPass const& rhs) {
|
|
return lhs.shadowMap->getLayer() < rhs.shadowMap->getLayer();
|
|
});
|
|
}
|
|
|
|
// 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();
|
|
},
|
|
[=, passBuilder = passBuilder,
|
|
&engine = const_cast<FEngine /*const*/ &>(engine), // FIXME: we want this const
|
|
&view = const_cast<FView const&>(view)]
|
|
(FrameGraphResources const&, auto const& data, DriverApi& driver) mutable {
|
|
|
|
// 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 const& shadowMap = *entry.shadowMap;
|
|
|
|
// 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. Maybe this could
|
|
// be done with indirect draw calls.
|
|
|
|
// Note: the output of culling below is stored in scene->getRenderableData()
|
|
|
|
switch (shadowMap.getShadowType()) {
|
|
case ShadowType::DIRECTIONAL:
|
|
// we should never be here
|
|
break;
|
|
case ShadowType::SPOT:
|
|
if (shadowMap.hasVisibleShadows()) {
|
|
ShadowMapManager::cullSpotShadowMap(shadowMap, engine, view,
|
|
scene->getRenderableData(), entry.range,
|
|
scene->getLightData());
|
|
}
|
|
break;
|
|
case ShadowType::POINT:
|
|
if (shadowMap.hasVisibleShadows()) {
|
|
ShadowMapManager::cullPointShadowMap(shadowMap, view,
|
|
scene->getRenderableData(), entry.range,
|
|
scene->getLightData());
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 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::prepareMaterialGlobals(transaction, view.getMaterialGlobals());
|
|
ShadowMap::prepareShadowMapping(transaction,
|
|
vsmShadowOptions.highPrecision);
|
|
shadowMap.commit(transaction, engine, driver);
|
|
|
|
// updatePrimitivesLod must be run before RenderPass::appendCommands.
|
|
FView::updatePrimitivesLod(scene->getRenderableData(),
|
|
engine, cameraInfo, entry.range);
|
|
|
|
// generate and sort the commands for rendering the shadow map
|
|
|
|
RenderPass::RenderFlags renderPassFlags{};
|
|
|
|
bool const canUseDepthClamp =
|
|
shadowMap.getShadowType() == ShadowType::DIRECTIONAL &&
|
|
!view.hasVSM() &&
|
|
mIsDepthClampSupported &&
|
|
engine.debug.shadowmap.depth_clamp;
|
|
|
|
if (canUseDepthClamp) {
|
|
renderPassFlags |= RenderPass::HAS_DEPTH_CLAMP;
|
|
}
|
|
|
|
RenderPass const pass = passBuilder
|
|
.renderFlags(RenderPass::HAS_DEPTH_CLAMP, renderPassFlags)
|
|
.camera(cameraInfo.getPosition(), cameraInfo.getForwardVector())
|
|
.visibilityMask(entry.visibilityMask)
|
|
.geometry(scene->getRenderableData(), entry.range)
|
|
.commandTypeFlags(RenderPass::CommandTypeFlags::SHADOW)
|
|
.build(engine, driver);
|
|
|
|
entry.executor = pass.getExecutor();
|
|
|
|
if (!view.hasVSM()) {
|
|
auto const* options = shadowMap.getShadowOptions();
|
|
PolygonOffset const 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;
|
|
auto first = passList.begin();
|
|
while (first != passList.end()) {
|
|
auto const& entry = *first;
|
|
|
|
const uint8_t layer = entry.shadowMap->getLayer();
|
|
const auto* options = entry.shadowMap->getShadowOptions();
|
|
const auto msaaSamples = textureRequirements.msaaSamples;
|
|
const bool blur = entry.shadowMap->hasVisibleShadows() &&
|
|
view.hasVSM() && options->vsm.blurWidth > 0.0f;
|
|
|
|
auto last = first;
|
|
// loop over each shadow pass to find its layer range
|
|
while (last != passList.end() && last->shadowMap->getLayer() == layer) {
|
|
++last;
|
|
}
|
|
|
|
assert_invariant(mFeatureShadowAllocator ||
|
|
std::distance(first, last) == 1);
|
|
|
|
// And render all shadow pass of a given layer as a single render pass
|
|
auto& shadowPass = fg.addPass<ShadowPassData>("Shadow Pass",
|
|
[&](FrameGraph::Builder& builder, auto& data) {
|
|
|
|
FrameGraphRenderPass::Descriptor renderTargetDesc{};
|
|
|
|
data.output = builder.createSubresource(prepareShadowPass->shadows,
|
|
"Shadowmap Layer", { .layer = layer });
|
|
|
|
if (UTILS_UNLIKELY(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,
|
|
});
|
|
|
|
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 (UTILS_UNLIKELY(blur)) {
|
|
// Temporary (resolved) texture used to render the shadowmap when blurring
|
|
// is needed -- it'll be used as the source of the blur.
|
|
data.tempBlurSrc = builder.createTexture("Temporary Shadowmap", {
|
|
.width = textureRequirements.size,
|
|
.height = textureRequirements.size,
|
|
.format = textureRequirements.format
|
|
});
|
|
|
|
data.tempBlurSrc = builder.write(data.tempBlurSrc,
|
|
FrameGraphTexture::Usage::COLOR_ATTACHMENT);
|
|
|
|
data.rt = builder.declareRenderPass("Temp Shadow RT", {
|
|
.attachments = {
|
|
.color = { data.tempBlurSrc },
|
|
.depth = depth },
|
|
.clearColor = 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](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);
|
|
|
|
driver.beginRenderPass(rt.target, rt.params);
|
|
|
|
for (auto curr = first; curr != last; curr++) {
|
|
// if we know there are no visible shadows, we can skip rendering, but
|
|
// we need the render-pass to clear/initialize the shadow-map
|
|
// Note: this is always true for directional/cascade shadows.
|
|
if (curr->shadowMap->hasVisibleShadows()) {
|
|
curr->shadowMap->bind(driver);
|
|
curr->executor.overrideScissor(curr->shadowMap->getScissor());
|
|
curr->executor.execute(engine, driver);
|
|
}
|
|
}
|
|
|
|
driver.endRenderPass();
|
|
});
|
|
|
|
first = last;
|
|
|
|
// now emit the blurring passes if needed
|
|
if (UTILS_UNLIKELY(blur)) {
|
|
auto& ppm = engine.getPostProcessManager();
|
|
|
|
// FIXME: this `options` is for the first shadowmap in the list, but it applies to
|
|
// the whole layer. Blurring should happen per shadowmap, not for the whole
|
|
// layer.
|
|
|
|
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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
if (UTILS_UNLIKELY(params.options.shadowFar > 0.0f)) {
|
|
cameraInfo.zf = params.options.shadowFar;
|
|
updateNearFarPlanes(&cameraInfo.cullingProjection, cameraInfo.zn, cameraInfo.zf);
|
|
}
|
|
|
|
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 ||
|
|
engine.getBackend() == Backend::WEBGPU,
|
|
.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);
|
|
|
|
// we always do culling without depth clamp, because objects behind the camera
|
|
// must be rendered regardless
|
|
shadowMap.updateDirectional(engine,
|
|
lightData, 0, cameraInfo, shadowMapInfo, sceneInfo, false);
|
|
|
|
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) {
|
|
uint32_t const 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({
|
|
.near = -cameraInfo.zn,
|
|
.far = -cameraInfo.zf,
|
|
.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.begin() + 1, splits.end(), &wsSplitPositionUniform[0]);
|
|
|
|
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);
|
|
|
|
// update cameraInfo culling projection for the cascade
|
|
float const* nearFarPlanes = splits.begin();
|
|
cameraInfo.zn = -nearFarPlanes[i];
|
|
cameraInfo.zf = -nearFarPlanes[i + 1];
|
|
updateNearFarPlanes(&cameraInfo.cullingProjection, cameraInfo.zn, cameraInfo.zf);
|
|
|
|
bool const canUseDepthClamp =
|
|
!view.hasVSM() &&
|
|
mIsDepthClampSupported &&
|
|
engine.debug.shadowmap.depth_clamp;
|
|
|
|
auto shaderParameters = shadowMap.updateDirectional(engine,
|
|
lightData, 0, cameraInfo, shadowMapInfo, sceneInfo,
|
|
canUseDepthClamp);
|
|
|
|
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 const 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::LightSoa const& lightData, ShadowMap::SceneInfo const& sceneInfo) noexcept {
|
|
|
|
const size_t lightIndex = shadowMap.getLightIndex();
|
|
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
|
|
|
|
// 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 ||
|
|
engine.getBackend() == Backend::WEBGPU,
|
|
.vsm = view.hasVSM()
|
|
};
|
|
|
|
auto shaderParameters = shadowMap.updateSpot(engine,
|
|
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::cullSpotShadowMap(ShadowMap const& shadowMap,
|
|
FEngine const& engine, FView const& view,
|
|
FScene::RenderableSoa& renderableData, utils::Range<uint32_t> range,
|
|
FScene::LightSoa const& lightData) noexcept {
|
|
auto& lcm = engine.getLightManager();
|
|
|
|
const size_t lightIndex = shadowMap.getLightIndex();
|
|
const FLightManager::Instance li = lightData.elementAt<FScene::LIGHT_INSTANCE>(lightIndex);
|
|
|
|
// 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 = 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());
|
|
}
|
|
|
|
void ShadowMapManager::preparePointShadowMap(ShadowMap& shadowMap,
|
|
FEngine& engine, FView& view, CameraInfo const& mainCameraInfo,
|
|
FScene::LightSoa const& lightData) const noexcept {
|
|
|
|
const uint8_t face = shadowMap.getFace();
|
|
const size_t lightIndex = shadowMap.getLightIndex();
|
|
FLightManager::ShadowOptions const* const options = shadowMap.getShadowOptions();
|
|
|
|
// 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 ||
|
|
engine.getBackend() == Backend::WEBGPU,
|
|
.vsm = view.hasVSM()
|
|
};
|
|
|
|
auto shaderParameters = shadowMap.updatePoint(engine, 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;
|
|
}
|
|
}
|
|
|
|
void ShadowMapManager::cullPointShadowMap(ShadowMap const& shadowMap, FView const& view,
|
|
FScene::RenderableSoa& renderableData, utils::Range<uint32_t> const range,
|
|
FScene::LightSoa const& lightData) noexcept {
|
|
|
|
uint8_t const face = shadowMap.getFace();
|
|
size_t const lightIndex = shadowMap.getLightIndex();
|
|
|
|
// 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
|
|
auto const position = lightData.elementAt<FScene::POSITION_RADIUS>(lightIndex).xyz;
|
|
auto const radius = lightData.elementAt<FScene::POSITION_RADIUS>(lightIndex).w;
|
|
|
|
// compute shadow map frustum for culling
|
|
mat4f const Mv = ShadowMap::getPointLightViewMatrix(TextureCubemapFace(face), position);
|
|
mat4f const Mp = mat4f::perspective(90.0f, 1.0f, 0.01f, radius);
|
|
Frustum const frustum{ 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());
|
|
}
|
|
|
|
ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps(FEngine& engine,
|
|
FScene::LightSoa const& lightData) const 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 {
|
|
|
|
uint32_t maxDimension = 0;
|
|
bool elvsm = false;
|
|
|
|
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;
|
|
}
|
|
|
|
for (ShadowMap const& shadowMap : getSpotShadowMaps()) {
|
|
auto const& options = shadowMap.getShadowOptions();
|
|
maxDimension = std::max(maxDimension, options->mapSize);
|
|
elvsm = elvsm || options->vsm.elvsm;
|
|
}
|
|
|
|
uint8_t layersNeeded = 0;
|
|
|
|
std::function const allocateFromAtlas =
|
|
[&layersNeeded, allocator = AtlasAllocator{ maxDimension }](
|
|
ShadowMap* pShadowMap) mutable {
|
|
// Allocate shadowmap from our Atlas Allocator
|
|
auto const& options = pShadowMap->getShadowOptions();
|
|
auto [layer, pos] = allocator.allocate(options->mapSize);
|
|
assert_invariant(layer >= 0);
|
|
assert_invariant(!pos.empty());
|
|
pShadowMap->setAllocation(layer, pos);
|
|
layersNeeded = std::max(uint8_t(layer + 1), layersNeeded);
|
|
};
|
|
|
|
std::function const allocateFromTextureArray =
|
|
[&layersNeeded, layer = 0](ShadowMap* pShadowMap) mutable {
|
|
// Layout 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.
|
|
pShadowMap->setAllocation(layer, {});
|
|
layersNeeded = ++layer;
|
|
};
|
|
|
|
auto& allocateShadowmapTexture = mFeatureShadowAllocator ?
|
|
allocateFromAtlas : allocateFromTextureArray;
|
|
|
|
for (ShadowMap& shadowMap : getCascadedShadowMap()) {
|
|
allocateShadowmapTexture(&shadowMap);
|
|
}
|
|
for (ShadowMap& shadowMap : getSpotShadowMaps()) {
|
|
allocateShadowmapTexture(&shadowMap);
|
|
}
|
|
|
|
// 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) {
|
|
format = elvsm ? TextureFormat::RGBA32F : TextureFormat::RG32F;
|
|
} else {
|
|
format = elvsm ? TextureFormat::RGBA16F : 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.
|
|
constexpr int 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];
|
|
}
|
|
}
|
|
|
|
void ShadowMapManager::updateNearFarPlanes(mat4f* projection,
|
|
float nearDistance, float farDistance) noexcept {
|
|
float const n = nearDistance;
|
|
float const f = farDistance;
|
|
|
|
/*
|
|
* 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 = *projection;
|
|
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;
|
|
}
|
|
|
|
utils::FixedCapacityVector<Camera const*>
|
|
ShadowMapManager::getDirectionalShadowCameras() const noexcept {
|
|
if (!mInitialized) return {};
|
|
auto const csm = getCascadedShadowMap();
|
|
auto result = utils::FixedCapacityVector<Camera const*>::with_capacity(csm.size());
|
|
for (ShadowMap const& sm : csm) {
|
|
result.push_back(sm.hasVisibleShadows() ? sm.getDebugCamera() : nullptr);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace filament
|