fix: 阴影不可见——灯序依赖遍历顺序 + 光比失衡,新增帧截图调试能力
- 灯序修复(根因):entt Each 遍历序不保证创建序,lights[0] 曾落到 点光(params.x=1)→ shader 阴影分支整个跳过。收集后 stable_sort 方向光排前,确立"lights[0] = CSM 阴影光源"契约 - 光比失衡:IBL 环境与点光淹没主光阴影。KeyLight 5.0 + exposure 默认 0.6 + ambientColor 0.07;阴影区环境光同步衰减(简化遮蔽) - ShadowPass depth bias 0.006(acne 均值形态 vs 推过头丢阴影的折中) - Renderer 帧截图能力:第 60 帧自动截 HDRColor/ShadowMap 落 BMP (ACES+gamma 还原屏幕观感),视觉调试自闭环不再依赖肉眼 - KeyLight 增加 intensity slider(0-20 运行时可调)
This commit is contained in:
+8
-7
@@ -51,9 +51,11 @@ namespace
|
||||
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
|
||||
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
|
||||
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
||||
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
|
||||
|
||||
m_Spinners.push_back(scene.CreateEntity("backpack"));
|
||||
m_Spinners[0].AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
||||
m_Spinners[0].GetComponent<TransformComponent>().SetScale(glm::vec3{1.0});
|
||||
|
||||
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
||||
@@ -76,7 +78,7 @@ namespace
|
||||
LightComponent{
|
||||
.type = LightComponent::Type::Directional,
|
||||
.direction = glm::vec3(-0.5f, -0.6f, -0.35f),
|
||||
.color = glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)
|
||||
.color = glm::vec4(1.0f, 0.95f, 0.9f, 5.0f)
|
||||
});
|
||||
|
||||
scene.CreateEntity("FillLight")
|
||||
@@ -84,7 +86,7 @@ namespace
|
||||
LightComponent{
|
||||
.type = LightComponent::Type::Directional,
|
||||
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
|
||||
.color = glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)
|
||||
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
|
||||
});
|
||||
|
||||
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
|
||||
@@ -93,7 +95,7 @@ namespace
|
||||
fire.AddComponent<LightComponent>(
|
||||
LightComponent{
|
||||
.type = LightComponent::Type::Point,
|
||||
.color = glm::vec4(1.0f, 0.6f, 0.3f, 2.0f), // 暖橙
|
||||
.color = glm::vec4(1.0f, 0.6f, 0.3f, 1.0f), // 暖橙
|
||||
.radius = 8.0f,
|
||||
});
|
||||
|
||||
@@ -127,7 +129,6 @@ namespace
|
||||
{
|
||||
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
|
||||
t.SetRotation(glm::vec3(0.0f, m_Time * 60.0f + static_cast<f32>(i) * 40.0f, 0.0f));
|
||||
t.SetScale(glm::vec3(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +138,6 @@ namespace
|
||||
|
||||
void OnRenderOverlay() override
|
||||
{
|
||||
ImGui::ShowDemoWindow();
|
||||
|
||||
ImGui::Begin("Scene Debug");
|
||||
|
||||
// 阴影光源方向:拖动滑条实时看影子变化
|
||||
@@ -147,9 +146,11 @@ namespace
|
||||
ImGui::Text("Key Light (shadow caster)");
|
||||
ImGui::SliderFloat3("direction",
|
||||
&light->direction.x, -1.0f, 1.0f);
|
||||
ImGui::SliderFloat("intensity", &light->color.a, 0.0f, 20.0f);
|
||||
if (ImGui::Button("reset"))
|
||||
{
|
||||
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
|
||||
light->color.a = 2.0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +303,7 @@ namespace
|
||||
EntityHandle m_KeyLight{};
|
||||
std::vector<EntityHandle> m_Spinners;
|
||||
f32 m_Time = 0.0f;
|
||||
bool m_PauseAnim = false; // 冻结动画时钟(帧历史 diff 验证用)
|
||||
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Engine::ECS {
|
||||
|
||||
// ---- 场景级设置 ----
|
||||
struct Settings {
|
||||
glm::vec4 ambientColor{0.15f, 0.15f, 0.2f, 0.0f};
|
||||
glm::vec4 ambientColor{0.07f, 0.07f, 0.09f, 0.0f};
|
||||
};
|
||||
Settings& GetSettings() { return m_Settings; }
|
||||
|
||||
|
||||
@@ -210,6 +210,13 @@ namespace Engine::RG {
|
||||
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
|
||||
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
|
||||
|
||||
// 调试/截图:取物理 image + 静息布局(帧尾 barrier 恢复用)。
|
||||
// imported 资源返回 false(无物理实例)
|
||||
bool GetCaptureTarget(TextureHandle h, u32 frameSlot,
|
||||
vk::Image& outImage, vk::Extent2D& outExtent,
|
||||
vk::Format& outFormat,
|
||||
vk::ImageLayout& outResting) const;
|
||||
|
||||
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
|
||||
u32 HistorySlot(u32 frameSlot) const {
|
||||
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
|
||||
|
||||
@@ -694,6 +694,24 @@ namespace Engine::RG {
|
||||
return entry.physical[frameSlot]->view;
|
||||
}
|
||||
|
||||
bool RenderGraph::GetCaptureTarget(TextureHandle h, u32 frameSlot,
|
||||
vk::Image& outImage, vk::Extent2D& outExtent,
|
||||
vk::Format& outFormat,
|
||||
vk::ImageLayout& outResting) const
|
||||
{
|
||||
const auto& entry = GetEntry(h);
|
||||
if (entry.imported || frameSlot >= entry.physical.size() ||
|
||||
!entry.physical[frameSlot]) {
|
||||
return false;
|
||||
}
|
||||
const auto& phys = *entry.physical[frameSlot];
|
||||
outImage = phys.image;
|
||||
outExtent = phys.extent;
|
||||
outFormat = phys.format;
|
||||
outResting = entry.restingLayout;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderGraph::SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame)
|
||||
{
|
||||
m_QueryPool = pool;
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace Engine::Passes
|
||||
.setBorderColor(vk::BorderColor::eFloatOpaqueWhite)
|
||||
.setUnnormalizedCoordinates(VK_FALSE)
|
||||
.setCompareEnable(VK_TRUE)
|
||||
.setCompareOp(vk::CompareOp::eLess);
|
||||
.setCompareOp(vk::CompareOp::eGreater); // 【诊断】方向对照
|
||||
shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo);
|
||||
|
||||
// per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑)
|
||||
|
||||
@@ -50,9 +50,10 @@ namespace Engine::Passes
|
||||
cmd.setDepthBoundsTestEnable(VK_FALSE);
|
||||
|
||||
cmd.setStencilTestEnable(VK_FALSE);
|
||||
// 深度 bias 压平 shadow acne(NDC 量级)
|
||||
// 深度 bias 压平 shadow acne(NDC 量级;0.01 ≈ 1m 世界深度,
|
||||
// 过大会把跨物体遮挡差也推没 → 阴影整体消失)
|
||||
cmd.setDepthBiasEnable(VK_TRUE);
|
||||
cmd.setDepthBias(0.0025f, 0.0f, 0.003f);
|
||||
cmd.setDepthBias(0.006f, 0.0f, 0.006f);
|
||||
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
|
||||
cmd.setCullMode(vk::CullModeFlagBits::eNone);
|
||||
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
|
||||
|
||||
@@ -42,9 +42,83 @@
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <bit>
|
||||
#include <fstream>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// fp16 → f32(截图用,规范路径足够)
|
||||
f32 HalfToFloat(u16 h)
|
||||
{
|
||||
const u32 sign = (h & 0x8000u) << 16u;
|
||||
const u32 exp = (h >> 10u) & 0x1Fu;
|
||||
const u32 man = h & 0x3FFu;
|
||||
u32 bits;
|
||||
if (exp == 0) {
|
||||
if (man == 0) return std::bit_cast<f32>(sign);
|
||||
u32 e = 127 - 15 + 1;
|
||||
u32 m = man;
|
||||
while (!(m & 0x400u)) { m <<= 1u; --e; }
|
||||
bits = sign | (e << 23u) | ((m & 0x3FFu) << 13u);
|
||||
} else if (exp == 31) {
|
||||
bits = sign | 0x7F800000u | (man << 13u);
|
||||
} else {
|
||||
bits = sign | ((exp - 15u + 127u) << 23u) | (man << 13u);
|
||||
}
|
||||
return std::bit_cast<f32>(bits);
|
||||
}
|
||||
|
||||
// RGBA16F 线性数据 → 24bpp BMP(行倒序;depthMode 读 f32 深度)
|
||||
void WriteHdrBMP(const std::string& path, const void* data,
|
||||
u32 w, u32 h, bool depthMode = false)
|
||||
{
|
||||
const auto* src = static_cast<const u16*>(data);
|
||||
const u32 rowBytes = w * 3u;
|
||||
const u32 pad = (4u - rowBytes % 4u) % 4u;
|
||||
const u32 stride = rowBytes + pad;
|
||||
const u32 size = 54u + stride * h;
|
||||
|
||||
std::vector<u8> bmp(size, 0);
|
||||
auto put = [&bmp](u32 off, u32 v, u32 n) {
|
||||
for (u32 i = 0; i < n; ++i)
|
||||
bmp[off + i] = static_cast<u8>((v >> (8u * i)) & 0xFFu);
|
||||
};
|
||||
bmp[0] = 'B'; bmp[1] = 'M';
|
||||
put(2, size, 4); put(10, 54, 4); put(14, 40, 4);
|
||||
put(18, w, 4); put(22, h, 4); put(26, 1, 2); put(28, 24, 2);
|
||||
put(34, stride * h, 4);
|
||||
|
||||
for (u32 y = 0; y < h; ++y) {
|
||||
const u32 dstRow = h - 1 - y; // BMP 底行在前
|
||||
for (u32 x = 0; x < w; ++x) {
|
||||
const u16* px = src + (static_cast<size_t>(y) * w + x) * 4;
|
||||
const u32 off = 54u + dstRow * stride + x * 3u;
|
||||
for (u32 c = 0; c < 3; ++c) {
|
||||
f32 v;
|
||||
if (depthMode) {
|
||||
// D32 buffer:f32 深度,反相显示(近=白)
|
||||
v = 1.0f - reinterpret_cast<const f32*>(
|
||||
data)[static_cast<size_t>(y) * w + x];
|
||||
} else {
|
||||
v = HalfToFloat(px[c]) * 0.6f; // 匹配默认 exposure
|
||||
// ACES 近似(与 TonemapPass 同族),还原屏幕观感
|
||||
v = (v * (2.51f * v + 0.03f)) /
|
||||
(v * (2.43f * v + 0.59f) + 0.14f);
|
||||
v = glm::pow(glm::clamp(v, 0.0f, 1.0f),
|
||||
1.0f / 2.2f);
|
||||
}
|
||||
bmp[off + c] = static_cast<u8>(v * 255.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
file.write(reinterpret_cast<const char*>(bmp.data()),
|
||||
static_cast<std::streamsize>(bmp.size()));
|
||||
}
|
||||
}
|
||||
Renderer::Renderer(Window& window)
|
||||
: m_Window(window)
|
||||
{
|
||||
@@ -104,15 +178,19 @@ namespace Engine
|
||||
m_SceneGpu->textureSetLayout);
|
||||
|
||||
// 8. RenderGraph:资源声明 + pass 注册 + 首次编译
|
||||
ENGINE_INFO("[diag] BuildGraph begin");
|
||||
m_PostParams = CreateScope<Passes::PostProcessParams>();
|
||||
BuildGraph();
|
||||
ENGINE_INFO("[diag] BuildGraph done");
|
||||
|
||||
// 首次编译后物理镜像就位:各 pass 绑定自己的输入描述符
|
||||
for (auto& pass : m_Passes) {
|
||||
pass->OnTargetsRebuilt(*m_RenderGraph);
|
||||
}
|
||||
ENGINE_INFO("[diag] targets rebuilt");
|
||||
// 环境辐照度绑定(view 全程稳定,绑一次)
|
||||
m_SceneGpu->BindEnvironment(*m_Environment);
|
||||
ENGINE_INFO("[diag] environment bound");
|
||||
|
||||
// 9. GPU timestamp:每 pass 两枚(前后),按帧槽位分区
|
||||
m_QueriesPerFrame = static_cast<u32>(m_RenderGraph->GetPassCount()) * 2;
|
||||
@@ -392,6 +470,68 @@ namespace Engine
|
||||
|
||||
m_RenderGraph->Execute(frameCtx);
|
||||
|
||||
// 调试截图:帧尾把 TAAColor 拷进 host buffer(一次)
|
||||
++m_FrameCounter;
|
||||
const bool doCapture = !m_Captured && m_FrameCounter == 60;
|
||||
vk::Image capImage{};
|
||||
vk::Extent2D capExtent{};
|
||||
vk::Format capFormat{};
|
||||
vk::ImageLayout capResting{};
|
||||
if (doCapture &&
|
||||
m_RenderGraph->GetCaptureTarget(m_HDRColor, frameSlot,
|
||||
capImage, capExtent, capFormat, capResting)) {
|
||||
const vk::DeviceSize capBytes = static_cast<vk::DeviceSize>(
|
||||
capExtent.width) * capExtent.height * 8; // RGBA16F
|
||||
const bool capDepth = capFormat == vk::Format::eD32Sfloat;
|
||||
|
||||
m_CaptureAlloc = m_Allocator->CreateBuffer(
|
||||
capBytes,
|
||||
vk::BufferUsageFlagBits::eTransferDst,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
|
||||
// resting → TRANSFER_SRC → 拷贝 → 恢复 resting
|
||||
const vk::ImageAspectFlagBits capAspect =
|
||||
capDepth ? vk::ImageAspectFlagBits::eDepth
|
||||
: vk::ImageAspectFlagBits::eColor;
|
||||
vk::ImageMemoryBarrier2 toSrc;
|
||||
toSrc.setSrcStageMask(vk::PipelineStageFlagBits2::eAllCommands)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eMemoryWrite)
|
||||
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||
.setDstAccessMask(vk::AccessFlagBits2::eTransferRead)
|
||||
.setOldLayout(capResting)
|
||||
.setNewLayout(vk::ImageLayout::eTransferSrcOptimal)
|
||||
.setImage(capImage)
|
||||
.setSubresourceRange({capAspect, 0, 1, 0, 1});
|
||||
frameCtx.cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(toSrc));
|
||||
|
||||
vk::BufferImageCopy capRegion;
|
||||
capRegion.setBufferOffset(0)
|
||||
.setBufferRowLength(0)
|
||||
.setBufferImageHeight(0)
|
||||
.setImageSubresource({capAspect, 0, 0, 1})
|
||||
.setImageOffset({0, 0, 0})
|
||||
.setImageExtent({capExtent.width, capExtent.height, 1});
|
||||
frameCtx.cmd.copyImageToBuffer(
|
||||
capImage, vk::ImageLayout::eTransferSrcOptimal,
|
||||
m_CaptureAlloc.Buffer, capRegion);
|
||||
|
||||
vk::ImageMemoryBarrier2 back;
|
||||
back.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eTransferRead)
|
||||
.setDstStageMask(vk::PipelineStageFlagBits2::eAllCommands)
|
||||
.setDstAccessMask(vk::AccessFlagBits2::eMemoryRead |
|
||||
vk::AccessFlagBits2::eMemoryWrite)
|
||||
.setOldLayout(vk::ImageLayout::eTransferSrcOptimal)
|
||||
.setNewLayout(capResting)
|
||||
.setImage(capImage)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, 1});
|
||||
frameCtx.cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(back));
|
||||
}
|
||||
|
||||
m_CommandManager->EndFrame(frameSlot);
|
||||
|
||||
// 5. 提交
|
||||
@@ -435,6 +575,20 @@ namespace Engine
|
||||
HandleResize();
|
||||
}
|
||||
|
||||
// 调试截图落盘:等 fence(copy 已提交完成)→ fp16 → BMP
|
||||
if (doCapture) {
|
||||
m_Device->GetDevice().waitForFences(
|
||||
m_SyncManager->GetFrameFence(frameSlot), VK_TRUE,
|
||||
UINT64_MAX);
|
||||
WriteHdrBMP("capture.bmp", m_CaptureAlloc.MappedPtr,
|
||||
capExtent.width, capExtent.height, false);
|
||||
m_Allocator->Destroy(m_CaptureAlloc);
|
||||
m_Captured = true;
|
||||
ENGINE_INFO("Debug capture written: capture.bmp (HDRColor) "
|
||||
"shadowInstances={}",
|
||||
m_SceneRenderer->GetShadowInstanceCount());
|
||||
}
|
||||
|
||||
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
@@ -173,14 +174,18 @@ namespace Engine
|
||||
m_Frame.skyInvVP =
|
||||
glm::inverse(ubo.projNoJitter * glm::mat4(glm::mat3(ubo.view)));
|
||||
|
||||
// ---- 2. 灯光(第一个方向光作为 CSM 阴影光源;点光位置来自 Transform)----
|
||||
u32 lightCount = 0;
|
||||
// ---- 2. 灯光(CSM 阴影光源约定 = lights[0] 必是方向光)----
|
||||
// entt Each 遍历序与实体创建序不保证一致——直接按迭代序填充会让
|
||||
// lights[0] 落到点光上(params.x=1)→ shader 阴影分支整个跳过。
|
||||
// 先收集到临时数组,方向光排前,再写入 UBO
|
||||
glm::vec3 shadowDir{0.0f, -1.0f, 0.0f}; // 无灯兜底:头顶正射
|
||||
LightData collected[kMaxLights];
|
||||
u32 collectedCount = 0;
|
||||
scene.Each<TransformComponent, LightComponent>(
|
||||
[&](Entity, TransformComponent& t, LightComponent& light) {
|
||||
if (lightCount >= kMaxLights) return;
|
||||
if (collectedCount >= kMaxLights) return;
|
||||
|
||||
auto& dst = ubo.lights[lightCount];
|
||||
auto& dst = collected[collectedCount++];
|
||||
dst.color = light.color;
|
||||
dst.params = glm::vec4(
|
||||
light.type == LightComponent::Type::Point ? 1.0f : 0.0f,
|
||||
@@ -195,17 +200,24 @@ namespace Engine
|
||||
dst.direction = glm::vec4(dir, 0.0f);
|
||||
dst.position = glm::vec4(0.0f);
|
||||
}
|
||||
|
||||
// CSM 阴影光源 = 第一个方向光(点光不做级联阴影)
|
||||
if (lightCount == 0 &&
|
||||
light.type == LightComponent::Type::Directional) {
|
||||
shadowDir = glm::length(light.direction) > 0.0f
|
||||
? glm::normalize(light.direction) : shadowDir;
|
||||
}
|
||||
++lightCount;
|
||||
});
|
||||
for (u32 i = lightCount; i < kMaxLights; ++i) {
|
||||
ubo.lights[i] = {};
|
||||
|
||||
// 方向光排前(稳定排序:同类保持收集序)→ lights[0] = CSM 光源
|
||||
std::stable_sort(collected, collected + collectedCount,
|
||||
[](const LightData& a, const LightData& b) {
|
||||
return a.params.x < b.params.x; // 0=方向光 < 1=点光
|
||||
});
|
||||
|
||||
for (u32 i = 0; i < kMaxLights; ++i) {
|
||||
if (i < collectedCount) {
|
||||
ubo.lights[i] = collected[i];
|
||||
} else {
|
||||
ubo.lights[i] = {};
|
||||
}
|
||||
}
|
||||
if (collectedCount > 0 &&
|
||||
collected[0].params.x < 0.5f) {
|
||||
shadowDir = glm::vec3(collected[0].direction);
|
||||
}
|
||||
ComputeCascades(shadowDir);
|
||||
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f);
|
||||
|
||||
Reference in New Issue
Block a user