Fix directional shadow alignment across backends

This commit is contained in:
2026-04-05 12:40:34 +08:00
parent bc6722e5ab
commit ec97445071
5 changed files with 677 additions and 50 deletions

View File

@@ -41,15 +41,15 @@ float ComputeShadowAttenuation(vec3 positionWS) {
vec3 shadowNdc = shadowClip.xyz / shadowClip.w;
vec2 shadowUv = vec2(
shadowNdc.x * 0.5 + 0.5,
shadowNdc.y * -0.5 + 0.5);
shadowNdc.y * 0.5 + 0.5);
if (shadowUv.x < 0.0 || shadowUv.x > 1.0 ||
shadowUv.y < 0.0 || shadowUv.y > 1.0 ||
shadowNdc.z < 0.0 || shadowNdc.z > 1.0) {
shadowNdc.z < -1.0 || shadowNdc.z > 1.0) {
return 1.0;
}
float shadowDepth = texture(uShadowMapTexture, shadowUv).r;
float receiverDepth = shadowNdc.z - gShadowBiasAndTexelSize.x;
float receiverDepth = shadowNdc.z * 0.5 + 0.5 - gShadowBiasAndTexelSize.x;
float shadowStrength = clamp(gShadowBiasAndTexelSize.w, 0.0, 1.0);
return receiverDepth <= shadowDepth ? 1.0 : (1.0 - shadowStrength);
}

View File

@@ -133,13 +133,10 @@ Quaternion Quaternion::LookRotation(const Vector3& forward, const Vector3& up) {
Vector3 r = Vector3::Normalize(Vector3::Cross(upVec, f));
Vector3 u = Vector3::Cross(f, r);
Matrix4 m;
m.m[0][0] = r.x; m.m[0][1] = r.y; m.m[0][2] = r.z;
m.m[1][0] = u.x; m.m[1][1] = u.y; m.m[1][2] = u.z;
m.m[2][0] = f.x; m.m[2][1] = f.y; m.m[2][2] = f.z;
m.m[0][3] = m[1][3] = m[2][3] = 0.0f;
m.m[3][0] = m[3][1] = m[3][2] = 0.0f;
m.m[3][3] = 1.0f;
Matrix4 m = Matrix4::Identity();
m.m[0][0] = r.x; m.m[1][0] = r.y; m.m[2][0] = r.z;
m.m[0][1] = u.x; m.m[1][1] = u.y; m.m[2][1] = u.z;
m.m[0][2] = f.x; m.m[1][2] = f.y; m.m[2][2] = f.z;
return FromRotationMatrix(m);
}

File diff suppressed because one or more lines are too long

View File

