/* * 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 #include #include 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(&entry))); } } void ShadowMapManager::terminate(FEngine& engine) { DriverApi& driver = engine.getDriverApi(); driver.destroyBufferObject(mShadowUbh); UTILS_NOUNROLL for (auto& entry : mShadowMapCache) { std::launder(reinterpret_cast(&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 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 ShadowMapManager::render(FEngine& engine, FrameGraph& fg, RenderPass const& pass, FView& view, CameraInfo const& mainCameraInfo, float4 const& userTime) noexcept { const float moment2 = std::numeric_limits::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 range; FScene::VisibleMaskType visibilityMask; }; // the actual shadow map atlas (currently a 2D texture array) FrameGraphId shadows; // a RenderPass per shadow map utils::FixedCapacityVector passList; }; VsmShadowOptions const& vsmShadowOptions = view.getVsmShadowOptions(); auto& prepareShadowPass = fg.addPass("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 tempBlurSrc{}; // temporary shadowmap when blurring FrameGraphId 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("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(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 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(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 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::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 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(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(lightIndex).xyz; const auto direction = lightData.elementAt(lightIndex); const auto radius = lightData.elementAt(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(); float3 const* worldAABBExtent = renderableData.data(); FScene::VisibleMaskType* visibleArray = renderableData.data(); 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(); auto const* visibility = renderableData.data(); 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 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(lightIndex).xyz; const auto radius = lightData.elementAt(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(); float3 const* worldAABBExtent = renderableData.data(); FScene::VisibleMaskType* visibleArray = renderableData.data(); 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(); auto const* visibility = renderableData.data(); 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( lightData.data()); ShadowTechnique shadowTechnique{}; utils::Slice 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(); 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