diff --git a/filament/include/filament/Box.h b/filament/include/filament/Box.h index 36f19924a1..88d789859f 100644 --- a/filament/include/filament/Box.h +++ b/filament/include/filament/Box.h @@ -170,6 +170,8 @@ struct UTILS_PUBLIC Aabb { value_type const* data() const { return vertices; } value_type * data() { return vertices; } size_t size() const { return 8; } + value_type const& operator[](size_t i) const noexcept { return vertices[i]; } + value_type& operator[](size_t i) noexcept { return vertices[i]; } value_type vertices[8]; }; diff --git a/filament/src/ShadowMap.cpp b/filament/src/ShadowMap.cpp index a709d6c44e..e6cc8eadea 100644 --- a/filament/src/ShadowMap.cpp +++ b/filament/src/ShadowMap.cpp @@ -115,145 +115,19 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, ShadowMapInfo const& shadowMapInfo, SceneInfo const& sceneInfo) noexcept { - // Note: we keep the polygon offset even with VSM as it seems to help. - auto& lcm = engine.getLightManager(); + FLightManager const& lcm = engine.getLightManager(); FLightManager::Instance const li = lightData.elementAt(index); - FLightManager::ShadowParams params = lcm.getShadowParams(li); - -#ifndef NDEBUG - // LISPSM debugging for directional light (works because we only have one) - const float dz = camera.zf - camera.zn; - float& dzn = engine.debug.shadowmap.dzn; - float& dzf = engine.debug.shadowmap.dzf; - if (dzn < 0) dzn = std::max(0.0f, params.options.shadowNearHint - camera.zn) / dz; - else params.options.shadowNearHint = dzn * dz - camera.zn; - if (dzf > 0) dzf =-std::max(0.0f, camera.zf - params.options.shadowFarHint) / dz; - else params.options.shadowFarHint = dzf * dz + camera.zf; -#endif - - const auto direction = params.options.transform * lightData.elementAt(index); - - /* - * Compute the light's model matrix - */ - - // 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); - - const Aabb wsShadowCastersVolume = sceneInfo.wsShadowCastersVolume; - const Aabb wsShadowReceiversVolume = sceneInfo.wsShadowReceiversVolume; - if (wsShadowCastersVolume.isEmpty() || wsShadowReceiversVolume.isEmpty()) { - mHasVisibleShadows = false; - return {}; - } - - // view frustum vertices in world-space - float3 wsViewFrustumVertices[8]; - const mat4f worldToClipMatrix = camera.cullingProjection * camera.view; - const Frustum wsFrustum(worldToClipMatrix); - computeFrustumCorners(wsViewFrustumVertices, inverse(worldToClipMatrix), sceneInfo.csNearFar); - - // we use aligned_storage<> here to avoid the default initialization of std::array<> - std::aligned_storage::type localStorage; // NOLINT(cppcoreguidelines-pro-type-member-init) - FrustumBoxIntersection& wsClippedShadowReceiverVolume{ reinterpret_cast(localStorage) }; - - // compute the intersection of the shadow receivers' volume with the view volume - // in world space. This returns a set of points on the convex-hull of the intersection. - size_t const vertexCount = intersectFrustumWithBox(wsClippedShadowReceiverVolume, - wsFrustum, wsViewFrustumVertices, wsShadowReceiversVolume); - - if (UTILS_UNLIKELY(vertexCount < 4)) { - mHasVisibleShadows = false; - return {}; - } - - /* - * compute scene zmax (i.e. Near plane) and zmin (i.e. Far plane) in light space. - * (near/far correspond to max/min because the light looks down the -z axis). - * - The Near plane is set to the shadow casters max z (i.e. closest to the light) - * - The Far plane is set to the closest of the farthest shadow casters and receivers - * i.e.: shadow casters behind the last receivers can't cast any shadows - * - * If "depth clamp" is supported, we can further tighten the near plane to the - * shadow receiver. - * - * Note: L has no influence here, since we're only interested in z values - * (L is a rotation around z) - */ - - Aabb lsLightFrustumBounds; - if constexpr (!USE_DEPTH_CLAMP) { - // near plane from shadow caster volume - lsLightFrustumBounds.max.z = sceneInfo.lsNearFar[0]; - } - for (size_t i = 0; i < vertexCount; ++i) { - // far: figure out the farthest shadow receivers - float3 const v = mat4f::project(MvAtOrigin, wsClippedShadowReceiverVolume[i]); - lsLightFrustumBounds.min.z = std::min(lsLightFrustumBounds.min.z, v.z); - if constexpr (USE_DEPTH_CLAMP) { - // tighten the shadow receiver volume further - lsLightFrustumBounds.max.z = std::max(lsLightFrustumBounds.max.z, v.z); - } - } - if (engine.debug.shadowmap.far_uses_shadowcasters) { - // far: closest of the farthest shadow casters and receivers - lsLightFrustumBounds.min.z = std::max(lsLightFrustumBounds.min.z, sceneInfo.lsNearFar[1]); - } - - // Now that we know the znear (-lsLightFrustumBounds.max.z), adjust the light's position such - // that znear = 0, this is only needed for VSM, but doesn't hurt PCF. - const mat4f Mv = getDirectionalLightViewMatrix(direction, direction * -lsLightFrustumBounds.max.z); - - // near / far planes are specified relative to the direction the eye is looking at - // i.e. the -z axis (see: ortho) - const float znear = 0.0f; - const float zfar = lsLightFrustumBounds.max.z - lsLightFrustumBounds.min.z; - - // if znear >= zfar, it means we don't have any shadow caster in front of a shadow receiver - if (UTILS_UNLIKELY(znear >= zfar)) { - mHasVisibleShadows = false; - return {}; - } - - float4 viewVolumeBoundingSphere = {}; - if (UTILS_UNLIKELY(params.options.stable)) { - // In stable mode, the light frustum size must be fixed, so we can choose either the - // whole view frustum, or the whole scene bounding volume. We simply pick whichever - // is smaller. - - // in stable mode we simply take the shadow receivers volume - const float4 shadowReceiverVolumeBoundingSphere = computeBoundingSphere( - wsShadowReceiversVolume.getCorners().data(), 8); - - // in stable mode we simply take the view volume bounding sphere, but we calculate it - // in view space, so that it's perfectly stable. - float3 vertices[8]; - computeFrustumCorners(vertices, inverse(camera.cullingProjection), sceneInfo.csNearFar); - viewVolumeBoundingSphere = computeBoundingSphere(vertices, 8); - - if (shadowReceiverVolumeBoundingSphere.w < viewVolumeBoundingSphere.w) { - - // When using the shadowReceiver volume, we don't have to use its enclosing sphere - // because (we assume) the scene volume doesn't change. Seen from the light it only - // changes when the light moves or rotates, and it is acceptable in that case to have - // non "stable" shadows (the shadow will never be stable when the light moves). - // - // On the other hand, when using the view volume, we must use a sphere because otherwise - // its projection's bounds in light space change with the camera, leading to unstable - // shadows with camera movement. - - viewVolumeBoundingSphere.w = 0; - std::copy_n(wsShadowReceiversVolume.getCorners().data(), 8, - wsClippedShadowReceiverVolume.data()); - } - } - - mHasVisibleShadows = true; + FLightManager::ShadowParams const params = lcm.getShadowParams(li); // We can't use LISPSM in stable mode - const bool useLispsm = params.options.lispsm && !params.options.stable; + const auto direction = params.options.transform * lightData.elementAt(index); + + auto [Mv, znear, zfar, lsClippedShadowVolume, vertexCount, visibleShadows] = + computeDirectionalShadowBounds(engine, direction, params, camera, sceneInfo); + + if (UTILS_UNLIKELY(!visibleShadows)) { + return {}; + } /* * Compute the light's projection matrix @@ -262,129 +136,48 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, // The light's projection, ortho for directional lights, perspective otherwise const mat4f Mp = directionalLightFrustum(znear, zfar); - - const mat4f MpMv(Mp * Mv); + mat4f LMp = Mp; + mat4f LMpMv = Mp * Mv; /* * Compute warping (optional, improve quality) */ - mat4f LMpMv = MpMv; - - // Compute the LiSPSM warping - mat4f W, Wp; - mat4f L; // Rotation matrix in light space + mat4f W, Wp, L; + const bool useLispsm = params.options.lispsm && !params.options.stable; if (useLispsm) { // Orient the shadow map in the direction of the view vector by constructing a - // rotation matrix in light space around the z-axis, that aligns the y-axis with the camera's - // forward vector (V) -- this gives the wrap direction, vp, for LiSPSM. - const float3 wsCameraFwd = camera.getForwardVector(); - const float3 lsCameraFwd = Mv.upperLeft() * wsCameraFwd; - // If the light and view vector are parallel, this rotation becomes - // meaningless. Just use identity. - // (LdotV == (Mv*V).z, because L = {0,0,1} in light-space) - if (UTILS_LIKELY(std::abs(lsCameraFwd.z) < 0.9997f)) { // this is |dot(L, V)| - const float3 vp{ normalize(lsCameraFwd.xy), 0 }; // wrap direction in light-space - L[0].xyz = cross(vp, float3{ 0, 0, 1 }); - L[1].xyz = vp; - L[2].xyz = { 0, 0, 1 }; - L = transpose(L); - } - - LMpMv = L * MpMv; - - W = applyLISPSM(Wp, camera, params, LMpMv, - wsClippedShadowReceiverVolume, vertexCount, direction); + const float3 lsCameraFwd = Mv.upperLeft() * camera.getForwardVector(); + L = computeLightRotation(lsCameraFwd); + LMp = L * Mp; + LMpMv = LMp * Mv; + W = applyLISPSM(Wp, camera, params, + LMp, Mv, LMpMv, lsClippedShadowVolume, vertexCount, direction); } /* * Compute focusing matrix (optional, greatly improves quality) */ - // construct the warped light-space - const mat4f WLMpMv = W * LMpMv; + const mat4f WLMp = W * LMp; // 2) Now we find the x-y bounds of our convex-hull (view volume & shadow receivers) // in light space, so we can "focus" the shadow map to the interesting area. // This is the most important step to increase the quality of the shadow map. // // In LiPSM mode, we're using the warped space here. - - float2 s, o; - if (params.options.stable) { - if (viewVolumeBoundingSphere.w > 0) { - s = 1.0f / viewVolumeBoundingSphere.w; - o = mat4f::project(LMpMv * camera.model, viewVolumeBoundingSphere.xyz).xy; - } else { - Aabb const bounds = compute2DBounds(LMpMv, - wsClippedShadowReceiverVolume.data(), vertexCount); - if (UTILS_UNLIKELY((bounds.min.x >= bounds.max.x) || (bounds.min.y >= bounds.max.y))) { - // this could happen if the only thing visible is a perfectly horizontal or - // vertical thin line - mHasVisibleShadows = false; - return {}; - } - assert_invariant(bounds.min.x < bounds.max.x); - assert_invariant(bounds.min.y < bounds.max.y); - - s = 2.0f / float2(bounds.max.xy - bounds.min.xy); - o = float2(bounds.max.xy + bounds.min.xy) * 0.5f; - - // Quantize the scale in world-space units. This value can be very small because - // if it wasn't for floating-point imprecision, the scale would be a constant. - double2 const quantizer = 0.0625; - s = 1.0 / (ceil(1.0 / (s * quantizer)) * quantizer); - } - } else { - Aabb const bounds = compute2DBounds(WLMpMv, - wsClippedShadowReceiverVolume.data(), vertexCount); - lsLightFrustumBounds.min.xy = bounds.min.xy; - lsLightFrustumBounds.max.xy = bounds.max.xy; - // For directional lights, we further constraint the light frustum to the - // intersection of the shadow casters & shadow receivers in light-space. - // ** This relies on the 1-texel shadow map border ** - - if (engine.debug.shadowmap.focus_shadowcasters) { - intersectWithShadowCasters(&lsLightFrustumBounds, WLMpMv, wsShadowCastersVolume); - } - if (UTILS_UNLIKELY((lsLightFrustumBounds.min.x >= lsLightFrustumBounds.max.x) || - (lsLightFrustumBounds.min.y >= lsLightFrustumBounds.max.y))) { - // this could happen if the only thing visible is a perfectly horizontal or - // vertical thin line - mHasVisibleShadows = false; - return {}; - } - assert_invariant(lsLightFrustumBounds.min.x < lsLightFrustumBounds.max.x); - assert_invariant(lsLightFrustumBounds.min.y < lsLightFrustumBounds.max.y); - - s = 2.0f / float2(lsLightFrustumBounds.max.xy - lsLightFrustumBounds.min.xy); - o = float2(lsLightFrustumBounds.max.xy + lsLightFrustumBounds.min.xy) * 0.5f; - - // TODO: we could quantize `s` here to give some stability when lispsm is disabled, - // however, the quantization paramater should probably be user settable. - } - - // adjust offset for scale - o = -s * o; - - if (params.options.stable) { - snapLightFrustum(s, o, LMpMv, - sceneInfo.wsShadowCastersVolume.center(), shadowMapInfo.shadowDimension); - } - - const mat4f F(mat4f::row_major_init { - s.x, 0.0f, 0.0f, o.x, - 0.0f, s.y, 0.0f, o.y, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f, - }); + const mat4f F = computeFocusMatrix(LMpMv, WLMp, + sceneInfo.wsShadowReceiversVolume, + lsClippedShadowVolume, vertexCount, + camera, sceneInfo.csNearFar, + shadowMapInfo.shadowDimension, params.options.stable); /* * Final shadow map transform */ - // Final shadow transform - const mat4f S = F * WLMpMv; + // Final shadow transform (focused warped light-space) + const mat4f S = F * (W * LMpMv); // Computes St the transform to use in the shader to access the shadow map texture // i.e. it transforms a world-space vertex to a texture coordinate in the shadowmap @@ -392,17 +185,18 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, const mat4f St = math::highPrecisionMultiply(Mt, S); ShadowMap::ShaderParameters shaderParameters; + shaderParameters.scissorNormalized = getClampToEdgeCoords(shadowMapInfo); // note: in texelSizeWorldSpace() below, we can use Mb * Mt * F * W because // L * Mp * Mv is a rigid transform for directional lights, and doesn't matter. // if Wp[3][1] is 0, then LISPSM was cancelled. if (useLispsm && Wp[3][1] != 0.0f) { - shaderParameters.texelSizeAtOneMeterWs = texelSizeWorldSpace(Wp, mat4f(Mt * F), - shadowMapInfo.shadowDimension); + shaderParameters.texelSizeAtOneMeterWs = + texelSizeWorldSpace(Wp, mat4f(Mt * F), shadowMapInfo.shadowDimension); } else { // We know we're using an ortho projection - shaderParameters.texelSizeAtOneMeterWs = texelSizeWorldSpace(St.upperLeft(), - shadowMapInfo.shadowDimension); + shaderParameters.texelSizeAtOneMeterWs = + texelSizeWorldSpace(St.upperLeft(), shadowMapInfo.shadowDimension); } if (!shadowMapInfo.vsm) { shaderParameters.lightSpace = St; @@ -410,8 +204,6 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, shaderParameters.lightSpace = computeVsmLightSpaceMatrix(St, Mv, znear, zfar); } - shaderParameters.scissorNormalized = getClampToEdgeCoords(shadowMapInfo); - // We apply the constant bias in world space (as opposed to light-space) to account // for perspective and lispsm shadow maps. This also allows us to do this at zero-cost // by baking it in the shadow-map itself. @@ -427,11 +219,13 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, // shader as 'viewFromWorldMatrix', and is used in the VSM case to compute the depth metric. // (see depth_main.fs). Note that in the case of VSM, 'b' below is identity. mCamera->setModelMatrix(FCamera::rigidTransformInverse(math::highPrecisionMultiplyd(Mv, b))); - mCamera->setCustomProjection(mat4(Mn * F * W * L * Mp), znear, zfar); + mCamera->setCustomProjection(mat4(Mn * F * WLMp), znear, zfar); // for the debug camera, we need to undo the world origin mDebugCamera->setCustomProjection(mat4(S * b * camera.worldOrigin), znear, zfar); + mHasVisibleShadows = true; + return shaderParameters; } @@ -514,18 +308,18 @@ ShadowMap::ShaderParameters ShadowMap::updateSpot(FEngine& engine, // find decent near/far ShadowMap::updateSceneInfoSpot(Mv, scene, sceneInfo); } else { - sceneInfo.lsNearFar = { -0.01f, -radius }; + sceneInfo.lsCastersNearFar = { -0.01f, -radius }; } // if the scene was empty, near > far - mHasVisibleShadows = -sceneInfo.lsNearFar[0] < -sceneInfo.lsNearFar[1]; + mHasVisibleShadows = -sceneInfo.lsCastersNearFar[0] < -sceneInfo.lsCastersNearFar[1]; if (!mHasVisibleShadows) { return {}; } // FIXME: we need a configuration for minimum near plane (for now hardcoded to 1cm) - float const nearPlane = std::max(0.01f, -sceneInfo.lsNearFar[0]); - float const farPlane = std::min(radius, -sceneInfo.lsNearFar[1]); + float const nearPlane = std::max(0.01f, -sceneInfo.lsCastersNearFar[0]); + float const farPlane = std::min(radius, -sceneInfo.lsCastersNearFar[1]); auto outerConeAngle = lcm.getSpotLightOuterCone(li); return updatePunctual(Mv, outerConeAngle, nearPlane, farPlane, shadowMapInfo, params); @@ -559,10 +353,134 @@ ShadowMap::ShaderParameters ShadowMap::updatePoint(FEngine& engine, return updatePunctual(Mv, 45.0f * f::DEG_TO_RAD, 0.01f, radius, shadowMapInfo, params); } +ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds( + FEngine& engine, + float3 const direction, + FLightManager::ShadowParams params, + filament::CameraInfo const& camera, + SceneInfo const& sceneInfo) noexcept { + + // we use aligned_storage<> here to avoid the default initialization of std::array<> + using Storage = std::aligned_storage::type; + Storage localStorage; // NOLINT(cppcoreguidelines-pro-type-member-init) + FrustumBoxIntersection& lsClippedShadowVolume = + reinterpret_cast(localStorage); + + if (sceneInfo.lsReceiversNearFar[0] <= sceneInfo.lsReceiversNearFar[1]) { + // no visible receivers + return {}; + } + + if (sceneInfo.lsCastersNearFar[0] <= sceneInfo.lsCastersNearFar[1]) { + // no casters + return {}; + } + + // early exit if we don't have shadow receivers or casters + Aabb const& wsShadowCastersVolume = sceneInfo.wsShadowCastersVolume; + Aabb const& wsShadowReceiversVolume = sceneInfo.wsShadowReceiversVolume; + if (wsShadowCastersVolume.isEmpty() || wsShadowReceiversVolume.isEmpty()) { + return {}; + } + +#ifndef NDEBUG + // LISPSM debugging for directional light (works because we only have one) + const float dz = camera.zf - camera.zn; + float& dzn = engine.debug.shadowmap.dzn; + float& dzf = engine.debug.shadowmap.dzf; + if (dzn < 0) dzn = std::max(0.0f, params.options.shadowNearHint - camera.zn) / dz; + else params.options.shadowNearHint = dzn * dz - camera.zn; + if (dzf > 0) dzf =-std::max(0.0f, camera.zf - params.options.shadowFarHint) / dz; + else params.options.shadowFarHint = dzf * dz + camera.zf; +#endif + + /* + * Compute the light's model matrix + */ + + // 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. + mat4f const MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction); + + + Aabb lsLightFrustumBounds = computeLightFrustumBounds( + MvAtOrigin, wsShadowReceiversVolume, wsShadowCastersVolume, + sceneInfo, + params.options.stable, + engine.debug.shadowmap.focus_shadowcasters, + engine.debug.shadowmap.far_uses_shadowcasters); + + if (UTILS_UNLIKELY(!((lsLightFrustumBounds.min.x < lsLightFrustumBounds.max.x) && + (lsLightFrustumBounds.min.y < lsLightFrustumBounds.max.y)))) { + return {}; + } + + // Compute the intersection of the view volume with the intersection of receivers and casters + // in light space. This returns a set of points on the convex-hull of the intersection. + size_t const vertexCount = intersectFrustumWithBox(lsClippedShadowVolume, + camera.cullingProjection * camera.view * FCamera::rigidTransformInverse(MvAtOrigin), + sceneInfo.csNearFar, lsLightFrustumBounds); + + if (UTILS_UNLIKELY(vertexCount < 4)) { + return {}; + } + + /* + * Adjust the scene's zmax (i.e. Near plane) and zmin (i.e. Far plane) in light space. + * (near/far correspond to max/min because the light looks down the -z axis). + * - The Near plane is set to the shadow casters max z (i.e. closest to the light) + * - The Far plane is set to the closest of the farthest shadow casters and receivers + * i.e.: shadow casters behind the last receivers can't cast any shadows + * + * If "depth clamp" is supported, we can further tighten the near plane to the + * shadow receiver. + * + * Note: L has no influence here, since we're only interested in z values + * (L is a rotation around z) + */ + + for (size_t i = 0; i < vertexCount; ++i) { + float3 const v = lsClippedShadowVolume[i]; + // far: figure out the farthest shadow receivers + lsLightFrustumBounds.min.z = min(lsLightFrustumBounds.min.z, v.z); + if constexpr (USE_DEPTH_CLAMP) { + lsLightFrustumBounds.max.z = max(lsLightFrustumBounds.max.z, v.z); + } + } + if (engine.debug.shadowmap.far_uses_shadowcasters) { + // far: closest of the farthest shadow casters and receivers + lsLightFrustumBounds.min.z = + std::max(lsLightFrustumBounds.min.z, sceneInfo.lsCastersNearFar[1]); + } + + // Now that we know the znear (-lsLightFrustumBounds.max.z), adjust the light's position such + // that znear = 0, this is only needed for VSM, but doesn't hurt PCF. + const mat4f Mv = getDirectionalLightViewMatrix(direction, direction * -lsLightFrustumBounds.max.z); + + // near / far planes are specified relative to the direction the eye is looking at + // i.e. the -z axis (see: ortho) + const float znear = 0.0f; + const float zfar = lsLightFrustumBounds.max.z - lsLightFrustumBounds.min.z; + // if znear >= zfar, it means we don't have any shadow caster in front of a shadow receiver + if (UTILS_UNLIKELY(znear >= zfar)) { + return {}; + } + + // update lsLightFrustum from MvAtOrigin to Mv + for (auto& v : lsClippedShadowVolume) { + v.z -= lsLightFrustumBounds.max.z; + } + + return { Mv, znear, zfar, lsClippedShadowVolume, vertexCount, true }; +} + mat4f ShadowMap::applyLISPSM(mat4f& Wp, filament::CameraInfo const& camera, FLightManager::ShadowParams const& params, + mat4f const& LMp, + mat4f const& Mv, mat4f const& LMpMv, - FrustumBoxIntersection const& wsShadowReceiversVolume, size_t vertexCount, + FrustumBoxIntersection const& lsShadowVolume, size_t vertexCount, float3 const& dir) { const float LoV = dot(camera.getForwardVector(), dir); @@ -575,7 +493,11 @@ mat4f ShadowMap::applyLISPSM(mat4f& Wp, const float dzf = std::max(0.0f, camera.zf - params.options.shadowFarHint); // near/far plane's distance from the eye in view space of the shadow receiver volume. - float2 znf = -computeNearFar(camera.view, wsShadowReceiversVolume.data(), vertexCount); + + float2 const znf = -computeNearFar( + camera.view * FCamera::rigidTransformInverse(Mv), + lsShadowVolume.data(), vertexCount); + const float zn = std::max(camera.zn, znf[0]); // near plane distance from the eye const float zf = std::min(camera.zf, znf[1]); // far plane distance from the eye @@ -584,7 +506,7 @@ mat4f ShadowMap::applyLISPSM(mat4f& Wp, // i.e. the axis orthogonal to the light direction) and taking the min/max // of the shadow receivers' volume. // Note: znear/zfar encoded in Mp has no influence here (b/c we're interested only by the y-axis) - const float2 nf = computeNearFarOfWarpSpace(LMpMv, wsShadowReceiversVolume.data(), vertexCount); + const float2 nf = computeNearFarOfWarpSpace(LMp, lsShadowVolume.data(), vertexCount); const float n = nf[0]; // near plane coordinate of Mp (light space) const float f = nf[1]; // far plane coordinate of Mp (light space) const float d = std::abs(f - n); // Wp's depth-range d (abs necessary because we're dealing with z-coordinates, not distances) @@ -636,6 +558,112 @@ mat4f ShadowMap::applyLISPSM(mat4f& Wp, return W; } +math::mat4f ShadowMap::computeLightRotation(math::float3 const& lsDirection) noexcept { + mat4f L; + // Orient the shadow map in the direction of the view vector by constructing a + // rotation matrix in light space around the z-axis, that aligns the y-axis with the camera's + // forward vector (V) -- this gives the wrap direction, vp, for LiSPSM. + // If the light and view vector are parallel, this rotation becomes + // meaningless. Just use identity. + // (LdotV == (Mv*V).z, because L = {0,0,1} in light-space) + if (UTILS_LIKELY(std::abs(lsDirection.z) < 0.9997f)) { // this is |dot(L, V)| + const float3 vp{ normalize(lsDirection.xy), 0 }; // wrap direction in light-space + L[0].xyz = cross(vp, float3{ 0, 0, 1 }); + L[1].xyz = vp; + L[2].xyz = { 0, 0, 1 }; + L = transpose(L); + } + return L; +} + +math::mat4f ShadowMap::computeFocusMatrix( + const mat4f& LMpMv, const mat4f& WLMp, + Aabb const& wsShadowReceiversVolume, + FrustumBoxIntersection const& lsShadowVolume, size_t vertexCount, + filament::CameraInfo const& camera, float2 const& csNearFar, + uint16_t shadowDimension, bool stable) noexcept { + + float2 s, o; + float4 wsViewVolumeBoundingSphere = {}; + + if (stable) { + // In stable mode, the light frustum size must be fixed, so we can choose either the + // whole view frustum, or the whole scene bounding volume. We simply pick whichever + // is smaller. + + // in stable mode we simply take the shadow receivers volume + const float4 shadowReceiverVolumeBoundingSphere = computeBoundingSphere( + wsShadowReceiversVolume.getCorners().data(), 8); + + // in stable mode we simply take the view volume bounding sphere, but we calculate it + // in view space, so that it's perfectly stable. + mat4f const viewFromClip = inverse(camera.cullingProjection); + Corners const wsFrustumVertices = computeFrustumCorners(viewFromClip, csNearFar); + wsViewVolumeBoundingSphere = computeBoundingSphere(wsFrustumVertices.vertices, 8); + + if (shadowReceiverVolumeBoundingSphere.w < wsViewVolumeBoundingSphere.w) { + // When using the shadowReceiver volume, we don't have to use its enclosing sphere + // because (we assume) the scene volume doesn't change. Seen from the light it only + // changes when the light moves or rotates, and it is acceptable in that case to have + // non "stable" shadows (the shadow will never be stable when the light moves). + // + // On the other hand, when using the view volume, we must use a sphere because otherwise + // its projection's bounds in light space change with the camera, leading to unstable + // shadows with camera movement. + + wsViewVolumeBoundingSphere.w = 0; + } + + if (wsViewVolumeBoundingSphere.w > 0) { + s = 1.0f / wsViewVolumeBoundingSphere.w; + o = mat4f::project(LMpMv * camera.model, wsViewVolumeBoundingSphere.xyz).xy; + } else { + // TODO: another options is the sphere around the intersections of receiver & casters + // FIXME: this is not stable with the global rotation because wsShadowReceiversVolume + // is not stable with it. + Aabb const bounds = compute2DBounds(LMpMv, + wsShadowReceiversVolume.getCorners().data(), + wsShadowReceiversVolume.getCorners().size()); + assert_invariant(bounds.min.x < bounds.max.x); + assert_invariant(bounds.min.y < bounds.max.y); + + s = 2.0f / float2(bounds.max.xy - bounds.min.xy); + o = float2(bounds.max.xy + bounds.min.xy) * 0.5f; + + // Quantize the scale in world-space units. This value can be very small because + // if it wasn't for floating-point imprecision, the scale would be a constant. + double2 const quantizer = 0.0625; + s = 1.0 / (ceil(1.0 / (s * quantizer)) * quantizer); + } + } else { + Aabb const bounds = compute2DBounds(WLMp, lsShadowVolume.data(), vertexCount); + assert_invariant(bounds.min.x < bounds.max.x); + assert_invariant(bounds.min.y < bounds.max.y); + + s = 2.0f / float2(bounds.max.xy - bounds.min.xy); + o = float2(bounds.max.xy + bounds.min.xy) * 0.5f; + + // TODO: we could quantize `s` here to give some stability when lispsm is disabled, + // however, the quantization paramater should probably be user settable. + } + + // adjust offset for scale + o = -s * o; + + if (stable) { + snapLightFrustum(s, o, LMpMv, wsShadowReceiversVolume.center(), shadowDimension); + } + + const mat4f F(mat4f::row_major_init { + s.x, 0.0f, 0.0f, o.x, + 0.0f, s.y, 0.0f, o.y, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f, + }); + + return F; +} + // Apply these remapping in double to maintain a high precision for the depth axis ShadowMap::TextureCoordsMapping ShadowMap::getTextureCoordsMapping(ShadowMapInfo const& info, @@ -747,7 +775,7 @@ float4 ShadowMap::computeBoundingSphere(float3 const* vertices, size_t count) no for (size_t i = 0; i < count; i++) { s.xyz += vertices[i]; } - s.xyz *= 1.0f / count; + s.xyz *= 1.0f / float(count); for (size_t i = 0; i < count; i++) { s.w = std::max(s.w, length2(vertices[i] - s.xyz)); } @@ -758,71 +786,89 @@ float4 ShadowMap::computeBoundingSphere(float3 const* vertices, size_t count) no Aabb ShadowMap::compute2DBounds(const mat4f& lightView, float3 const* wsVertices, size_t count) noexcept { Aabb bounds{}; - // disable vectorization here because vertexCount is <= 64, not worth the increased code size. - #pragma clang loop vectorize(disable) - for (size_t i = 0; i < count; ++i) { - const float3 v = mat4f::project(lightView, wsVertices[i]); + utils::Slice const vertices{ wsVertices, count }; + for (auto const& vertice : vertices) { + const float3 v = mat4f::project(lightView, vertice); bounds.min.xy = min(bounds.min.xy, v.xy); bounds.max.xy = max(bounds.max.xy, v.xy); } return bounds; } -void ShadowMap::intersectWithShadowCasters(Aabb* UTILS_RESTRICT lightFrustum, - mat4f const& lightView, Aabb const& wsShadowCastersVolume) noexcept { - - // construct the Focus transform (scale + offset) - const float2 s = 2.0f / float2(lightFrustum->max.xy - lightFrustum->min.xy); - const float2 o = -s * float2(lightFrustum->max.xy + lightFrustum->min.xy) * 0.5f; - const mat4f F(mat4f::row_major_init { - s.x, 0.0f, 0.0f, o.x, - 0.0f, s.y, 0.0f, o.y, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f, - }); - float3 wsLightFrustumCorners[8]; - const mat4f projection = F * lightView; - const Frustum wsLightFrustum(projection); - computeFrustumCorners(wsLightFrustumCorners, inverse(projection)); - - // Intersect shadow-caster AABB with current light frustum in world-space: - // - // This (obviously) guarantees that the resulting volume is inside the light frustum; - // when using LiSPSM (or projection lights, i.e. when lightView is a projection), we must - // first intersect wsShadowCastersVolume with the light's frustum, otherwise we end-up - // transforming vertices that are "outside" the frustum, and that's forbidden. - FrustumBoxIntersection wsClippedShadowCasterVolumeVertices; - size_t const vertexCount = intersectFrustumWithBox(wsClippedShadowCasterVolumeVertices, - wsLightFrustum, wsLightFrustumCorners, wsShadowCastersVolume); - - // compute shadow-caster bounds in light space - Aabb const box = compute2DBounds(lightView, wsClippedShadowCasterVolumeVertices.data(), vertexCount); - - // intersect shadow-caster and current light frustum bounds - lightFrustum->min.xy = max(box.min.xy, lightFrustum->min.xy); - lightFrustum->max.xy = min(box.max.xy, lightFrustum->max.xy); +Aabb ShadowMap::compute2DBounds(const mat4f& lightView, + Aabb const& volume) noexcept { + Aabb bounds{}; + auto const& corners = volume.getCorners(); + for (auto const& corner : corners) { + const float3 v = mat4f::project(lightView, corner); + bounds.min = min(bounds.min, v); + bounds.max = max(bounds.max, v); + } + return bounds; } -void ShadowMap::computeFrustumCorners(float3* UTILS_RESTRICT out, - const mat4f& UTILS_RESTRICT projectionViewInverse, float2 csNearFar) noexcept { +ShadowMap::Corners ShadowMap::computeFrustumCorners( + mat4f const& projectionInverse, + float2 csNearFar) noexcept { + + Corners out; // compute view frustum in world space (from its NDC) // matrix to convert: ndc -> camera -> world - float near = csNearFar.x; - float far = csNearFar.y; - float3 const csViewFrustumCorners[8] = { - { -1, -1, far }, - { 1, -1, far }, - { -1, 1, far }, - { 1, 1, far }, - { -1, -1, near }, - { 1, -1, near }, - { -1, 1, near }, - { 1, 1, near }, + float const near = csNearFar.x; + float const far = csNearFar.y; + + Corners const csViewFrustumCorners = { + .vertices = { + { -1, -1, far }, + { 1, -1, far }, + { -1, 1, far }, + { 1, 1, far }, + { -1, -1, near }, + { 1, -1, near }, + { -1, 1, near }, + { 1, 1, near }, + } }; - for (float3 const c : csViewFrustumCorners) { - *out++ = mat4f::project(projectionViewInverse, c); + + for (size_t i = 0; i < csViewFrustumCorners.size(); i++) { + out[i] = mat4f::project(projectionInverse, csViewFrustumCorners[i]); } + + return out; +} + +Aabb ShadowMap::computeLightFrustumBounds(mat4f const& lightView, + Aabb const& wsShadowReceiversVolume, Aabb const& wsShadowCastersVolume, + ShadowMap::SceneInfo const& sceneInfo, bool stable, bool focusShadowCasters, + bool farUsesShadowCasters) noexcept { + Aabb lsLightFrustumBounds{}; + + float const receiversFar = sceneInfo.lsReceiversNearFar[1]; + float const castersNear = sceneInfo.lsCastersNearFar[0]; + float const castersFar = sceneInfo.lsCastersNearFar[1]; + + Aabb const lsReceiversBounds = compute2DBounds(lightView, wsShadowReceiversVolume); + // far plane from receivers + lsLightFrustumBounds.min = { lsReceiversBounds.min.xy, receiversFar }; + // near plane from casters + lsLightFrustumBounds.max = { lsReceiversBounds.max.xy, castersNear }; + + if (UTILS_LIKELY(!stable && focusShadowCasters)) { + // For directional lights, we further constraint the light frustum to the + // intersection of the shadow casters & shadow receivers in light-space. + // ** This relies on the 1-texel shadow map border ** + Aabb const lsCastersBounds = compute2DBounds(lightView, wsShadowCastersVolume); + // intersection of in light space of casters and receiver + lsLightFrustumBounds.min.xy = max(lsLightFrustumBounds.min.xy, lsCastersBounds.min.xy); + lsLightFrustumBounds.max.xy = min(lsLightFrustumBounds.max.xy, lsCastersBounds.max.xy); + } + + if (farUsesShadowCasters) { + // far: closest of the farthest shadow casters and receivers + lsLightFrustumBounds.min.z = max(lsLightFrustumBounds.min.z, castersFar); + } + return lsLightFrustumBounds; } void ShadowMap::snapLightFrustum(float2& s, float2& o, @@ -863,10 +909,12 @@ void ShadowMap::snapLightFrustum(float2& s, float2& o, size_t ShadowMap::intersectFrustumWithBox( FrustumBoxIntersection& UTILS_RESTRICT outVertices, - Frustum const& UTILS_RESTRICT wsFrustum, - float3 const* UTILS_RESTRICT wsFrustumCorners, - Aabb const& UTILS_RESTRICT wsBox) + mat4f const& UTILS_RESTRICT projection, math::float2 const& UTILS_RESTRICT csNearFar, + Aabb const& UTILS_RESTRICT box) { + Frustum const frustum{ projection }; + Corners const frustumVertices{ computeFrustumCorners(inverse(projection), csNearFar) }; + constexpr const float EPSILON = 1.0f / 8192.0f; // ~0.012 mm size_t vertexCount = 0; @@ -883,16 +931,16 @@ size_t ShadowMap::intersectFrustumWithBox( */ // world-space scene volume - const Aabb::Corners wsSceneReceiversCorners = wsBox.getCorners(); + const Aabb::Corners boxCorners = box.getCorners(); // a) Keep the frustum's vertices that are known to be inside the scene's box UTILS_NOUNROLL for (size_t i = 0; i < 8; i++) { - float3 const p = wsFrustumCorners[i]; + float3 const p = frustumVertices[i]; outVertices[vertexCount] = p; - if ((p.x >= wsBox.min.x && p.x <= wsBox.max.x) && - (p.y >= wsBox.min.y && p.y <= wsBox.max.y) && - (p.z >= wsBox.min.z && p.z <= wsBox.max.z)) { + if ((p.x >= box.min.x && p.x <= box.max.x) && + (p.y >= box.min.y && p.y <= box.max.y) && + (p.z >= box.min.z && p.z <= box.max.z)) { vertexCount++; } } @@ -900,7 +948,7 @@ size_t ShadowMap::intersectFrustumWithBox( // at this point if we have 8 vertices, we can skip the rest if (vertexCount < 8) { const size_t frustumVerticesInsideBoxCount = vertexCount; - float4 const* wsFrustumPlanes = wsFrustum.getNormalizedPlanes(); + float4 const* frustumPlanes = frustum.getNormalizedPlanes(); // b) add the scene's vertices that are known to be inside the view frustum // @@ -908,14 +956,14 @@ size_t ShadowMap::intersectFrustumWithBox( // the frustum. This actually often happens due to fitting light-space // We fudge the distance to the plane by a small amount. #pragma nounroll - for (float3 const p : wsSceneReceiversCorners) { + for (float3 const p : boxCorners) { outVertices[vertexCount] = p; - float const l = dot(wsFrustumPlanes[0].xyz, p) + wsFrustumPlanes[0].w; - float const b = dot(wsFrustumPlanes[1].xyz, p) + wsFrustumPlanes[1].w; - float const r = dot(wsFrustumPlanes[2].xyz, p) + wsFrustumPlanes[2].w; - float const t = dot(wsFrustumPlanes[3].xyz, p) + wsFrustumPlanes[3].w; - float const f = dot(wsFrustumPlanes[4].xyz, p) + wsFrustumPlanes[4].w; - float const n = dot(wsFrustumPlanes[5].xyz, p) + wsFrustumPlanes[5].w; + float const l = dot(frustumPlanes[0].xyz, p) + frustumPlanes[0].w; + float const b = dot(frustumPlanes[1].xyz, p) + frustumPlanes[1].w; + float const r = dot(frustumPlanes[2].xyz, p) + frustumPlanes[2].w; + float const t = dot(frustumPlanes[3].xyz, p) + frustumPlanes[3].w; + float const f = dot(frustumPlanes[4].xyz, p) + frustumPlanes[4].w; + float const n = dot(frustumPlanes[5].xyz, p) + frustumPlanes[5].w; if ((l <= EPSILON) && (b <= EPSILON) && (r <= EPSILON) && (t <= EPSILON) && (f <= EPSILON) && (n <= EPSILON)) { @@ -968,11 +1016,11 @@ size_t ShadowMap::intersectFrustumWithBox( if (boxVerticesInsideFrustumCount < 8) { // c) intersect scene's volume edges with frustum planes vertexCount = intersectFrustum(outVertices.data(), vertexCount, - wsSceneReceiversCorners.vertices, wsFrustumCorners); + boxCorners.vertices, frustumVertices.vertices); // d) intersect frustum edges with the scene's volume planes vertexCount = intersectFrustum(outVertices.data(), vertexCount, - wsFrustumCorners, wsSceneReceiversCorners.vertices); + frustumVertices.vertices, boxCorners.vertices); } else { // by construction vertexCount should be 8 here, but it can be more because // step (b) above can classify a point as inside the frustum that isn't quite inside. @@ -1203,14 +1251,22 @@ void ShadowMap::updateSceneInfoDirectional(mat4f const& Mv, FScene const& scene, // the code below only works with affine transforms assert_invariant(transpose(Mv)[3] == float4(0, 0, 0, 1)); - sceneInfo.lsNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; + sceneInfo.lsCastersNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; + sceneInfo.lsReceiversNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; visitScene(scene, sceneInfo.visibleLayers, [&](Aabb caster, Culler::result_type) { auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster); - sceneInfo.lsNearFar.x = std::max(sceneInfo.lsNearFar.x, r.max.z); // near - sceneInfo.lsNearFar.y = std::min(sceneInfo.lsNearFar.y, r.min.z); // far + sceneInfo.lsCastersNearFar.x = max(sceneInfo.lsCastersNearFar.x, r.max.z); + sceneInfo.lsCastersNearFar.y = min(sceneInfo.lsCastersNearFar.y, r.min.z); }, - [&](Aabb receiver, Culler::result_type) { + [&](Aabb receiver, Culler::result_type vis) { + // account only for objects that are visible by the camera + auto mask = 1u << VISIBLE_RENDERABLE_BIT; + if ((vis & mask) == mask) { + auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, receiver); + sceneInfo.lsReceiversNearFar.x = max(sceneInfo.lsReceiversNearFar.x, r.max.z); + sceneInfo.lsReceiversNearFar.y = min(sceneInfo.lsReceiversNearFar.y, r.min.z); + } } ); } @@ -1221,13 +1277,15 @@ void ShadowMap::updateSceneInfoSpot(mat4f const& Mv, FScene const& scene, // the code below only works with affine transforms assert_invariant(transpose(Mv)[3] == float4(0, 0, 0, 1)); - sceneInfo.lsNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; + sceneInfo.lsCastersNearFar = { std::numeric_limits::lowest(), std::numeric_limits::max() }; + // account only for objects that are visible by both the camera and the light visitScene(scene, sceneInfo.visibleLayers, - [&](Aabb caster, Culler::result_type mask) { - if (mask & VISIBLE_DYN_SHADOW_RENDERABLE) { + [&](Aabb caster, Culler::result_type vis) { + auto mask = VISIBLE_DYN_SHADOW_RENDERABLE; + if ((vis & mask) == mask) { auto r = Aabb::transform(Mv.upperLeft(), Mv[3].xyz, caster); - sceneInfo.lsNearFar.x = std::max(sceneInfo.lsNearFar.x, r.max.z); // near - sceneInfo.lsNearFar.y = std::min(sceneInfo.lsNearFar.y, r.min.z); // far + sceneInfo.lsCastersNearFar.x = std::max(sceneInfo.lsCastersNearFar.x, r.max.z); // near + sceneInfo.lsCastersNearFar.y = std::min(sceneInfo.lsCastersNearFar.y, r.min.z); // far } }, [&](Aabb receiver, Culler::result_type) { diff --git a/filament/src/ShadowMap.h b/filament/src/ShadowMap.h index 5aee0163c6..af78e271a4 100644 --- a/filament/src/ShadowMap.h +++ b/filament/src/ShadowMap.h @@ -26,6 +26,7 @@ #include +#include #include #include @@ -104,7 +105,8 @@ public: // scratch data: light's near/far expressed in light-space, calculated from the scene's // content assuming the light is at the origin. - math::float2 lsNearFar; + math::float2 lsCastersNearFar; + math::float2 lsReceiversNearFar; // Viewing camera's near/far expressed in view-space, calculated from the // scene's content. @@ -207,6 +209,8 @@ private: uint8_t v0, v1, v2, v3; }; + using Corners = Aabb::Corners; + // 8 corners, 12 segments w/ 2 intersection max -- all of this twice (8 + 12 * 2) * 2 (768 bytes) using FrustumBoxIntersection = std::array; @@ -215,17 +219,50 @@ private: const ShadowMapInfo& shadowMapInfo, const FLightManager::ShadowParams& params) noexcept; + struct DirectionalShadowBounds { + math::mat4f Mv; + float zNear; + float zFar; + FrustumBoxIntersection lsClippedShadowVolume; + size_t vertexCount; + bool visibleShadows = false; + }; + + static DirectionalShadowBounds computeDirectionalShadowBounds( + FEngine& engine, + math::float3 direction, + FLightManager::ShadowParams params, + filament::CameraInfo const& camera, + SceneInfo const& sceneInfo) noexcept; + static math::mat4f applyLISPSM(math::mat4f& Wp, filament::CameraInfo const& camera, FLightManager::ShadowParams const& params, + const math::mat4f& LMp, + const math::mat4f& Mv, const math::mat4f& LMpMv, - FrustumBoxIntersection const& wsShadowReceiverVolume, size_t vertexCount, + FrustumBoxIntersection const& lsShadowVolume, size_t vertexCount, const math::float3& dir); + static inline math::mat4f computeLightRotation(math::float3 const& lsDirection) noexcept; + + static inline math::mat4f computeFocusMatrix( + const math::mat4f& LMpMv, + const math::mat4f& WLMp, + Aabb const& wsShadowReceiversVolume, + FrustumBoxIntersection const& lsShadowVolume, size_t vertexCount, + filament::CameraInfo const& camera, math::float2 const& csNearFar, + uint16_t shadowDimension, bool stable) noexcept; + static inline void snapLightFrustum(math::float2& s, math::float2& o, math::mat4f const& Mv, math::double3 wsSnapCoords, math::int2 resolution) noexcept; - static inline void computeFrustumCorners(math::float3* out, - const math::mat4f& projectionViewInverse, math::float2 csNearFar = { -1.0f, 1.0f }) noexcept; + static inline Aabb computeLightFrustumBounds(const math::mat4f& lightView, + Aabb const& wsShadowReceiversVolume, Aabb const& wsShadowCastersVolume, + SceneInfo const& sceneInfo, + bool stable, bool focusShadowCasters, bool farUsesShadowCasters) noexcept; + + static Corners computeFrustumCorners(const math::mat4f& projectionInverse, + math::float2 csNearFar = { -1.0f, 1.0f }) noexcept; static inline math::float2 computeNearFar(math::mat4f const& view, math::float3 const* wsVertices, size_t count) noexcept; @@ -240,9 +277,8 @@ private: static inline Aabb compute2DBounds(const math::mat4f& lightView, math::float3 const* wsVertices, size_t count) noexcept; - static inline void intersectWithShadowCasters(Aabb* lightFrustum, - math::mat4f const& lightView, - Aabb const& wsShadowCastersVolume) noexcept; + static inline Aabb compute2DBounds(const math::mat4f& lightView, + Aabb const& volume) noexcept; static inline math::float2 computeNearFarOfWarpSpace(math::mat4f const& lightView, math::float3 const* wsVertices, size_t count) noexcept; @@ -261,9 +297,8 @@ private: static size_t intersectFrustumWithBox( FrustumBoxIntersection& outVertices, - Frustum const& wsFrustum, - math::float3 const* wsFrustumCorners, - Aabb const& wsBox); + math::mat4f const& projection, math::float2 const& csNearFar, + Aabb const& box); static math::mat4f warpFrustum(float n, float f) noexcept; diff --git a/samples/gltf_viewer.cpp b/samples/gltf_viewer.cpp index f445462586..2303e412c4 100644 --- a/samples/gltf_viewer.cpp +++ b/samples/gltf_viewer.cpp @@ -761,6 +761,8 @@ int main(int argc, char** argv) { ImGui::Checkbox("Camera at origin", debug.getPropertyAddress("d.view.camera_at_origin")); ImGui::Checkbox("Far Origin", &app.originIsFarAway); ImGui::SliderFloat("Origin", &app.originDistance, 0, 10000000); + ImGui::Checkbox("Far uses shadow casters", debug.getPropertyAddress("d.shadowmap.far_uses_shadowcasters")); + ImGui::Checkbox("Focus shadow casters", debug.getPropertyAddress("d.shadowmap.focus_shadowcasters")); auto dataSource = debug.getDataSource("d.view.frame_info"); if (dataSource.data) { ImGuiExt::PlotLinesSeries("FrameInfo", 6,