diff --git a/engine/include/XCEngine/Rendering/CameraRenderRequest.h b/engine/include/XCEngine/Rendering/CameraRenderRequest.h index 570146ee..e0f26c27 100644 --- a/engine/include/XCEngine/Rendering/CameraRenderRequest.h +++ b/engine/include/XCEngine/Rendering/CameraRenderRequest.h @@ -38,6 +38,26 @@ struct ScenePassRenderRequest { using DepthOnlyRenderRequest = ScenePassRenderRequest; using ShadowCasterRenderRequest = ScenePassRenderRequest; +struct DirectionalShadowRenderPlan { + bool enabled = false; + Math::Vector3 lightDirection = Math::Vector3::Back(); + Math::Vector3 focusPoint = Math::Vector3::Zero(); + float orthographicHalfExtent = 0.0f; + float nearClipPlane = 0.1f; + float farClipPlane = 0.0f; + uint32_t mapWidth = 0; + uint32_t mapHeight = 0; + RenderCameraData cameraData = {}; + + bool IsValid() const { + return enabled && + mapWidth > 0 && + mapHeight > 0 && + cameraData.viewportWidth == mapWidth && + cameraData.viewportHeight == mapHeight; + } +}; + struct ObjectIdRenderRequest { RenderSurface surface; @@ -62,6 +82,7 @@ struct CameraRenderRequest { RenderSurface surface; DepthOnlyRenderRequest depthOnly; ShadowCasterRenderRequest shadowCaster; + DirectionalShadowRenderPlan directionalShadow; ObjectIdRenderRequest objectId; float cameraDepth = 0.0f; uint8_t cameraStackOrder = 0; diff --git a/engine/include/XCEngine/Rendering/RenderSceneExtractor.h b/engine/include/XCEngine/Rendering/RenderSceneExtractor.h index a795f4c9..28b9b739 100644 --- a/engine/include/XCEngine/Rendering/RenderSceneExtractor.h +++ b/engine/include/XCEngine/Rendering/RenderSceneExtractor.h @@ -18,6 +18,7 @@ namespace Rendering { struct RenderDirectionalLightData { bool enabled = false; + bool castsShadows = false; Math::Vector3 direction = Math::Vector3::Back(); float intensity = 1.0f; Math::Color color = Math::Color::White(); diff --git a/engine/src/Rendering/RenderSceneExtractor.cpp b/engine/src/Rendering/RenderSceneExtractor.cpp index e6fe213e..1b38332d 100644 --- a/engine/src/Rendering/RenderSceneExtractor.cpp +++ b/engine/src/Rendering/RenderSceneExtractor.cpp @@ -168,6 +168,7 @@ void RenderSceneExtractor::ExtractLighting( RenderDirectionalLightData lightData; lightData.enabled = true; + lightData.castsShadows = mainDirectionalLight->GetCastsShadows(); lightData.intensity = mainDirectionalLight->GetIntensity(); lightData.color = mainDirectionalLight->GetColor(); diff --git a/engine/src/Rendering/SceneRenderRequestPlanner.cpp b/engine/src/Rendering/SceneRenderRequestPlanner.cpp index 2536b20e..8a2558e8 100644 --- a/engine/src/Rendering/SceneRenderRequestPlanner.cpp +++ b/engine/src/Rendering/SceneRenderRequestPlanner.cpp @@ -1,5 +1,7 @@ #include "Rendering/SceneRenderRequestPlanner.h" +#include "Components/LightComponent.h" +#include "Core/Math/Matrix4.h" #include "Rendering/SceneRenderRequestUtils.h" #include "Scene/Scene.h" @@ -9,6 +11,139 @@ namespace XCEngine { namespace Rendering { +namespace { + +constexpr uint32_t kDirectionalShadowMapDimension = 1024; +constexpr float kMinShadowFocusDistance = 5.0f; +constexpr float kMaxShadowFocusDistance = 25.0f; +constexpr float kPerspectiveShadowFocusFactor = 0.25f; +constexpr float kOrthographicShadowFocusFactor = 2.0f; +constexpr float kMinShadowHalfExtent = 10.0f; +constexpr float kMaxShadowHalfExtent = 40.0f; +constexpr float kPerspectiveShadowCoverageFactor = 0.20f; +constexpr float kOrthographicShadowCoverageFactor = 2.0f; +constexpr float kShadowNearClipPlane = 0.1f; +constexpr float kMinShadowDepthRange = 20.0f; + +bool IsUsableDirectionalLight(const Components::LightComponent* light) { + return light != nullptr && + light->IsEnabled() && + light->GetGameObject() != nullptr && + light->GetGameObject()->IsActiveInHierarchy() && + light->GetLightType() == Components::LightType::Directional; +} + +Components::LightComponent* FindMainDirectionalLight(const Components::Scene& scene) { + const std::vector lights = + scene.FindObjectsOfType(); + + Components::LightComponent* mainDirectionalLight = nullptr; + for (Components::LightComponent* light : lights) { + if (!IsUsableDirectionalLight(light)) { + continue; + } + + if (mainDirectionalLight == nullptr || + light->GetIntensity() > mainDirectionalLight->GetIntensity()) { + mainDirectionalLight = light; + } + } + + return mainDirectionalLight; +} + +bool ShouldPlanDirectionalShadowForCamera( + const Components::CameraComponent& camera, + size_t renderedBaseCameraCount, + size_t renderedRequestCount) { + if (camera.GetStackType() == Components::CameraStackType::Overlay) { + return renderedBaseCameraCount == 0u && + renderedRequestCount == 0u; + } + + return true; +} + +DirectionalShadowRenderPlan BuildDirectionalShadowRenderPlan( + const Components::CameraComponent& camera, + const Components::LightComponent& light) { + DirectionalShadowRenderPlan plan = {}; + if (!light.GetCastsShadows()) { + return plan; + } + + Math::Vector3 lightDirection = light.transform().GetForward() * -1.0f; + if (lightDirection.SqrMagnitude() <= Math::EPSILON) { + lightDirection = Math::Vector3::Back(); + } else { + lightDirection = lightDirection.Normalized(); + } + + const Math::Vector3 viewForward = camera.transform().GetForward().SqrMagnitude() <= Math::EPSILON + ? Math::Vector3::Forward() + : camera.transform().GetForward().Normalized(); + const Math::Vector3 cameraPosition = camera.transform().GetPosition(); + + const float focusDistance = camera.GetProjectionType() == Components::CameraProjectionType::Perspective + ? std::clamp( + camera.GetFarClipPlane() * kPerspectiveShadowFocusFactor, + kMinShadowFocusDistance, + kMaxShadowFocusDistance) + : std::clamp( + camera.GetOrthographicSize() * kOrthographicShadowFocusFactor, + kMinShadowFocusDistance, + kMaxShadowFocusDistance); + const Math::Vector3 focusPoint = cameraPosition + viewForward * focusDistance; + + const float shadowHalfExtent = camera.GetProjectionType() == Components::CameraProjectionType::Perspective + ? std::clamp( + camera.GetFarClipPlane() * kPerspectiveShadowCoverageFactor, + kMinShadowHalfExtent, + kMaxShadowHalfExtent) + : std::clamp( + camera.GetOrthographicSize() * kOrthographicShadowCoverageFactor, + kMinShadowHalfExtent, + kMaxShadowHalfExtent); + const float shadowDepthRange = std::max(shadowHalfExtent * 4.0f, kMinShadowDepthRange); + const Math::Vector3 shadowWorldPosition = + focusPoint - lightDirection * (shadowDepthRange * 0.5f); + + Math::Vector3 shadowUp = Math::Vector3::Up(); + if (std::abs(Math::Vector3::Dot(lightDirection, shadowUp)) > 0.98f) { + shadowUp = Math::Vector3::Forward(); + } + + const Math::Matrix4x4 view = + Math::Matrix4x4::LookAt(shadowWorldPosition, focusPoint, shadowUp); + const Math::Matrix4x4 projection = Math::Matrix4x4::Orthographic( + -shadowHalfExtent, + shadowHalfExtent, + -shadowHalfExtent, + shadowHalfExtent, + kShadowNearClipPlane, + shadowDepthRange); + + plan.enabled = true; + plan.lightDirection = lightDirection; + plan.focusPoint = focusPoint; + plan.orthographicHalfExtent = shadowHalfExtent; + plan.nearClipPlane = kShadowNearClipPlane; + plan.farClipPlane = shadowDepthRange; + plan.mapWidth = kDirectionalShadowMapDimension; + plan.mapHeight = kDirectionalShadowMapDimension; + plan.cameraData.view = view.Transpose(); + plan.cameraData.projection = projection.Transpose(); + plan.cameraData.viewProjection = (projection * view).Transpose(); + plan.cameraData.worldPosition = shadowWorldPosition; + plan.cameraData.clearColor = Math::Color::Black(); + plan.cameraData.clearFlags = RenderClearFlags::Depth; + plan.cameraData.viewportWidth = plan.mapWidth; + plan.cameraData.viewportHeight = plan.mapHeight; + return plan; +} + +} // namespace + std::vector SceneRenderRequestPlanner::CollectCameras( const Components::Scene& scene, Components::CameraComponent* overrideCamera) const { @@ -56,6 +191,23 @@ std::vector SceneRenderRequestPlanner::BuildRequests( continue; } + if (ShouldPlanDirectionalShadowForCamera( + *camera, + renderedBaseCameraCount, + requests.size())) { + if (Components::LightComponent* mainDirectionalLight = FindMainDirectionalLight(scene); + mainDirectionalLight != nullptr && + mainDirectionalLight->GetCastsShadows()) { + request.directionalShadow = + BuildDirectionalShadowRenderPlan(*camera, *mainDirectionalLight); + if (request.directionalShadow.IsValid()) { + request.shadowCaster.clearFlags = RenderClearFlags::Depth; + request.shadowCaster.hasCameraDataOverride = true; + request.shadowCaster.cameraDataOverride = request.directionalShadow.cameraData; + } + } + } + requests.push_back(request); if (camera->GetStackType() == Components::CameraStackType::Base) { ++renderedBaseCameraCount; diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index 6c928430..bdc7af64 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -165,6 +165,7 @@ TEST(RenderSceneExtractor_Test, ExtractsBrightestDirectionalLightAsMainLight) { mainLight->SetLightType(LightType::Directional); mainLight->SetColor(Color(1.0f, 0.8f, 0.6f, 1.0f)); mainLight->SetIntensity(2.5f); + mainLight->SetCastsShadows(true); mainLightObject->GetTransform()->SetLocalRotation( Quaternion::LookRotation(Vector3(-0.3f, -1.0f, -0.2f).Normalized())); @@ -177,6 +178,7 @@ TEST(RenderSceneExtractor_Test, ExtractsBrightestDirectionalLightAsMainLight) { EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.r, 1.0f); EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.g, 0.8f); EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.b, 0.6f); + EXPECT_TRUE(sceneData.lighting.mainDirectionalLight.castsShadows); EXPECT_EQ( sceneData.lighting.mainDirectionalLight.direction, mainLightObject->GetTransform()->GetForward().Normalized() * -1.0f); @@ -357,7 +359,6 @@ TEST(RenderSceneExtractor_Test, FallsBackToEmbeddedMeshMaterialsWhenRendererHasN EXPECT_EQ(ResolveMaterial(sceneData.visibleItems[0]), embeddedMaterial); meshFilter->ClearMesh(); - delete embeddedMaterial; delete mesh; } diff --git a/tests/Rendering/unit/test_scene_render_request_planner.cpp b/tests/Rendering/unit/test_scene_render_request_planner.cpp index 5ef78310..db3d9764 100644 --- a/tests/Rendering/unit/test_scene_render_request_planner.cpp +++ b/tests/Rendering/unit/test_scene_render_request_planner.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -126,3 +127,76 @@ TEST(SceneRenderRequestPlanner_Test, BuildsRequestsAndDropsZeroSizedViewportsWit EXPECT_EQ(requests[1].camera, overlay); EXPECT_EQ(requests[1].clearFlags, RenderClearFlags::Depth); } + +TEST(SceneRenderRequestPlanner_Test, BuildsDirectionalShadowPlanForBaseCameraWhenMainDirectionalLightCastsShadows) { + Scene scene("SceneRenderRequestPlannerDirectionalShadow"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetStackType(CameraStackType::Base); + camera->SetDepth(1.0f); + + GameObject* shadowLightObject = scene.CreateGameObject("ShadowLight"); + auto* shadowLight = shadowLightObject->AddComponent(); + shadowLight->SetLightType(LightType::Directional); + shadowLight->SetIntensity(2.5f); + shadowLight->SetCastsShadows(true); + + SceneRenderRequestPlanner planner; + const std::vector requests = planner.BuildRequests( + scene, + nullptr, + CreateValidContext(), + RenderSurface(640, 360)); + + ASSERT_EQ(requests.size(), 1u); + const CameraRenderRequest& request = requests[0]; + EXPECT_EQ(request.camera, camera); + ASSERT_TRUE(request.directionalShadow.IsValid()); + EXPECT_TRUE(request.directionalShadow.enabled); + EXPECT_EQ(request.directionalShadow.mapWidth, 1024u); + EXPECT_EQ(request.directionalShadow.mapHeight, 1024u); + EXPECT_EQ(request.directionalShadow.lightDirection, XCEngine::Math::Vector3::Back()); + EXPECT_GT(request.directionalShadow.focusPoint.z, 0.0f); + EXPECT_TRUE(request.shadowCaster.hasCameraDataOverride); + EXPECT_EQ(request.shadowCaster.clearFlags, RenderClearFlags::Depth); + EXPECT_EQ(request.shadowCaster.cameraDataOverride.viewportWidth, 1024u); + EXPECT_EQ(request.shadowCaster.cameraDataOverride.viewportHeight, 1024u); + EXPECT_EQ(request.shadowCaster.cameraDataOverride.clearFlags, RenderClearFlags::Depth); +} + +TEST(SceneRenderRequestPlanner_Test, SkipsDirectionalShadowPlanForOverlayCameraWhenBaseCameraExists) { + Scene scene("SceneRenderRequestPlannerOverlayShadow"); + + GameObject* baseCameraObject = scene.CreateGameObject("BaseCamera"); + auto* baseCamera = baseCameraObject->AddComponent(); + baseCamera->SetStackType(CameraStackType::Base); + baseCamera->SetDepth(1.0f); + + GameObject* overlayCameraObject = scene.CreateGameObject("OverlayCamera"); + auto* overlayCamera = overlayCameraObject->AddComponent(); + overlayCamera->SetStackType(CameraStackType::Overlay); + overlayCamera->SetDepth(2.0f); + + GameObject* shadowLightObject = scene.CreateGameObject("ShadowLight"); + auto* shadowLight = shadowLightObject->AddComponent(); + shadowLight->SetLightType(LightType::Directional); + shadowLight->SetIntensity(3.0f); + shadowLight->SetCastsShadows(true); + + SceneRenderRequestPlanner planner; + const std::vector requests = planner.BuildRequests( + scene, + nullptr, + CreateValidContext(), + RenderSurface(640, 360)); + + ASSERT_EQ(requests.size(), 2u); + EXPECT_EQ(requests[0].camera, baseCamera); + EXPECT_TRUE(requests[0].directionalShadow.enabled); + EXPECT_TRUE(requests[0].shadowCaster.hasCameraDataOverride); + + EXPECT_EQ(requests[1].camera, overlayCamera); + EXPECT_FALSE(requests[1].directionalShadow.enabled); + EXPECT_FALSE(requests[1].shadowCaster.hasCameraDataOverride); +}