@@ -18,17 +18,24 @@
#include <XCEngine/Debug/ConsoleLogSink.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/Rendering/SceneRenderer.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <XCEngine/RHI/RHIDescriptorPool.h>
#include <XCEngine/RHI/RHIDescriptorSet.h>
#include <XCEngine/RHI/RHIPipelineLayout.h>
#include <XCEngine/RHI/RHIPipelineState.h>
#include <XCEngine/RHI/RHISampler.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/Scene/Scene.h>
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
#include <cstring>
#include <memory>
#include <vector>
@@ -45,9 +52,79 @@ 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<float> 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<StaticMeshVertex>& vertices,
std::vector<uint32_t>& indices,
@@ -93,45 +170,37 @@ Mesh* CreateGroundMesh() {
params.guid = ResourceGUID::Generate(params.path);
mesh->Initialize(params);
std::vector<StaticMeshVertex> vertices;
std::vector<uint32_t> indices;
vertices.reserve(8);
indices.reserve(12);
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);
AppendQuadFace(
vertices, indices,
Vector3(-8.0f, 0.0f, 0.5f),
Vector3(-8.0f, 0.0f, 12.0f),
Vector3(8.0f, 0.0f, 0.5f),
Vector3(8.0f, 0.0f, 12.0f),
Vector3::Up());
AppendQuadFace(
vertices, indices,
Vector3(-8.0f, 0.0f, 12.0f),
Vector3(8.0f, 0.0f, 12.0f),
Vector3(-8.0f, 6.0f, 12.0f),
Vector3(8.0f, 6.0f, 12.0f),
Vector3::Back());
const uint32_t indices[6] = { 0, 1, 2, 2, 1, 3 };
mesh->SetVertexData(
vertices.data(),
vertices.size() * sizeof(StaticMeshVertex),
static_cast<uint32_t>(vertices.size()),
vertices,
sizeof(vertices),
4,
sizeof(StaticMeshVertex),
VertexAttribute::Position | VertexAttribute::Normal | VertexAttribute::UV0);
mesh->SetIndexData(
indices.data(),
indices.size() * sizeof(uint32_t),
static_cast<uint32_t>(indices.size()),
true);
const Bounds bounds(Vector3(0.0f, 3.0f, 6.25f), Vector3(16.0f, 6.0f, 11.5f));
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 = static_cast<uint32_t>(vertices.size());
section.vertexCount = 4;
section.startIndex = 0;
section.indexCount = static_cast<uint32_t>(indices.size());
section.indexCount = 6;
section.materialID = 0;
section.bounds = bounds;
mesh->AddSection(section);
@@ -248,15 +317,509 @@ const char* GetScreenshotFilename(RHIType backendType) {
}
}
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<int>(point.uvFlipY.x * static_cast<float>(shadowMapWidth)),
static_cast<int>(point.uvFlipY.y * static_cast<float>(shadowMapHeight)),
static_cast<int>(point.uvNoFlipY.x * static_cast<float>(shadowMapWidth)),
static_cast<int>(point.uvNoFlipY.y * static_cast<float>(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<RHIResourceView*>& 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<float>(renderArea.x),
static_cast<float>(renderArea.y),
static_cast<float>(renderArea.width),
static_cast<float>(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<int>(context.backendType));
mDevice = context.device;
mBackendType = context.backendType;
SamplerDesc samplerDesc = {};
samplerDesc.filter = static_cast<uint32_t>(FilterMode::Point);
samplerDesc.addressU = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressV = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.addressW = static_cast<uint32_t>(TextureAddressMode::Clamp);
samplerDesc.comparisonFunc = static_cast<uint32_t>(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<uint32_t>(DescriptorType::SRV);
textureBinding.count = 1;
textureBinding.visibility = static_cast<uint32_t>(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<uint32_t>(DescriptorType::Sampler);
samplerBinding.count = 1;
samplerBinding.visibility = static_cast<uint32_t>(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<uint32_t>(PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(Format::Unknown);
pipelineDesc.sampleCount = 1;
pipelineDesc.rasterizerState.fillMode = static_cast<uint32_t>(FillMode::Solid);
pipelineDesc.rasterizerState.cullMode = static_cast<uint32_t>(CullMode::None);
pipelineDesc.rasterizerState.frontFace = static_cast<uint32_t>(FrontFace::CounterClockwise);
pipelineDesc.rasterizerState.depthClipEnable = true;
pipelineDesc.blendState.blendEnable = false;
pipelineDesc.blendState.colorWriteMask = static_cast<uint8_t>(ColorWriteMask::All);
pipelineDesc.depthStencilState.depthTestEnable = false;
pipelineDesc.depthStencilState.depthWriteEnable = false;
pipelineDesc.depthStencilState.depthFunc = static_cast<uint32_t>(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();
@@ -264,6 +827,9 @@ private:
std::unique_ptr<Scene> mScene;
std::unique_ptr<SceneRenderer> mSceneRenderer;
RenderPassSequence mShadowMapDebugPasses;
ShadowMapDebugOverlayPass* mShadowMapDebugOverlayPass = nullptr;
bool mLoggedShadowProbe = false;
std::vector<RHIResourceView*> mBackBufferViews;
RHITexture* mDepthTexture = nullptr;
RHIResourceView* mDepthView = nullptr;
@@ -294,6 +860,10 @@ void DirectionalShadowSceneTest::SetUp() {
"Tests/Rendering/DirectionalShadowCaster.material",
Vector4(0.72f, 0.22f, 0.16f, 1.0f));
auto shadowMapDebugPass = std::make_unique<ShadowMapDebugOverlayPass>();
mShadowMapDebugOverlayPass = shadowMapDebugPass.get();
mShadowMapDebugPasses.AddPass(std::move(shadowMapDebugPass));
BuildScene();
TextureDesc depthDesc = {};
@@ -323,6 +893,11 @@ void DirectionalShadowSceneTest::SetUp() {
void DirectionalShadowSceneTest::TearDown() {
mSceneRenderer.reset();
if (mShadowMapDebugOverlayPass != nullptr) {
mShadowMapDebugOverlayPass->ReleaseResources();
mShadowMapDebugOverlayPass = nullptr;
}
if (mDepthView != nullptr) {
mDepthView->Shutdown();
delete mDepthView;
@@ -368,34 +943,36 @@ void DirectionalShadowSceneTest::BuildScene() {
GameObject* cameraObject = mScene->CreateGameObject("MainCamera");
auto* camera = cameraObject->AddComponent<CameraComponent>();
camera->SetPrimary(true);
camera->SetFieldOfView(45.0f);
camera->SetFieldOfView(42.0f);
camera->SetNearClipPlane(0.1f);
camera->SetFarClipPlane(100.0f);
camera->SetFarClipPlane(40.0f);
camera->SetClearColor(XCEngine::Math::Color(0.03f, 0.03f, 0.05f, 1.0f));
cameraObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 2.6f, -4.6f));
cameraObject->GetTransform()->SetLocalPosition(Vector3(0.2f, 3.4f, -7.2f));
cameraObject->GetTransform()->SetLocalRotation(
Quaternion::LookRotation(Vector3(0.0f, -0.10f, 1.0f).Normalized()));
Quaternion::LookRotation(Vector3(-0.12f, -0.14f, 1.0f).Normalized()));
GameObject* lightObject = mScene->CreateGameObject("MainDirectionalLight");
auto* light = lightObject->AddComponent<LightComponent>();
light->SetLightType(LightType::Directional);
light->SetColor(XCEngine::Math::Color(1.0f, 1.0f, 0.97f, 1.0f));
light->SetIntensity(2.1f);
light->SetIntensity(2.3f);
light->SetCastsShadows(true);
lightObject->GetTransform()->SetLocalRotation(
Quaternion::LookRotation(Vector3(0.55f, -0.35f, 0.76f).Normalized()));
Quaternion::LookRotation(Vector3(0.84f, -0.52f, -0.14f).Normalized()));
GameObject* groundObject = mScene->CreateGameObject("Ground");
auto* groundMeshFilter = groundObject->AddComponent<MeshFilterComponent>();
auto* groundMeshRenderer = groundObject->AddComponent<MeshRendererComponent>();
groundMeshFilter->SetMesh(ResourceHandle<Mesh>(mGroundMesh));
groundObject->GetTransform()->SetLocalPosition(Vector3(0.0f, -0.18f, 10.5f));
groundObject->GetTransform()->SetLocalScale(Vector3(12.0f, 0.18f, 9.5f));
groundMeshFilter->SetMesh(ResourceHandle<Mesh>(mCubeMesh));
groundMeshRenderer->SetMaterial(0, mGroundMaterial);
groundMeshRenderer->SetCastShadows(false);
groundMeshRenderer->SetReceiveShadows(true);
GameObject* casterObject = mScene->CreateGameObject("CasterCube");
casterObject->GetTransform()->SetLocalPosition(Vector3(-3.2f, 1.4f, 7.4f));
casterObject->GetTransform()->SetLocalScale(Vector3(1.4f, 2.8f, 1.4f));
casterObject->GetTransform()->SetLocalPosition(Vector3(-1.2f, 2.2f, 8.8f));
casterObject->GetTransform()->SetLocalScale(Vector3(1.8f, 3.6f, 1.8f));
auto* casterMeshFilter = casterObject->AddComponent<MeshFilterComponent>();
auto* casterMeshRenderer = casterObject->AddComponent<MeshRendererComponent>();
casterMeshFilter->SetMesh(ResourceHandle<Mesh>(mCubeMesh));
@@ -426,6 +1003,10 @@ RHIResourceView* DirectionalShadowSceneTest::GetCurrentBackBufferView() {
}
void DirectionalShadowSceneTest::RenderFrame() {
RenderSceneFrame(false);
}
void DirectionalShadowSceneTest::RenderSceneFrame(bool visualizeShadowMap) {
ASSERT_NE(mScene, nullptr);
ASSERT_NE(mSceneRenderer, nullptr);
@@ -444,11 +1025,26 @@ void DirectionalShadowSceneTest::RenderFrame() {
renderContext.commandQueue = GetCommandQueue();
renderContext.backendType = GetBackendType();
ASSERT_TRUE(mSceneRenderer->Render(*mScene, nullptr, renderContext, surface));
std::vector<CameraRenderRequest> 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) {
@@ -477,6 +1073,30 @@ TEST_P(DirectionalShadowSceneTest, RenderDirectionalShadowScene) {
}
}
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));

View File

@@ -204,6 +204,16 @@ TEST(Math_Quaternion, LookRotation_Right) {
EXPECT_NEAR(q.w, 0.707f, 1e-3f);
}
TEST(Math_Quaternion, LookRotation_ArbitraryForwardPreserved) {
const Vector3 requestedForward = Vector3(0.84f, -0.52f, -0.14f).Normalized();
const Quaternion q = Quaternion::LookRotation(requestedForward);
const Vector3 actualForward = q * Vector3::Forward();
EXPECT_NEAR(actualForward.x, requestedForward.x, 1e-4f);
EXPECT_NEAR(actualForward.y, requestedForward.y, 1e-4f);
EXPECT_NEAR(actualForward.z, requestedForward.z, 1e-4f);
}
TEST(Math_Quaternion, Multiply_ProducesRotation) {
Quaternion q1 = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Quaternion q2 = Quaternion::FromAxisAngle(Vector3::Right(), PI * 0.5f);