From 763950ec1829bca914ce083f0ee8dcee4e4dd90c Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Wed, 8 Nov 2023 12:33:24 -0800 Subject: [PATCH] Revert "fix a couple shadow stability bugs" This reverts commit 1b0db0fca28982f796abc67b589d1b077ef0efc0. --- RELEASE_NOTES.md | 1 - filament/src/PerShadowMapUniforms.cpp | 2 +- filament/src/PerViewUniforms.cpp | 4 +- filament/src/ShadowMap.cpp | 185 ++++++++++++++++---------- filament/src/ShadowMap.h | 15 ++- filament/src/ShadowMapManager.cpp | 5 +- filament/src/details/Camera.cpp | 6 +- filament/src/details/Camera.h | 6 +- filament/src/details/Scene.cpp | 66 +++------ filament/src/details/Scene.h | 6 +- filament/src/details/View.cpp | 20 ++- filament/test/filament_test.cpp | 4 +- libs/math/include/math/mat3.h | 31 ----- libs/math/include/math/mat4.h | 29 ++-- libs/math/tests/test_mat.cpp | 112 +++++++--------- samples/gltf_viewer.cpp | 11 +- 16 files changed, 222 insertions(+), 281 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 52ba23eeaf..453d4f4c16 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -15,7 +15,6 @@ Instead, if you are authoring a PR for the main branch, add your release note to - engine: New tone mapper: `AgXTonemapper`. - matinfo: Add support for viewing ESSL 1.0 shaders - engine: Add `Renderer::getClearOptions()` [b/243846268] -- engine: Fix stable shadows (again) when an IBL rotation is used ## v1.45.0 diff --git a/filament/src/PerShadowMapUniforms.cpp b/filament/src/PerShadowMapUniforms.cpp index 1c1ac24b72..87b78a9a9e 100644 --- a/filament/src/PerShadowMapUniforms.cpp +++ b/filament/src/PerShadowMapUniforms.cpp @@ -60,7 +60,7 @@ void PerShadowMapUniforms::prepareCamera(Transaction const& transaction, s.viewFromClipMatrix = viewFromClip; // 1/projection s.clipFromWorldMatrix[0] = clipFromWorld; // projection * view s.worldFromClipMatrix = worldFromClip; // 1/(projection * view) - s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldTransform)); + s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldOrigin)); s.clipTransform = camera.clipTransform; s.cameraFar = camera.zf; s.oneOverFarMinusNear = 1.0f / (camera.zf - camera.zn); diff --git a/filament/src/PerViewUniforms.cpp b/filament/src/PerViewUniforms.cpp index 2f68bdfd57..6ea6f4f629 100644 --- a/filament/src/PerViewUniforms.cpp +++ b/filament/src/PerViewUniforms.cpp @@ -75,7 +75,7 @@ void PerViewUniforms::prepareCamera(FEngine& engine, const CameraInfo& camera) n s.clipFromViewMatrix = clipFromView; // projection s.viewFromClipMatrix = viewFromClip; // 1/projection s.worldFromClipMatrix = worldFromClip; // 1/(projection * view) - s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldTransform)); + s.userWorldFromWorldMatrix = mat4f(inverse(camera.worldOrigin)); s.clipTransform = camera.clipTransform; s.cameraFar = camera.zf; s.oneOverFarMinusNear = 1.0f / (camera.zf - camera.zn); @@ -151,7 +151,7 @@ void PerViewUniforms::prepareFog(FEngine& engine, const CameraInfo& cameraInfo, // why we store the cofactor matrix. mat4f const viewFromWorld = cameraInfo.view; - mat4 const worldFromUserWorld = cameraInfo.worldTransform; + mat4 const worldFromUserWorld = cameraInfo.worldOrigin; mat4 const worldFromFog = worldFromUserWorld * userWorldFromFog; mat4 const viewFromFog = viewFromWorld * worldFromFog; diff --git a/filament/src/ShadowMap.cpp b/filament/src/ShadowMap.cpp index e2b8dc769e..318a566cc3 100644 --- a/filament/src/ShadowMap.cpp +++ b/filament/src/ShadowMap.cpp @@ -77,13 +77,20 @@ void ShadowMap::initialize(size_t lightIndex, ShadowType shadowType, mFace = face; } -math::mat4f ShadowMap::getDirectionalLightViewMatrix(math::float3 direction, math::float3 up, - math::float3 position) noexcept { - // 1. we use the x-axis as the "up" reference so that the math is stable when the light - // is pointing down, which is a common case for lights. - // 2. we do the math in double to avoid some precision issues when the light is almost - // straight (i.e. parallel to the x-axis) - mat4f const Mm = mat4f{ mat4::lookTo(direction, position, up) }; +mat4f ShadowMap::getDirectionalLightViewMatrix(float3 direction, float3 position) noexcept { + auto z_axis = direction; + auto norm_up = float3{ 0, 1, 0 }; + if (UTILS_UNLIKELY(std::abs(dot(z_axis, norm_up)) > 0.999f)) { + // Fix up vector if we're degenerate (looking straight up, basically) + norm_up = { norm_up.z, norm_up.x, norm_up.y }; + } + auto x_axis = normalize(cross(z_axis, norm_up)); + auto y_axis = cross(x_axis, z_axis); + const mat4f Mm{ + float4{ x_axis, 0 }, + float4{ y_axis, 0 }, + float4{ -z_axis, 0 }, + float4{ position, 1 }}; return FCamera::rigidTransformInverse(Mm); } @@ -98,7 +105,7 @@ math::mat4f ShadowMap::getPointLightViewMatrix(backend::TextureCubemapFace face, case TextureCubemapFace::POSITIVE_Z: direction = { 0, 0, 1 }; break; case TextureCubemapFace::NEGATIVE_Z: direction = { 0, 0, -1 }; break; } - const mat4f Mv = getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position); + const mat4f Mv = getDirectionalLightViewMatrix(direction, position); return Mv; } @@ -113,7 +120,7 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, FLightManager::ShadowParams const params = lcm.getShadowParams(li); // We can't use LISPSM in stable mode - const auto direction = lightData.elementAt(index); + const auto direction = params.options.transform * lightData.elementAt(index); auto [Mv, znear, zfar, lsClippedShadowVolume, vertexCount, visibleShadows] = computeDirectionalShadowBounds(engine, direction, params, camera, sceneInfo); @@ -159,22 +166,11 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, // This is the most important step to increase the quality of the shadow map. // // In LiPSM mode, we're using the warped space here. - float4 f = computeFocusParams(LMpMv, WLMp, + const mat4f F = computeFocusMatrix(LMpMv, WLMp, + sceneInfo.wsShadowReceiversVolume, lsClippedShadowVolume, vertexCount, camera, sceneInfo.csNearFar, - params.options.shadowFar, params.options.stable); - - if (params.options.stable) { - const auto lsRef = lightData.elementAt(index); - snapLightFrustum(f.xy, f.zw, lsRef, shadowMapInfo.shadowDimension); - } - - const mat4f F(mat4f::row_major_init { - f.x, 0.0f, 0.0f, f.z, - 0.0f, f.y, 0.0f, f.w, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f, - }); + shadowMapInfo.shadowDimension, params.options.stable); /* * Final shadow map transform @@ -226,7 +222,7 @@ ShadowMap::ShaderParameters ShadowMap::updateDirectional(FEngine& engine, 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.worldTransform), znear, zfar); + mDebugCamera->setCustomProjection(mat4(S * b * camera.worldOrigin), znear, zfar); mHasVisibleShadows = true; @@ -301,7 +297,7 @@ ShadowMap::ShaderParameters ShadowMap::updateSpot(FEngine& engine, auto radius = lightData.elementAt(index).w; auto li = lightData.elementAt(index); const FLightManager::ShadowParams& params = lcm.getShadowParams(li); - const mat4f Mv = getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position); + const mat4f Mv = getDirectionalLightViewMatrix(direction, position); // We only keep this for reference. updateSceneInfoSpot() is quite expensive on large scenes // currently, and only needed to find a near/far. Instead, we just use a small near and the @@ -405,8 +401,7 @@ ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds( // 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, - normalize(camera.worldTransform[0].xyz)); + mat4f const MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction); Aabb lsLightFrustumBounds = computeLightFrustumBounds( @@ -460,6 +455,10 @@ ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds( 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; @@ -474,11 +473,6 @@ ShadowMap::DirectionalShadowBounds ShadowMap::computeDirectionalShadowBounds( v.z -= lsLightFrustumBounds.max.z; } - // 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, normalize(camera.worldTransform[0].xyz), - direction * -lsLightFrustumBounds.max.z); - return { Mv, znear, zfar, lsClippedShadowVolume, vertexCount, true }; } @@ -583,36 +577,65 @@ math::mat4f ShadowMap::computeLightRotation(math::float3 const& lsDirection) noe return L; } -math::float4 ShadowMap::computeFocusParams( - mat4f const& LMpMv, - mat4f const& WLMp, +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, - float shadowFar, bool stable) noexcept { + 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 choose the - // whole view frustum. - // We simply take the view volume bounding sphere, but we calculate it + // 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); - auto getViewVolumeBoundingSphere = [&]() { - if (shadowFar > 0) { - float4 const wsViewVolumeBoundingSphere = { camera.getPosition(), shadowFar }; - return wsViewVolumeBoundingSphere; - } else { - mat4f const viewFromClip = inverse(camera.cullingProjection); - Corners const wsFrustumVertices = computeFrustumCorners(viewFromClip, csNearFar); - float4 const wsViewVolumeBoundingSphere = - computeBoundingSphere(wsFrustumVertices.vertices, 8); - return wsViewVolumeBoundingSphere; - } - }; + 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. - float4 const wsViewVolumeBoundingSphere = getViewVolumeBoundingSphere(); - s = 1.0f / wsViewVolumeBoundingSphere.w; - o = mat4f::project(LMpMv * camera.model, wsViewVolumeBoundingSphere.xyz).xy; - o = -s * o; + 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); @@ -620,11 +643,29 @@ math::float4 ShadowMap::computeFocusParams( s = 2.0f / float2(bounds.max.xy - bounds.min.xy); o = float2(bounds.max.xy + bounds.min.xy) * 0.5f; - o = -s * o; + + // TODO: we could quantize `s` here to give some stability when lispsm is disabled, + // however, the quantization paramater should probably be user settable. } - return { s, o }; + + // 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, backend::Viewport const& viewport) noexcept { @@ -638,8 +679,6 @@ ShadowMap::TextureCoordsMapping ShadowMap::getTextureCoordsMapping(ShadowMapInfo 0.0f, 0.0f, 0.0f, 1.0f }}; - constexpr mat4f MtInverse = inverse(Mt); - // apply the viewport transform const float2 o = float2{ viewport.left, viewport.bottom } / float(info.atlasDimension); const float2 s = float2{ viewport.width, viewport.height } / float(info.atlasDimension); @@ -661,7 +700,7 @@ ShadowMap::TextureCoordsMapping ShadowMap::getTextureCoordsMapping(ShadowMapInfo }} : mat4f{}; // Compute shadow-map texture access and viewport transform - return { Mf * (Mv * Mt), MtInverse * (Mv * Mt) }; + return { Mf * (Mv * Mt), inverse(Mt) * (Mv * Mt) }; } mat4f ShadowMap::computeVsmLightSpaceMatrix(const mat4f& lightSpacePcf, @@ -802,8 +841,8 @@ ShadowMap::Corners ShadowMap::computeFrustumCorners( Aabb ShadowMap::computeLightFrustumBounds(mat4f const& lightView, Aabb const& wsShadowReceiversVolume, Aabb const& wsShadowCastersVolume, - ShadowMap::SceneInfo const& sceneInfo, - bool stable, bool focusShadowCasters, bool farUsesShadowCasters) noexcept { + ShadowMap::SceneInfo const& sceneInfo, bool stable, bool focusShadowCasters, + bool farUsesShadowCasters) noexcept { Aabb lsLightFrustumBounds{}; float const receiversFar = sceneInfo.lsReceiversNearFar[1]; @@ -834,13 +873,13 @@ Aabb ShadowMap::computeLightFrustumBounds(mat4f const& lightView, } void ShadowMap::snapLightFrustum(float2& s, float2& o, - double2 lsRef, int2 resolution) noexcept { + mat4f const& Mv, double3 wsSnapCoords, int2 resolution) noexcept { - auto proj2 = [](mat4 m, double2 v) -> double2 { - double2 p; - p.x = dot(double2{ m[0].x, m[1].x }, v) + m[3].x; - p.y = dot(double2{ m[0].y, m[1].y }, v) + m[3].y; - return p; + auto proj = [](mat4 m, double3 v) -> double3 { + // for directional light p.w == 1, exactly + auto p = m * v; + assert_invariant(p.w == 1.0); + return p.xyz; }; auto fract = [](auto v) { @@ -857,13 +896,13 @@ void ShadowMap::snapLightFrustum(float2& s, float2& o, }); // The (resolution * 0.5) comes from Mv having a NDC in the range -1,1 (so a range of 2). - // Another (resolution * 0.5) is there to snap on even texels, which helps with debugging + + // focused light-space + mat4 const FMv{ F * Mv }; // This offsets the texture coordinates, so it has a fixed offset w.r.t the world - // F * Mv * ref - - double2 const lsFocusedOrigin = proj2(F, lsRef); - double2 const d = fract(lsFocusedOrigin * (resolution * 0.25)) / (resolution * 0.25); + double2 const lsOrigin = proj(FMv, wsSnapCoords).xy; + double2 const d = (fract(lsOrigin * resolution * 0.5) * 2.0) / resolution; // adjust offset o -= d; diff --git a/filament/src/ShadowMap.h b/filament/src/ShadowMap.h index b1678503dd..3849710c3b 100644 --- a/filament/src/ShadowMap.h +++ b/filament/src/ShadowMap.h @@ -122,8 +122,8 @@ public: uint8_t visibleLayers; }; - static math::mat4f getDirectionalLightViewMatrix(math::float3 direction, math::float3 up, - math::float3 position = {}) noexcept; + static math::mat4f getDirectionalLightViewMatrix( + math::float3 direction, math::float3 position = {}) noexcept; static math::mat4f getPointLightViewMatrix(backend::TextureCubemapFace face, math::float3 position) noexcept; @@ -246,15 +246,16 @@ private: static inline math::mat4f computeLightRotation(math::float3 const& lsDirection) noexcept; - static inline math::float4 computeFocusParams( - math::mat4f const& LMpMv, - math::mat4f const& WLMp, + 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, - float shadowFar, bool stable) noexcept; + uint16_t shadowDimension, bool stable) noexcept; static inline void snapLightFrustum(math::float2& s, math::float2& o, - math::double2 lsRef, math::int2 resolution) noexcept; + math::mat4f const& Mv, math::double3 wsSnapCoords, math::int2 resolution) noexcept; static inline Aabb computeLightFrustumBounds(const math::mat4f& lightView, Aabb const& wsShadowReceiversVolume, Aabb const& wsShadowCastersVolume, diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index 73a51b16c5..30880c96a9 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -527,8 +527,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps(FEng // 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)); + const mat4f MvAtOrigin = ShadowMap::getDirectionalLightViewMatrix(direction); // Compute scene-dependent values shared across all cascades ShadowMap::updateSceneInfoDirectional(MvAtOrigin, *scene, sceneInfo); @@ -696,7 +695,7 @@ void ShadowMapManager::prepareSpotShadowMap(ShadowMap& shadowMap, const auto outerConeAngle = lcm.getSpotLightOuterCone(li); // compute shadow map frustum for culling - const mat4f Mv = ShadowMap::getDirectionalLightViewMatrix(direction, { 0, 1, 0 }, position); + const mat4f Mv = ShadowMap::getDirectionalLightViewMatrix(direction, 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); diff --git a/filament/src/details/Camera.cpp b/filament/src/details/Camera.cpp index 88bdb246e8..3ea6c5b3bc 100644 --- a/filament/src/details/Camera.cpp +++ b/filament/src/details/Camera.cpp @@ -266,8 +266,8 @@ CameraInfo::CameraInfo(FCamera const& camera) noexcept { d = std::max(zn, camera.getFocusDistance()); } -CameraInfo::CameraInfo(FCamera const& camera, math::mat4 const& inWorldTransform) noexcept { - const mat4 modelMatrix{ inWorldTransform * camera.getModelMatrix() }; +CameraInfo::CameraInfo(FCamera const& camera, const math::mat4& worldOriginCamera) noexcept { + const mat4 modelMatrix{ worldOriginCamera * camera.getModelMatrix() }; for (uint8_t i = 0; i < CONFIG_STEREOSCOPIC_EYES; i++) { eyeProjection[i] = mat4f{ camera.getProjectionMatrix(i) }; eyeFromView[i] = mat4f{ camera.getEyeFromViewMatrix(i) }; @@ -275,7 +275,7 @@ CameraInfo::CameraInfo(FCamera const& camera, math::mat4 const& inWorldTransform cullingProjection = mat4f{ camera.getCullingProjectionMatrix() }; model = mat4f{ modelMatrix }; view = mat4f{ inverse(modelMatrix) }; - worldTransform = inWorldTransform; + worldOrigin = worldOriginCamera; zn = (float)camera.getNear(); zf = (float)camera.getCullingFar(); ev100 = Exposure::ev100(camera); diff --git a/filament/src/details/Camera.h b/filament/src/details/Camera.h index bab9f11326..26e1297c56 100644 --- a/filament/src/details/Camera.h +++ b/filament/src/details/Camera.h @@ -223,7 +223,7 @@ private: struct CameraInfo { CameraInfo() noexcept {} explicit CameraInfo(FCamera const& camera) noexcept; - CameraInfo(FCamera const& camera, math::mat4 const& inWorldTransform) noexcept; + CameraInfo(FCamera const& camera, const math::mat4& worldOriginCamera) noexcept; union { // projection matrix for drawing (infinite zfar) @@ -239,7 +239,7 @@ struct CameraInfo { math::mat4f model; // camera model matrix math::mat4f view; // camera view matrix (inverse(model)) math::mat4f eyeFromView[CONFIG_STEREOSCOPIC_EYES]; // eye view matrix (only for stereoscopic) - math::mat4 worldTransform; // world transform (already applied + math::mat4 worldOrigin; // world origin transform (already applied // to model and view) math::float4 clipTransform{1, 1, 0, 0}; // clip-space transform, only for VERTEX_DOMAIN_DEVICE float zn{}; // distance (positive) to the near plane @@ -250,7 +250,7 @@ struct CameraInfo { float d{}; // focus distance [m] math::float3 const& getPosition() const noexcept { return model[3].xyz; } math::float3 getForwardVector() const noexcept { return normalize(-model[2].xyz); } - math::mat4 getUserViewMatrix() const noexcept { return view * worldTransform; } + math::mat4 getUserViewMatrix() const noexcept { return view * worldOrigin; } }; FILAMENT_DOWNCAST(Camera) diff --git a/filament/src/details/Scene.cpp b/filament/src/details/Scene.cpp index 2d2a99746b..bc2ce1d264 100644 --- a/filament/src/details/Scene.cpp +++ b/filament/src/details/Scene.cpp @@ -33,8 +33,6 @@ #include #include -#include - #include using namespace filament::backend; @@ -54,7 +52,7 @@ FScene::~FScene() noexcept = default; void FScene::prepare(utils::JobSystem& js, LinearAllocatorArena& allocator, - mat4 const& worldTransform, + const mat4& worldOriginTransform, bool shadowReceiversAreCasters) noexcept { // TODO: can we skip this in most cases? Since we rely on indices staying the same, // we could only skip, if nothing changed in the RCM. @@ -170,7 +168,7 @@ void FScene::prepare(utils::JobSystem& js, * Fill the SoA with the JobSystem */ - auto renderableWork = [first = renderableInstances.data(), &rcm, &tcm, &worldTransform, + auto renderableWork = [first = renderableInstances.data(), &rcm, &tcm, &worldOriginTransform, &sceneData, shadowReceiversAreCasters](auto* p, auto c) { SYSTRACE_NAME("renderableWork"); @@ -178,12 +176,12 @@ void FScene::prepare(utils::JobSystem& js, auto [ri, ti] = p[i]; // this is where we go from double to float for our transforms - const mat4f shaderWorldTransform{ - worldTransform * tcm.getWorldTransformAccurate(ti) }; - const bool reversedWindingOrder = det(shaderWorldTransform.upperLeft()) < 0; + const mat4f worldTransform{ + worldOriginTransform * tcm.getWorldTransformAccurate(ti) }; + const bool reversedWindingOrder = det(worldTransform.upperLeft()) < 0; // compute the world AABB so we can perform culling - const Box worldAABB = rigidTransform(rcm.getAABB(ri), shaderWorldTransform); + const Box worldAABB = rigidTransform(rcm.getAABB(ri), worldTransform); auto visibility = rcm.getVisibility(ri); visibility.reversedWindingOrder = reversedWindingOrder; @@ -201,7 +199,7 @@ void FScene::prepare(utils::JobSystem& js, assert_invariant(index < sceneData.size()); sceneData.elementAt(index) = ri; - sceneData.elementAt(index) = shaderWorldTransform; + sceneData.elementAt(index) = worldTransform; sceneData.elementAt(index) = visibility; sceneData.elementAt(index) = rcm.getSkinningBufferInfo(ri); sceneData.elementAt(index) = rcm.getMorphingBufferInfo(ri); @@ -218,20 +216,19 @@ void FScene::prepare(utils::JobSystem& js, } }; - auto lightWork = [first = lightInstances.data(), &lcm, &tcm, &worldTransform, + auto lightWork = [first = lightInstances.data(), &lcm, &tcm, &worldOriginTransform, &lightData](auto* p, auto c) { SYSTRACE_NAME("lightWork"); for (size_t i = 0; i < c; i++) { auto [li, ti] = p[i]; // this is where we go from double to float for our transforms - mat4f const shaderWorldTransform{ - worldTransform * tcm.getWorldTransformAccurate(ti) }; - float4 const position = shaderWorldTransform * float4{ lcm.getLocalPosition(li), 1 }; + const mat4f worldTransform{ worldOriginTransform * tcm.getWorldTransformAccurate(ti) }; + const float4 position = worldTransform * float4{ lcm.getLocalPosition(li), 1 }; float3 d = 0; if (!lcm.isPointLight(li) || lcm.isIESLight(li)) { d = lcm.getLocalDirection(li); // using mat3f::getTransformForNormals handles non-uniform scaling - d = normalize(mat3f::getTransformForNormals(shaderWorldTransform.upperLeft()) * d); + d = normalize(mat3f::getTransformForNormals(worldTransform.upperLeft()) * d); } size_t const index = DIRECTIONAL_LIGHTS_COUNT + std::distance(first, p) + i; assert_invariant(index < lightData.size()); @@ -264,43 +261,14 @@ void FScene::prepare(utils::JobSystem& js, */ if (auto [li, ti] = directionalLightInstances ; li) { - // in the code below, we only transform directions, so the translation of the - // world transform is irrelevant, and we don't need to use getWorldTransformAccurate() - - FLightManager::ShadowParams const params = lcm.getShadowParams(li); - float3 const localDirection = lcm.getLocalDirection(li); - float3 const shadowLocalDirection = params.options.transform * localDirection; - mat3 const worldDirectionTransform = tcm.getWorldTransformAccurate(ti).upperLeft(); - mat3 const shaderWorldTransform = worldTransform.upperLeft() * worldDirectionTransform; - - // using mat3::getTransformForNormals handles non-uniform scaling - // note: in the common case of the rigid-body transform, getTransformForNormals() returns - // identity. - mat3 const worlTransformNormals = mat3::getTransformForNormals(shaderWorldTransform); - double3 const d = worlTransformNormals * localDirection; - double3 const s = worlTransformNormals * shadowLocalDirection; - - // We compute the reference point for snapping shadowmaps without applying the - // rotation of `worldOriginTransform` on both sides, so that we don't have any instability - // due to the limited precision of the "light space" matrix (even at double precision). - - // getMv() Returns the world-to-lightspace transformation. See ShadowMap.cpp. - auto getMv = [](double3 direction) -> mat3 { - // We use the x-axis as the "up" reference so that the math is stable when the light - // is pointing down, which is a common case for lights. See ShadowMap.cpp. - return transpose(mat3::lookTo(direction, double3{ 1, 0, 0 })); - }; - double3 const worldDirection = - mat3::getTransformForNormals(worldDirectionTransform) * shadowLocalDirection; - double3 const worldOrigin = transpose(worldTransform.upperLeft()) * worldTransform[3].xyz; - mat3 const Mv = getMv(worldDirection); - double2 const lsReferencePoint = (Mv * worldOrigin).xy; - + const mat4f worldTransform{ + worldOriginTransform * tcm.getWorldTransformAccurate(ti) }; + // using mat3f::getTransformForNormals handles non-uniform scaling + float3 d = lcm.getLocalDirection(li); + d = normalize(mat3f::getTransformForNormals(worldTransform.upperLeft()) * d); constexpr float inf = std::numeric_limits::infinity(); lightData.elementAt(0) = float4{ 0, 0, 0, inf }; - lightData.elementAt(0) = normalize(d); - lightData.elementAt(0) = normalize(s); - lightData.elementAt(0) = lsReferencePoint; + lightData.elementAt(0) = d; lightData.elementAt(0) = li; } else { lightData.elementAt(0) = 0; diff --git a/filament/src/details/Scene.h b/filament/src/details/Scene.h index 1882bb4dc3..9009861762 100644 --- a/filament/src/details/Scene.h +++ b/filament/src/details/Scene.h @@ -71,7 +71,7 @@ public: void terminate(FEngine& engine); void prepare(utils::JobSystem& js, LinearAllocatorArena& allocator, - math::mat4 const& worldTransform, bool shadowReceiversAreCasters) noexcept; + math::mat4 const& worldOriginTransform, bool shadowReceiversAreCasters) noexcept; void prepareVisibleRenderables(utils::Range visibleRenderables) noexcept; @@ -162,8 +162,6 @@ public: enum { POSITION_RADIUS, DIRECTION, - SHADOW_DIRECTION, - SHADOW_REF, LIGHT_INSTANCE, VISIBILITY, SCREEN_SPACE_Z_RANGE, @@ -173,8 +171,6 @@ public: using LightSoa = utils::StructureOfArrays< math::float4, math::float3, - math::float3, - math::double2, FLightManager::Instance, Culler::result_type, math::float2, diff --git a/filament/src/details/View.cpp b/filament/src/details/View.cpp index b02d50c5c7..271c39a710 100644 --- a/filament/src/details/View.cpp +++ b/filament/src/details/View.cpp @@ -397,8 +397,8 @@ CameraInfo FView::computeCameraInfo(FEngine& engine) const noexcept { * The "world origin" is also used to keep the origin close to the camera position to * improve fp precision in the shader for large scenes. */ - double3 translation; - mat3 rotation; + mat4 translation; + mat4 rotation; /* * Calculate all camera parameters needed to render this View for this frame. @@ -409,18 +409,16 @@ CameraInfo FView::computeCameraInfo(FEngine& engine) const noexcept { // view-space, which improves floating point precision in the shader by staying around // zero, where fp precision is highest. This also ensures that when the camera is placed // very far from the origin, objects are still rendered and lit properly. - translation = -camera->getPosition(); + translation = mat4::translation( -camera->getPosition() ); } FIndirectLight const* const ibl = scene->getIndirectLight(); if (ibl) { // the IBL transformation must be a rigid transform - rotation = mat3{ transpose(scene->getIndirectLight()->getRotation()) }; - // it is important to orthogonalize the matrix when converting it to doubles, because - // as float, it only has about a 1e-8 precision on the size of the basis vectors - rotation = orthogonalize(rotation); + rotation = mat4{ transpose(scene->getIndirectLight()->getRotation()) }; } - return { *camera, mat4{ rotation } * mat4::translation(translation) }; + + return { *camera, rotation * translation }; } void FView::prepare(FEngine& engine, DriverApi& driver, ArenaScope& arena, @@ -448,7 +446,7 @@ void FView::prepare(FEngine& engine, DriverApi& driver, ArenaScope& arena, // intent of the code, which is that we should only depend on CameraInfo here. // This is an extremely uncommon case. const mat4 projection = mCullingCamera->getCullingProjectionMatrix(); - const mat4 view = inverse(cameraInfo.worldTransform * mCullingCamera->getModelMatrix()); + const mat4 view = inverse(cameraInfo.worldOrigin * mCullingCamera->getModelMatrix()); return Frustum{ mat4f{ projection * view }}; } }; @@ -461,9 +459,7 @@ void FView::prepare(FEngine& engine, DriverApi& driver, ArenaScope& arena, * Gather all information needed to render this scene. Apply the world origin to all * objects in the scene. */ - scene->prepare(js, arena.getAllocator(), - cameraInfo.worldTransform, - hasVSM()); + scene->prepare(js, arena.getAllocator(), cameraInfo.worldOrigin, hasVSM()); /* * Light culling: runs in parallel with Renderable culling (below) diff --git a/filament/test/filament_test.cpp b/filament/test/filament_test.cpp index 5f47b0abee..8244197fd5 100644 --- a/filament/test/filament_test.cpp +++ b/filament/test/filament_test.cpp @@ -702,8 +702,8 @@ TEST(FilamentTest, FroxelData) { LightManager::Instance instance = engine->getLightManager().getInstance(e); FScene::LightSoa lights; - lights.push_back({}, {}, {}, {}, {}, {}, {}, {}); // first one is always skipped - lights.push_back(float4{ 0, 0, -5, 1 }, {}, {}, {}, instance, 1, {}, {}); + lights.push_back({}, {}, {}, {}, {}, {}); // first one is always skipped + lights.push_back(float4{ 0, 0, -5, 1 }, {}, instance, 1, {}, {}); { froxelData.froxelizeLights(*engine, {}, lights); diff --git a/libs/math/include/math/mat3.h b/libs/math/include/math/mat3.h index 5ad06bdf4b..8ebb93df43 100644 --- a/libs/math/include/math/mat3.h +++ b/libs/math/include/math/mat3.h @@ -289,14 +289,6 @@ public: return matrix::cof(m); } - /* - * Returns a matrix representing the pose of a virtual camera looking towards -Z in its - * local Y-up coordinate system. "up" defines where the Y axis of the camera's local coordinate - * system is. - */ - template - static TMat33 lookTo(const TVec3& direction, const TVec3& up) noexcept; - /** * Packs the tangent frame represented by the specified matrix into a quaternion. * Reflection is preserved by encoding it as the sign of the w component in the @@ -414,29 +406,6 @@ constexpr TMat33::TMat33(const TQuaternion& q) noexcept : m_value{} { m_value[2] = col_type(xz + yw, yz - xw, 1 - xx - yy); // NOLINT } -template -constexpr T dot_tolerance() noexcept; - -template<> -constexpr float dot_tolerance() noexcept { return 0.999f; } - -template<> -constexpr double dot_tolerance() noexcept { return 0.9999; } - -template -template -TMat33 TMat33::lookTo(const TVec3& direction, const TVec3& up) noexcept { - auto const z_axis = direction; - auto norm_up = up; - if (std::abs(dot(z_axis, norm_up)) > dot_tolerance< arithmetic_result_t >()) { - // Fix up vector if we're degenerate (looking straight up, basically) - norm_up = { norm_up.z, norm_up.x, norm_up.y }; - } - auto const x_axis = normalize(cross(z_axis, norm_up)); - auto const y_axis = cross(x_axis, z_axis); - return { x_axis, y_axis, -z_axis }; -} - //------------------------------------------------------------------------------ template constexpr TQuaternion TMat33::packTangentFrame(const TMat33& m, size_t storageSize) noexcept { diff --git a/libs/math/include/math/mat4.h b/libs/math/include/math/mat4.h index 570585397c..d44081b264 100644 --- a/libs/math/include/math/mat4.h +++ b/libs/math/include/math/mat4.h @@ -285,9 +285,6 @@ public: template static TMat44 lookAt(const TVec3& eye, const TVec3& center, const TVec3& up) noexcept; - template - static TMat44 lookTo(const TVec3& direction, const TVec3& position, const TVec3& up) noexcept; - template static constexpr TVec3 project(const TMat44& projectionMatrix, TVec3 vertice) noexcept{ TVec4 r = projectionMatrix * TVec4{ vertice, 1 }; @@ -520,19 +517,19 @@ template template TMat44 TMat44::lookAt(const TVec3& eye, const TVec3& center, const TVec3& up) noexcept { - return lookTo(normalize(center - eye), eye, normalize(up)); -} - -template -template -TMat44 TMat44::lookTo(const TVec3& direction, const TVec3& position, - const TVec3& up) noexcept { - auto r = TMat33::lookTo(direction, up); - return TMat44{ - TVec4{ r[0], 0 }, - TVec4{ r[1], 0 }, - TVec4{ r[2], 0 }, - TVec4{ position, 1 } }; + TVec3 z_axis(normalize(center - eye)); + TVec3 norm_up(normalize(up)); + if (std::abs(dot(z_axis, norm_up)) > T(0.999)) { + // Fix up vector if we're degenerate (looking straight up, basically) + norm_up = { norm_up.z, norm_up.x, norm_up.y }; + } + TVec3 x_axis(normalize(cross(z_axis, norm_up))); + TVec3 y_axis(cross(x_axis, z_axis)); + return TMat44( + TVec4(x_axis, 0), + TVec4(y_axis, 0), + TVec4(-z_axis, 0), + TVec4(eye, 1)); } // ---------------------------------------------------------------------------------------- diff --git a/libs/math/tests/test_mat.cpp b/libs/math/tests/test_mat.cpp index 047a5629cd..d53e73a916 100644 --- a/libs/math/tests/test_mat.cpp +++ b/libs/math/tests/test_mat.cpp @@ -32,71 +32,6 @@ class MatTest : public testing::Test { protected: }; -//------------------------------------------------------------------------------ -// A macro to help with vector comparisons within floating point range. -#define EXPECT_VEC_EQ(VEC1, VEC2) \ -do { \ - const decltype(VEC1) v1 = VEC1; \ - const decltype(VEC2) v2 = VEC2; \ - if (std::is_same::value) { \ - for (int i = 0; i < v1.size(); ++i) { \ - EXPECT_FLOAT_EQ(v1[i], v2[i]); \ - } \ - } else if (std::is_same::value) { \ - for (int i = 0; i < v1.size(); ++i) { \ - EXPECT_DOUBLE_EQ(v1[i], v2[i]); \ - } \ - } else { \ - for (int i = 0; i < v1.size(); ++i) { \ - EXPECT_EQ(v1[i], v2[i]); \ - } \ - } \ -} while(0) - -//------------------------------------------------------------------------------ -// A macro to help with vector comparisons within a range. -#define EXPECT_VEC_NEAR(VEC1, VEC2, eps) \ -do { \ - const decltype(VEC1) v1 = VEC1; \ - const decltype(VEC2) v2 = VEC2; \ - for (int i = 0; i < v1.size(); ++i) { \ - EXPECT_NEAR(v1[i], v2[i], eps); \ - } \ -} while(0) - - -//------------------------------------------------------------------------------ -// A macro to help with type comparisons within floating point range. -#define ASSERT_TYPE_EQ(T1, T2) \ -do { \ - const decltype(T1) t1 = T1; \ - const decltype(T2) t2 = T2; \ - if (std::is_same::value) { \ - ASSERT_FLOAT_EQ(t1, t2); \ - } else if (std::is_same::value) { \ - ASSERT_DOUBLE_EQ(t1, t2); \ - } else { \ - ASSERT_EQ(t1, t2); \ - } \ -} while(0) - - - -TEST_F(MatTest, LargeFloatRotationsWithOrthogonalization) { - double3 const t = { 2304097.1410110965, -4688442.9915525438, -3639452.5611694567 }; - mat4 const T = mat4::translation(t); - for (float d = 0; d < 90; d = d + 1.0) { - mat3f const R = mat3f::rotation(d * f::DEG_TO_RAD, float3{ 0, 1, 0 }); - mat3 RR = orthogonalize(mat3{ R }); - ASSERT_NEAR(dot(RR[0], RR[0]), 1.0, 1e-12); - ASSERT_NEAR(dot(RR[1], RR[1]), 1.0, 1e-12); - ASSERT_NEAR(dot(RR[2], RR[2]), 1.0, 1e-12); - mat4 M = mat4{ RR } * T; - double3 const t2 = transpose(M.upperLeft()) * M[3].xyz; - EXPECT_VEC_NEAR(t, t2, 0.0001); // 0.1mm - } -} - TEST_F(MatTest, ConstexprMat2) { constexpr float a = F_PI; constexpr mat2f M; @@ -617,6 +552,53 @@ TYPED_TEST(MatTestT, Inverse2) { TEST_MATRIX_INVERSE(m4, 20.0 * std::numeric_limits::epsilon()); } +//------------------------------------------------------------------------------ +// A macro to help with vector comparisons within floating point range. +#define EXPECT_VEC_EQ(VEC1, VEC2) \ +do { \ + const decltype(VEC1) v1 = VEC1; \ + const decltype(VEC2) v2 = VEC2; \ + if (std::is_same::value) { \ + for (int i = 0; i < v1.size(); ++i) { \ + EXPECT_FLOAT_EQ(v1[i], v2[i]); \ + } \ + } else if (std::is_same::value) { \ + for (int i = 0; i < v1.size(); ++i) { \ + EXPECT_DOUBLE_EQ(v1[i], v2[i]); \ + } \ + } else { \ + for (int i = 0; i < v1.size(); ++i) { \ + EXPECT_EQ(v1[i], v2[i]); \ + } \ + } \ +} while(0) + +//------------------------------------------------------------------------------ +// A macro to help with vector comparisons within a range. +#define EXPECT_VEC_NEAR(VEC1, VEC2, eps) \ +do { \ + const decltype(VEC1) v1 = VEC1; \ + const decltype(VEC2) v2 = VEC2; \ + for (int i = 0; i < v1.size(); ++i) { \ + EXPECT_NEAR(v1[i], v2[i], eps); \ + } \ +} while(0) + + +//------------------------------------------------------------------------------ +// A macro to help with type comparisons within floating point range. +#define ASSERT_TYPE_EQ(T1, T2) \ +do { \ + const decltype(T1) t1 = T1; \ + const decltype(T2) t2 = T2; \ + if (std::is_same::value) { \ + ASSERT_FLOAT_EQ(t1, t2); \ + } else if (std::is_same::value) { \ + ASSERT_DOUBLE_EQ(t1, t2); \ + } else { \ + ASSERT_EQ(t1, t2); \ + } \ +} while(0) TYPED_TEST(MatTestT, NormalsNegativeScale) { typedef filament::math::details::TMat33 M33T; diff --git a/samples/gltf_viewer.cpp b/samples/gltf_viewer.cpp index cd46e0d76c..1fb250897e 100644 --- a/samples/gltf_viewer.cpp +++ b/samples/gltf_viewer.cpp @@ -101,7 +101,7 @@ struct App { bool actualSize = false; bool originIsFarAway = false; - float originDistance = 1.0f; + float originDistance = 6378137; // Earth's radius in [m] struct Scene { Entity groundPlane; @@ -762,7 +762,7 @@ 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, 1); + 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", @@ -981,12 +981,7 @@ int main(int argc, char** argv) { tcm.setParent(tcm.getInstance(camera.getEntity()), root); tcm.setParent(tcm.getInstance(app.asset->getRoot()), root); tcm.setParent(tcm.getInstance(view->getFogEntity()), root); - - // these values represent a point somewhere on Earth's surface - float const d = app.originIsFarAway ? app.originDistance : 0.0f; -// tcm.setTransform(root, mat4::translation(double3{ 67.0, -6366759.0, -21552.0 } * d)); - tcm.setTransform(root, mat4::translation( - double3{ 2304097.1410110965, -4688442.9915525438, -3639452.5611694567 } * d)); + tcm.setTransform(root, mat4f::translation(float3{ app.originIsFarAway ? app.originDistance : 0.0f })); // Check if color grading has changed. ColorGradingSettings const& options = app.viewer->getSettings().view.colorGrading;