#define NOMINMAX #include #include #include "../RenderingIntegrationMain.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Debug; using namespace XCEngine::Math; using namespace XCEngine::Rendering; using namespace XCEngine::Resources; using namespace XCEngine::RHI; using namespace XCEngine::RHI::Integration; namespace { constexpr const char* kD3D12Screenshot = "directional_shadow_scene_d3d12.ppm"; constexpr const char* kOpenGLScreenshot = "directional_shadow_scene_opengl.ppm"; constexpr const char* kVulkanScreenshot = "directional_shadow_scene_vulkan.ppm"; constexpr const char* kD3D12ShadowMapScreenshot = "directional_shadow_map_d3d12.ppm"; constexpr const char* kOpenGLShadowMapScreenshot = "directional_shadow_map_opengl.ppm"; constexpr const char* kVulkanShadowMapScreenshot = "directional_shadow_map_vulkan.ppm"; constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; const char kShadowMapDebugHlsl[] = R"( Texture2D gShadowMap : register(t0); SamplerState gShadowSampler : register(s0); struct VSOutput { float4 position : SV_POSITION; float2 uv : TEXCOORD0; }; VSOutput MainVS(uint vertexId : SV_VertexID) { VSOutput output; const float2 positions[3] = { float2(-1.0f, -1.0f), float2(-1.0f, 3.0f), float2( 3.0f, -1.0f) }; const float2 uvs[3] = { float2(0.0f, 0.0f), float2(0.0f, 2.0f), float2(2.0f, 0.0f) }; output.position = float4(positions[vertexId], 0.0f, 1.0f); output.uv = uvs[vertexId]; return output; } float4 MainPS(VSOutput input) : SV_TARGET { const float depth = gShadowMap.Sample(gShadowSampler, input.uv); return float4(depth, depth, depth, 1.0f); } )"; const char kShadowMapDebugVertexShader[] = R"(#version 430 out vec2 vTexCoord; void main() { const vec2 positions[3] = vec2[]( vec2(-1.0, -1.0), vec2(-1.0, 3.0), vec2( 3.0, -1.0) ); const vec2 uvs[3] = vec2[]( vec2(0.0, 0.0), vec2(0.0, 2.0), vec2(2.0, 0.0) ); gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0); vTexCoord = uvs[gl_VertexID]; } )"; const char kShadowMapDebugFragmentShader[] = R"(#version 430 layout(binding = 0) uniform sampler2D uShadowMap; in vec2 vTexCoord; layout(location = 0) out vec4 fragColor; void main() { const float depth = texture(uShadowMap, vTexCoord).r; fragColor = vec4(depth, depth, depth, 1.0); } )"; void AppendQuadFace( std::vector& vertices, std::vector& indices, const Vector3& v0, const Vector3& v1, const Vector3& v2, const Vector3& v3, const Vector3& normal) { const uint32_t baseIndex = static_cast(vertices.size()); StaticMeshVertex vertex = {}; vertex.normal = normal; vertex.position = v0; vertex.uv0 = Vector2(0.0f, 1.0f); vertices.push_back(vertex); vertex.position = v1; vertex.uv0 = Vector2(1.0f, 1.0f); vertices.push_back(vertex); vertex.position = v2; vertex.uv0 = Vector2(0.0f, 0.0f); vertices.push_back(vertex); vertex.position = v3; vertex.uv0 = Vector2(1.0f, 0.0f); vertices.push_back(vertex); indices.push_back(baseIndex + 0); indices.push_back(baseIndex + 2); indices.push_back(baseIndex + 1); indices.push_back(baseIndex + 1); indices.push_back(baseIndex + 2); indices.push_back(baseIndex + 3); } Mesh* CreateGroundMesh() { auto* mesh = new Mesh(); IResource::ConstructParams params = {}; params.name = "DirectionalShadowGroundMesh"; params.path = "Tests/Rendering/DirectionalShadowGround.mesh"; params.guid = ResourceGUID::Generate(params.path); mesh->Initialize(params); StaticMeshVertex vertices[4] = {}; vertices[0].position = Vector3(-12.0f, 0.0f, 1.0f); vertices[0].normal = Vector3::Up(); vertices[0].uv0 = Vector2(0.0f, 1.0f); vertices[1].position = Vector3(-12.0f, 0.0f, 20.0f); vertices[1].normal = Vector3::Up(); vertices[1].uv0 = Vector2(0.0f, 0.0f); vertices[2].position = Vector3(12.0f, 0.0f, 1.0f); vertices[2].normal = Vector3::Up(); vertices[2].uv0 = Vector2(1.0f, 1.0f); vertices[3].position = Vector3(12.0f, 0.0f, 20.0f); vertices[3].normal = Vector3::Up(); vertices[3].uv0 = Vector2(1.0f, 0.0f); const uint32_t indices[6] = { 0, 1, 2, 2, 1, 3 }; mesh->SetVertexData( vertices, sizeof(vertices), 4, sizeof(StaticMeshVertex), VertexAttribute::Position | VertexAttribute::Normal | VertexAttribute::UV0); mesh->SetIndexData(indices, sizeof(indices), 6, true); const Bounds bounds(Vector3(0.0f, 0.0f, 10.5f), Vector3(24.0f, 0.1f, 19.0f)); mesh->SetBounds(bounds); MeshSection section = {}; section.baseVertex = 0; section.vertexCount = 4; section.startIndex = 0; section.indexCount = 6; section.materialID = 0; section.bounds = bounds; mesh->AddSection(section); return mesh; } Mesh* CreateCubeMesh() { auto* mesh = new Mesh(); IResource::ConstructParams params = {}; params.name = "DirectionalShadowCubeMesh"; params.path = "Tests/Rendering/DirectionalShadowCube.mesh"; params.guid = ResourceGUID::Generate(params.path); mesh->Initialize(params); std::vector vertices; std::vector indices; vertices.reserve(24); indices.reserve(36); constexpr float half = 0.5f; AppendQuadFace( vertices, indices, Vector3(-half, -half, half), Vector3(half, -half, half), Vector3(-half, half, half), Vector3(half, half, half), Vector3::Forward()); AppendQuadFace( vertices, indices, Vector3(half, -half, -half), Vector3(-half, -half, -half), Vector3(half, half, -half), Vector3(-half, half, -half), Vector3::Back()); AppendQuadFace( vertices, indices, Vector3(-half, -half, -half), Vector3(-half, -half, half), Vector3(-half, half, -half), Vector3(-half, half, half), Vector3::Left()); AppendQuadFace( vertices, indices, Vector3(half, -half, half), Vector3(half, -half, -half), Vector3(half, half, half), Vector3(half, half, -half), Vector3::Right()); AppendQuadFace( vertices, indices, Vector3(-half, half, half), Vector3(half, half, half), Vector3(-half, half, -half), Vector3(half, half, -half), Vector3::Up()); AppendQuadFace( vertices, indices, Vector3(-half, -half, -half), Vector3(half, -half, -half), Vector3(-half, -half, half), Vector3(half, -half, half), Vector3::Down()); mesh->SetVertexData( vertices.data(), vertices.size() * sizeof(StaticMeshVertex), static_cast(vertices.size()), sizeof(StaticMeshVertex), VertexAttribute::Position | VertexAttribute::Normal | VertexAttribute::UV0); mesh->SetIndexData( indices.data(), indices.size() * sizeof(uint32_t), static_cast(indices.size()), true); const Bounds bounds(Vector3::Zero(), Vector3::One()); mesh->SetBounds(bounds); MeshSection section = {}; section.baseVertex = 0; section.vertexCount = static_cast(vertices.size()); section.startIndex = 0; section.indexCount = static_cast(indices.size()); section.materialID = 0; section.bounds = bounds; mesh->AddSection(section); return mesh; } Material* CreateForwardLitMaterial( const char* name, const char* path, const Vector4& baseColor) { auto* material = new Material(); IResource::ConstructParams params = {}; params.name = name; params.path = path; params.guid = ResourceGUID::Generate(params.path); material->Initialize(params); material->SetShader(ResourceManager::Get().Load(GetBuiltinForwardLitShaderPath())); material->SetShaderPass("ForwardLit"); material->SetFloat4("_BaseColor", baseColor); return material; } const char* GetScreenshotFilename(RHIType backendType) { switch (backendType) { case RHIType::D3D12: return kD3D12Screenshot; case RHIType::Vulkan: return kVulkanScreenshot; case RHIType::OpenGL: default: return kOpenGLScreenshot; } } const char* GetShadowMapScreenshotFilename(RHIType backendType) { switch (backendType) { case RHIType::D3D12: return kD3D12ShadowMapScreenshot; case RHIType::Vulkan: return kVulkanShadowMapScreenshot; case RHIType::OpenGL: default: return kOpenGLShadowMapScreenshot; } } int GetComparisonThreshold(RHIType backendType) { return backendType == RHIType::D3D12 ? 10 : 10; } struct ShadowProjectionDebugPoint { Vector4 clip = Vector4::Zero(); Vector3 ndc = Vector3::Zero(); Vector2 uvFlipY = Vector2::Zero(); Vector2 uvNoFlipY = Vector2::Zero(); }; ShadowProjectionDebugPoint ProjectShadowPoint( const Matrix4x4& gpuWorldToShadowMatrix, const Vector3& worldPoint) { ShadowProjectionDebugPoint result = {}; const Matrix4x4 worldToShadow = gpuWorldToShadowMatrix.Transpose(); result.clip = worldToShadow * Vector4(worldPoint.x, worldPoint.y, worldPoint.z, 1.0f); if (std::abs(result.clip.w) > EPSILON) { result.ndc = Vector3( result.clip.x / result.clip.w, result.clip.y / result.clip.w, result.clip.z / result.clip.w); result.uvFlipY = Vector2( result.ndc.x * 0.5f + 0.5f, result.ndc.y * -0.5f + 0.5f); result.uvNoFlipY = Vector2( result.ndc.x * 0.5f + 0.5f, result.ndc.y * 0.5f + 0.5f); } return result; } void LogShadowProjection( const char* label, const ShadowProjectionDebugPoint& point, uint32_t shadowMapWidth, uint32_t shadowMapHeight) { Log( "[TEST] ShadowProbe %s clip=(%.4f, %.4f, %.4f, %.4f) " "ndc=(%.4f, %.4f, %.4f) uvFlip=(%.4f, %.4f) uvNoFlip=(%.4f, %.4f) " "texelFlip=(%d, %d) texelNoFlip=(%d, %d)", label, point.clip.x, point.clip.y, point.clip.z, point.clip.w, point.ndc.x, point.ndc.y, point.ndc.z, point.uvFlipY.x, point.uvFlipY.y, point.uvNoFlipY.x, point.uvNoFlipY.y, static_cast(point.uvFlipY.x * static_cast(shadowMapWidth)), static_cast(point.uvFlipY.y * static_cast(shadowMapHeight)), static_cast(point.uvNoFlipY.x * static_cast(shadowMapWidth)), static_cast(point.uvNoFlipY.y * static_cast(shadowMapHeight))); } void LogShadowAlignmentAnalysis( const Scene& scene, const CameraRenderRequest& request) { if (!request.directionalShadow.IsValid()) { Log("[TEST] ShadowProbe skipped because directional shadow plan was invalid"); return; } const GameObject* casterObject = scene.Find("CasterCube"); const GameObject* groundObject = scene.Find("Ground"); if (casterObject == nullptr || groundObject == nullptr) { Log("[TEST] ShadowProbe skipped because test scene objects were missing"); return; } const Vector3 casterCenter = casterObject->GetTransform()->GetPosition(); const Vector3 casterScale = casterObject->GetTransform()->GetScale(); const Vector3 groundCenter = groundObject->GetTransform()->GetPosition(); const Vector3 groundScale = groundObject->GetTransform()->GetScale(); const float groundTopY = groundCenter.y + groundScale.y * 0.5f; const Vector3 lightDirectionToLight = request.directionalShadow.lightDirection.Normalized(); const Vector3 shadowRayDirection = lightDirectionToLight * -1.0f; if (std::abs(shadowRayDirection.y) <= EPSILON) { Log("[TEST] ShadowProbe skipped because shadow ray direction was parallel to the ground"); return; } const Vector3 casterBottomCenter = casterCenter - Vector3(0.0f, casterScale.y * 0.5f, 0.0f); const float centerTravel = (groundTopY - casterCenter.y) / shadowRayDirection.y; const float bottomTravel = (groundTopY - casterBottomCenter.y) / shadowRayDirection.y; const Vector3 receiverFromCenter = casterCenter + shadowRayDirection * centerTravel; const Vector3 receiverFromBottom = casterBottomCenter + shadowRayDirection * bottomTravel; const ShadowProjectionDebugPoint casterCenterProjection = ProjectShadowPoint(request.directionalShadow.cameraData.viewProjection, casterCenter); const ShadowProjectionDebugPoint casterBottomProjection = ProjectShadowPoint(request.directionalShadow.cameraData.viewProjection, casterBottomCenter); const ShadowProjectionDebugPoint receiverFromCenterProjection = ProjectShadowPoint(request.directionalShadow.cameraData.viewProjection, receiverFromCenter); const ShadowProjectionDebugPoint receiverFromBottomProjection = ProjectShadowPoint(request.directionalShadow.cameraData.viewProjection, receiverFromBottom); Log( "[TEST] ShadowProbe lightDirToLight=(%.4f, %.4f, %.4f) shadowRay=(%.4f, %.4f, %.4f) " "groundTopY=%.4f centerTravel=%.4f bottomTravel=%.4f", lightDirectionToLight.x, lightDirectionToLight.y, lightDirectionToLight.z, shadowRayDirection.x, shadowRayDirection.y, shadowRayDirection.z, groundTopY, centerTravel, bottomTravel); LogShadowProjection( "CasterCenter", casterCenterProjection, request.directionalShadow.mapWidth, request.directionalShadow.mapHeight); LogShadowProjection( "CasterBottom", casterBottomProjection, request.directionalShadow.mapWidth, request.directionalShadow.mapHeight); LogShadowProjection( "ReceiverFromCenter", receiverFromCenterProjection, request.directionalShadow.mapWidth, request.directionalShadow.mapHeight); LogShadowProjection( "ReceiverFromBottom", receiverFromBottomProjection, request.directionalShadow.mapWidth, request.directionalShadow.mapHeight); Log( "[TEST] ShadowProbe deltaUvFlip center=%.6f bottom=%.6f deltaUvNoFlip center=%.6f bottom=%.6f", (receiverFromCenterProjection.uvFlipY - casterCenterProjection.uvFlipY).Magnitude(), (receiverFromBottomProjection.uvFlipY - casterBottomProjection.uvFlipY).Magnitude(), (receiverFromCenterProjection.uvNoFlipY - casterCenterProjection.uvNoFlipY).Magnitude(), (receiverFromBottomProjection.uvNoFlipY - casterBottomProjection.uvNoFlipY).Magnitude()); } class ShadowMapDebugOverlayPass final : public RenderPass { public: ~ShadowMapDebugOverlayPass() override { DestroyResources(); } const char* GetName() const override { return "ShadowMapDebugOverlayPass"; } bool Initialize(const RenderContext& context) override { return EnsureInitialized(context); } void ReleaseResources() { DestroyResources(); } void Shutdown() override { Log("[TEST] ShadowMapDebugOverlayPass: Shutdown (deferred)"); } bool Execute(const RenderPassContext& context) override { Log("[TEST] ShadowMapDebugOverlayPass: Execute begin"); if (!context.renderContext.IsValid() || !context.sceneData.lighting.HasMainDirectionalShadow()) { Log("[TEST] ShadowMapDebugOverlayPass: invalid context or missing shadow"); return false; } const std::vector& colorAttachments = context.surface.GetColorAttachments(); if (colorAttachments.empty() || colorAttachments[0] == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: missing color attachment"); return false; } if (!EnsureInitialized(context.renderContext)) { Log("[TEST] ShadowMapDebugOverlayPass: EnsureInitialized failed"); return false; } RHIResourceView* shadowMap = context.sceneData.lighting.mainDirectionalShadow.shadowMap; if (shadowMap == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: shadowMap null"); return false; } Log("[TEST] ShadowMapDebugOverlayPass: updating descriptors"); mTextureSet->Update(0, shadowMap); RHICommandList* commandList = context.renderContext.commandList; RHIResourceView* renderTarget = colorAttachments[0]; const XCEngine::Math::RectInt renderArea = context.surface.GetRenderArea(); const XCEngine::RHI::ResourceStates restoredColorState = context.surface.GetColorStateAfter(); if (context.surface.IsAutoTransitionEnabled()) { Log("[TEST] ShadowMapDebugOverlayPass: transition color -> RT"); commandList->TransitionBarrier( renderTarget, restoredColorState, XCEngine::RHI::ResourceStates::RenderTarget); } Log("[TEST] ShadowMapDebugOverlayPass: transition shadow -> SRV"); commandList->TransitionBarrier( shadowMap, XCEngine::RHI::ResourceStates::DepthWrite, XCEngine::RHI::ResourceStates::PixelShaderResource); Log("[TEST] ShadowMapDebugOverlayPass: bind RT"); commandList->SetRenderTargets(1, &renderTarget, nullptr); const XCEngine::RHI::Viewport viewport = { static_cast(renderArea.x), static_cast(renderArea.y), static_cast(renderArea.width), static_cast(renderArea.height), 0.0f, 1.0f }; const XCEngine::RHI::Rect scissorRect = { renderArea.x, renderArea.y, renderArea.x + renderArea.width, renderArea.y + renderArea.height }; Log("[TEST] ShadowMapDebugOverlayPass: set viewport/scissor"); commandList->SetViewport(viewport); commandList->SetScissorRect(scissorRect); commandList->SetPrimitiveTopology(XCEngine::RHI::PrimitiveTopology::TriangleList); Log("[TEST] ShadowMapDebugOverlayPass: set pipeline"); commandList->SetPipelineState(mPipelineState); RHIDescriptorSet* descriptorSets[] = { mTextureSet, mSamplerSet }; Log("[TEST] ShadowMapDebugOverlayPass: bind descriptors"); commandList->SetGraphicsDescriptorSets(0, 2, descriptorSets, mPipelineLayout); Log("[TEST] ShadowMapDebugOverlayPass: draw"); commandList->Draw(3, 1, 0, 0); Log("[TEST] ShadowMapDebugOverlayPass: end render pass"); commandList->EndRenderPass(); Log("[TEST] ShadowMapDebugOverlayPass: transition shadow -> DepthWrite"); commandList->TransitionBarrier( shadowMap, XCEngine::RHI::ResourceStates::PixelShaderResource, XCEngine::RHI::ResourceStates::DepthWrite); if (context.surface.IsAutoTransitionEnabled()) { Log("[TEST] ShadowMapDebugOverlayPass: transition color -> restore"); commandList->TransitionBarrier( renderTarget, XCEngine::RHI::ResourceStates::RenderTarget, restoredColorState); } Log("[TEST] ShadowMapDebugOverlayPass: Execute end"); return true; } private: bool EnsureInitialized(const RenderContext& context) { if (mPipelineLayout != nullptr && mPipelineState != nullptr && mTexturePool != nullptr && mTextureSet != nullptr && mSamplerPool != nullptr && mSamplerSet != nullptr && mSampler != nullptr && mDevice == context.device && mBackendType == context.backendType) { return true; } DestroyResources(); return CreateResources(context); } bool CreateResources(const RenderContext& context) { Log("[TEST] ShadowMapDebugOverlayPass: CreateResources backend=%d", static_cast(context.backendType)); mDevice = context.device; mBackendType = context.backendType; SamplerDesc samplerDesc = {}; samplerDesc.filter = static_cast(FilterMode::Point); samplerDesc.addressU = static_cast(TextureAddressMode::Clamp); samplerDesc.addressV = static_cast(TextureAddressMode::Clamp); samplerDesc.addressW = static_cast(TextureAddressMode::Clamp); samplerDesc.comparisonFunc = static_cast(ComparisonFunc::Always); samplerDesc.maxAnisotropy = 1; samplerDesc.minLod = 0.0f; samplerDesc.maxLod = 1000.0f; mSampler = mDevice->CreateSampler(samplerDesc); if (mSampler == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: CreateSampler failed"); DestroyResources(); return false; } DescriptorSetLayoutBinding textureBinding = {}; textureBinding.binding = 0; textureBinding.type = static_cast(DescriptorType::SRV); textureBinding.count = 1; textureBinding.visibility = static_cast(ShaderVisibility::Pixel); DescriptorSetLayoutDesc textureLayout = {}; textureLayout.bindings = &textureBinding; textureLayout.bindingCount = 1; DescriptorPoolDesc texturePoolDesc = {}; texturePoolDesc.type = DescriptorHeapType::CBV_SRV_UAV; texturePoolDesc.descriptorCount = 1; texturePoolDesc.shaderVisible = true; mTexturePool = mDevice->CreateDescriptorPool(texturePoolDesc); if (mTexturePool == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: texture pool failed"); DestroyResources(); return false; } mTextureSet = mTexturePool->AllocateSet(textureLayout); if (mTextureSet == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: texture set failed"); DestroyResources(); return false; } DescriptorSetLayoutBinding samplerBinding = {}; samplerBinding.binding = 0; samplerBinding.type = static_cast(DescriptorType::Sampler); samplerBinding.count = 1; samplerBinding.visibility = static_cast(ShaderVisibility::Pixel); DescriptorSetLayoutDesc samplerLayout = {}; samplerLayout.bindings = &samplerBinding; samplerLayout.bindingCount = 1; DescriptorPoolDesc samplerPoolDesc = {}; samplerPoolDesc.type = DescriptorHeapType::Sampler; samplerPoolDesc.descriptorCount = 1; samplerPoolDesc.shaderVisible = true; mSamplerPool = mDevice->CreateDescriptorPool(samplerPoolDesc); if (mSamplerPool == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: sampler pool failed"); DestroyResources(); return false; } mSamplerSet = mSamplerPool->AllocateSet(samplerLayout); if (mSamplerSet == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: sampler set failed"); DestroyResources(); return false; } mSamplerSet->UpdateSampler(0, mSampler); DescriptorSetLayoutDesc setLayouts[] = { textureLayout, samplerLayout }; RHIPipelineLayoutDesc pipelineLayoutDesc = {}; pipelineLayoutDesc.setLayouts = setLayouts; pipelineLayoutDesc.setLayoutCount = 2; mPipelineLayout = mDevice->CreatePipelineLayout(pipelineLayoutDesc); if (mPipelineLayout == nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: pipeline layout failed"); DestroyResources(); return false; } GraphicsPipelineDesc pipelineDesc = {}; pipelineDesc.pipelineLayout = mPipelineLayout; pipelineDesc.topologyType = static_cast(PrimitiveTopologyType::Triangle); pipelineDesc.renderTargetCount = 1; pipelineDesc.renderTargetFormats[0] = static_cast(Format::R8G8B8A8_UNorm); pipelineDesc.depthStencilFormat = static_cast(Format::Unknown); pipelineDesc.sampleCount = 1; pipelineDesc.rasterizerState.fillMode = static_cast(FillMode::Solid); pipelineDesc.rasterizerState.cullMode = static_cast(CullMode::None); pipelineDesc.rasterizerState.frontFace = static_cast(FrontFace::CounterClockwise); pipelineDesc.rasterizerState.depthClipEnable = true; pipelineDesc.blendState.blendEnable = false; pipelineDesc.blendState.colorWriteMask = static_cast(ColorWriteMask::All); pipelineDesc.depthStencilState.depthTestEnable = false; pipelineDesc.depthStencilState.depthWriteEnable = false; pipelineDesc.depthStencilState.depthFunc = static_cast(ComparisonFunc::Always); if (mBackendType == RHIType::D3D12) { pipelineDesc.vertexShader.source.assign( kShadowMapDebugHlsl, kShadowMapDebugHlsl + strlen(kShadowMapDebugHlsl)); pipelineDesc.vertexShader.sourceLanguage = XCEngine::RHI::ShaderLanguage::HLSL; pipelineDesc.vertexShader.entryPoint = L"MainVS"; pipelineDesc.vertexShader.profile = L"vs_5_0"; pipelineDesc.fragmentShader.source.assign( kShadowMapDebugHlsl, kShadowMapDebugHlsl + strlen(kShadowMapDebugHlsl)); pipelineDesc.fragmentShader.sourceLanguage = XCEngine::RHI::ShaderLanguage::HLSL; pipelineDesc.fragmentShader.entryPoint = L"MainPS"; pipelineDesc.fragmentShader.profile = L"ps_5_0"; } else { pipelineDesc.vertexShader.source.assign( kShadowMapDebugVertexShader, kShadowMapDebugVertexShader + strlen(kShadowMapDebugVertexShader)); pipelineDesc.vertexShader.sourceLanguage = XCEngine::RHI::ShaderLanguage::GLSL; pipelineDesc.vertexShader.entryPoint = L"main"; if (mBackendType == RHIType::OpenGL) { pipelineDesc.vertexShader.profile = L"vs_4_30"; } pipelineDesc.fragmentShader.source.assign( kShadowMapDebugFragmentShader, kShadowMapDebugFragmentShader + strlen(kShadowMapDebugFragmentShader)); pipelineDesc.fragmentShader.sourceLanguage = XCEngine::RHI::ShaderLanguage::GLSL; pipelineDesc.fragmentShader.entryPoint = L"main"; if (mBackendType == RHIType::OpenGL) { pipelineDesc.fragmentShader.profile = L"fs_4_30"; } } mPipelineState = mDevice->CreatePipelineState(pipelineDesc); if (mPipelineState == nullptr || !mPipelineState->IsValid()) { Log("[TEST] ShadowMapDebugOverlayPass: pipeline state failed"); DestroyResources(); return false; } Log("[TEST] ShadowMapDebugOverlayPass: CreateResources success"); return true; } void DestroyResources() { Log("[TEST] ShadowMapDebugOverlayPass: DestroyResources begin"); if (mPipelineState != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy pipeline state"); mPipelineState->Shutdown(); delete mPipelineState; mPipelineState = nullptr; } if (mPipelineLayout != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy pipeline layout"); mPipelineLayout->Shutdown(); delete mPipelineLayout; mPipelineLayout = nullptr; } if (mTextureSet != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy texture set"); mTextureSet->Shutdown(); delete mTextureSet; mTextureSet = nullptr; } if (mTexturePool != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy texture pool"); mTexturePool->Shutdown(); delete mTexturePool; mTexturePool = nullptr; } if (mSamplerSet != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy sampler set"); mSamplerSet->Shutdown(); delete mSamplerSet; mSamplerSet = nullptr; } if (mSamplerPool != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy sampler pool"); mSamplerPool->Shutdown(); delete mSamplerPool; mSamplerPool = nullptr; } if (mSampler != nullptr) { Log("[TEST] ShadowMapDebugOverlayPass: destroy sampler"); mSampler->Shutdown(); delete mSampler; mSampler = nullptr; } mDevice = nullptr; mBackendType = RHIType::D3D12; Log("[TEST] ShadowMapDebugOverlayPass: DestroyResources end"); } RHIDevice* mDevice = nullptr; RHIType mBackendType = RHIType::D3D12; RHIPipelineLayout* mPipelineLayout = nullptr; RHIPipelineState* mPipelineState = nullptr; RHIDescriptorPool* mTexturePool = nullptr; RHIDescriptorSet* mTextureSet = nullptr; RHIDescriptorPool* mSamplerPool = nullptr; RHIDescriptorSet* mSamplerSet = nullptr; RHISampler* mSampler = nullptr; }; class DirectionalShadowSceneTest : public RHIIntegrationFixture { protected: void SetUp() override; void TearDown() override; void RenderFrame() override; void RenderSceneFrame(bool visualizeShadowMap); private: void BuildScene(); RHIResourceView* GetCurrentBackBufferView(); std::unique_ptr mScene; std::unique_ptr mSceneRenderer; RenderPassSequence mShadowMapDebugPasses; ShadowMapDebugOverlayPass* mShadowMapDebugOverlayPass = nullptr; bool mLoggedShadowProbe = false; std::vector mBackBufferViews; RHITexture* mDepthTexture = nullptr; RHIResourceView* mDepthView = nullptr; Mesh* mGroundMesh = nullptr; Mesh* mCubeMesh = nullptr; Material* mGroundMaterial = nullptr; Material* mCasterMaterial = nullptr; }; void DirectionalShadowSceneTest::SetUp() { RHIIntegrationFixture::SetUp(); mSceneRenderer = std::make_unique(); mScene = std::make_unique("DirectionalShadowScene"); mGroundMesh = CreateGroundMesh(); ASSERT_NE(mGroundMesh, nullptr); mCubeMesh = CreateCubeMesh(); ASSERT_NE(mCubeMesh, nullptr); mGroundMaterial = CreateForwardLitMaterial( "DirectionalShadowGround", "Tests/Rendering/DirectionalShadowGround.material", Vector4(0.92f, 0.93f, 0.96f, 1.0f)); mCasterMaterial = CreateForwardLitMaterial( "DirectionalShadowCaster", "Tests/Rendering/DirectionalShadowCaster.material", Vector4(0.72f, 0.22f, 0.16f, 1.0f)); auto shadowMapDebugPass = std::make_unique(); mShadowMapDebugOverlayPass = shadowMapDebugPass.get(); mShadowMapDebugPasses.AddPass(std::move(shadowMapDebugPass)); BuildScene(); TextureDesc depthDesc = {}; depthDesc.width = kFrameWidth; depthDesc.height = kFrameHeight; depthDesc.depth = 1; depthDesc.mipLevels = 1; depthDesc.arraySize = 1; depthDesc.format = static_cast(Format::D24_UNorm_S8_UInt); depthDesc.textureType = static_cast(XCEngine::RHI::TextureType::Texture2D); depthDesc.sampleCount = 1; depthDesc.sampleQuality = 0; depthDesc.flags = 0; mDepthTexture = GetDevice()->CreateTexture(depthDesc); ASSERT_NE(mDepthTexture, nullptr); ResourceViewDesc depthViewDesc = {}; depthViewDesc.format = static_cast(Format::D24_UNorm_S8_UInt); depthViewDesc.dimension = ResourceViewDimension::Texture2D; depthViewDesc.mipLevel = 0; mDepthView = GetDevice()->CreateDepthStencilView(mDepthTexture, depthViewDesc); ASSERT_NE(mDepthView, nullptr); mBackBufferViews.resize(2, nullptr); } void DirectionalShadowSceneTest::TearDown() { mSceneRenderer.reset(); if (mShadowMapDebugOverlayPass != nullptr) { mShadowMapDebugOverlayPass->ReleaseResources(); mShadowMapDebugOverlayPass = nullptr; } if (mDepthView != nullptr) { mDepthView->Shutdown(); delete mDepthView; mDepthView = nullptr; } if (mDepthTexture != nullptr) { mDepthTexture->Shutdown(); delete mDepthTexture; mDepthTexture = nullptr; } for (RHIResourceView*& backBufferView : mBackBufferViews) { if (backBufferView != nullptr) { backBufferView->Shutdown(); delete backBufferView; backBufferView = nullptr; } } mBackBufferViews.clear(); mScene.reset(); delete mGroundMaterial; mGroundMaterial = nullptr; delete mCasterMaterial; mCasterMaterial = nullptr; delete mGroundMesh; mGroundMesh = nullptr; delete mCubeMesh; mCubeMesh = nullptr; RHIIntegrationFixture::TearDown(); } void DirectionalShadowSceneTest::BuildScene() { ASSERT_NE(mScene, nullptr); ASSERT_NE(mGroundMesh, nullptr); ASSERT_NE(mCubeMesh, nullptr); ASSERT_NE(mGroundMaterial, nullptr); ASSERT_NE(mCasterMaterial, nullptr); GameObject* cameraObject = mScene->CreateGameObject("MainCamera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetFieldOfView(42.0f); camera->SetNearClipPlane(0.1f); camera->SetFarClipPlane(40.0f); camera->SetClearColor(XCEngine::Math::Color(0.03f, 0.03f, 0.05f, 1.0f)); cameraObject->GetTransform()->SetLocalPosition(Vector3(0.2f, 3.4f, -7.2f)); cameraObject->GetTransform()->SetLocalRotation( Quaternion::LookRotation(Vector3(-0.12f, -0.14f, 1.0f).Normalized())); GameObject* lightObject = mScene->CreateGameObject("MainDirectionalLight"); auto* light = lightObject->AddComponent(); light->SetLightType(LightType::Directional); light->SetColor(XCEngine::Math::Color(1.0f, 1.0f, 0.97f, 1.0f)); light->SetIntensity(2.3f); light->SetCastsShadows(true); lightObject->GetTransform()->SetLocalRotation( Quaternion::LookRotation(Vector3(0.84f, -0.52f, -0.14f).Normalized())); GameObject* groundObject = mScene->CreateGameObject("Ground"); auto* groundMeshFilter = groundObject->AddComponent(); auto* groundMeshRenderer = groundObject->AddComponent(); groundObject->GetTransform()->SetLocalPosition(Vector3(0.0f, -0.18f, 10.5f)); groundObject->GetTransform()->SetLocalScale(Vector3(12.0f, 0.18f, 9.5f)); groundMeshFilter->SetMesh(ResourceHandle(mCubeMesh)); groundMeshRenderer->SetMaterial(0, mGroundMaterial); groundMeshRenderer->SetCastShadows(false); groundMeshRenderer->SetReceiveShadows(true); GameObject* casterObject = mScene->CreateGameObject("CasterCube"); casterObject->GetTransform()->SetLocalPosition(Vector3(-1.2f, 1.71f, 8.8f)); casterObject->GetTransform()->SetLocalScale(Vector3(1.8f, 3.6f, 1.8f)); auto* casterMeshFilter = casterObject->AddComponent(); auto* casterMeshRenderer = casterObject->AddComponent(); casterMeshFilter->SetMesh(ResourceHandle(mCubeMesh)); casterMeshRenderer->SetMaterial(0, mCasterMaterial); casterMeshRenderer->SetCastShadows(true); casterMeshRenderer->SetReceiveShadows(true); } RHIResourceView* DirectionalShadowSceneTest::GetCurrentBackBufferView() { const int backBufferIndex = GetCurrentBackBufferIndex(); if (backBufferIndex < 0) { return nullptr; } if (static_cast(backBufferIndex) >= mBackBufferViews.size()) { mBackBufferViews.resize(static_cast(backBufferIndex) + 1, nullptr); } if (mBackBufferViews[backBufferIndex] == nullptr) { ResourceViewDesc viewDesc = {}; viewDesc.format = static_cast(Format::R8G8B8A8_UNorm); viewDesc.dimension = ResourceViewDimension::Texture2D; viewDesc.mipLevel = 0; mBackBufferViews[backBufferIndex] = GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc); } return mBackBufferViews[backBufferIndex]; } void DirectionalShadowSceneTest::RenderFrame() { RenderSceneFrame(false); } void DirectionalShadowSceneTest::RenderSceneFrame(bool visualizeShadowMap) { ASSERT_NE(mScene, nullptr); ASSERT_NE(mSceneRenderer, nullptr); RHICommandList* commandList = GetCommandList(); ASSERT_NE(commandList, nullptr); commandList->Reset(); RenderSurface surface(kFrameWidth, kFrameHeight); surface.SetColorAttachment(GetCurrentBackBufferView()); surface.SetDepthAttachment(mDepthView); RenderContext renderContext = {}; renderContext.device = GetDevice(); renderContext.commandList = commandList; renderContext.commandQueue = GetCommandQueue(); renderContext.backendType = GetBackendType(); std::vector requests = mSceneRenderer->BuildRenderRequests(*mScene, nullptr, renderContext, surface); ASSERT_FALSE(requests.empty()); if (!mLoggedShadowProbe) { LogShadowAlignmentAnalysis(*mScene, requests[0]); mLoggedShadowProbe = true; } if (visualizeShadowMap) { requests[0].overlayPasses = &mShadowMapDebugPasses; } ASSERT_TRUE(mSceneRenderer->Render(requests)); Log("[TEST] DirectionalShadowSceneTest: closing command list"); commandList->Close(); void* commandLists[] = { commandList }; Log("[TEST] DirectionalShadowSceneTest: executing command list"); GetCommandQueue()->ExecuteCommandLists(1, commandLists); Log("[TEST] DirectionalShadowSceneTest: execute submitted"); } TEST_P(DirectionalShadowSceneTest, RenderDirectionalShadowScene) { RHICommandQueue* commandQueue = GetCommandQueue(); RHISwapChain* swapChain = GetSwapChain(); const int targetFrameCount = 30; const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); const int comparisonThreshold = GetComparisonThreshold(GetBackendType()); for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) { if (frameCount > 0) { commandQueue->WaitForPreviousFrame(); } BeginRender(); RenderFrame(); if (frameCount >= targetFrameCount) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(screenshotFilename)); ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", static_cast(comparisonThreshold))); break; } swapChain->Present(0, 0); } } TEST_P(DirectionalShadowSceneTest, RenderDirectionalShadowMapDebug) { RHICommandQueue* commandQueue = GetCommandQueue(); RHISwapChain* swapChain = GetSwapChain(); const int targetFrameCount = 30; const char* screenshotFilename = GetShadowMapScreenshotFilename(GetBackendType()); for (int frameCount = 0; frameCount <= targetFrameCount; ++frameCount) { if (frameCount > 0) { commandQueue->WaitForPreviousFrame(); } BeginRender(); RenderSceneFrame(true); if (frameCount >= targetFrameCount) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(screenshotFilename)); break; } swapChain->Present(0, 0); } } } // namespace INSTANTIATE_TEST_SUITE_P(D3D12, DirectionalShadowSceneTest, ::testing::Values(RHIType::D3D12)); INSTANTIATE_TEST_SUITE_P(OpenGL, DirectionalShadowSceneTest, ::testing::Values(RHIType::OpenGL)); #if defined(XCENGINE_SUPPORT_VULKAN) INSTANTIATE_TEST_SUITE_P(Vulkan, DirectionalShadowSceneTest, ::testing::Values(RHIType::Vulkan)); #endif GTEST_API_ int main(int argc, char** argv) { return RunRenderingIntegrationTestMain(argc, argv); }