feat: 透明窗口 + 实例预算扩容(修复斜光阴影静默截断)

透明窗口:SceneSettings 加 ClearColor/DrawSkybox,随 FrameSceneData
每帧下发到 SkyboxPass(替换硬编码清屏色);GLFW 透明帧缓冲已开,
tonemap 预乘 alpha 直通已有,链路全通。TAA alpha 时域混合为已知边界。

实例预算:kMaxInstances 16384 → 65536(四视图共享预算,100 实体
斜光时需求 16.4k 超限,cascade 2 实例被静默丢弃 = 用户看到的
"拖灯光方向部分背包阴影消失")。截断状态每帧落遥测 + ImGui 红字
(原 warn-once 对动态穿越阈值的场景等于没有告警)。
This commit is contained in:
2026-09-24 18:54:01 +08:00
parent fa9681aa41
commit 6df3be2846
7 changed files with 66 additions and 30 deletions
+28 -16
View File
@@ -22,13 +22,13 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include <atomic> #include <atomic>
#include <thread> #include <thread>
using namespace Engine; using namespace Engine;
using namespace Engine::ECS; using namespace Engine::ECS;
@@ -88,7 +88,7 @@ namespace
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); 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"); // const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
int countSize = 2; int countSize = 10;
for (int i = 0; i < countSize; i ++) for (int i = 0; i < countSize; i ++)
{ {
for (int j = 0; j < countSize; j ++) for (int j = 0; j < countSize; j ++)
@@ -316,23 +316,31 @@ namespace
{ {
// 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重) // 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重)
ImGui::Text("Environment (IBL)"); ImGui::Text("Environment (IBL)");
int envIdx = m_EnvIndex; bool& DrawSkybox = GetScene().GetSettings().DrawSkybox;
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount)) ImGui::Checkbox("DrawSkybox", &DrawSkybox);
if (DrawSkybox)
{ {
// 异步:解码后台线程,完成后 OnUpdate 里换绑 int envIdx = m_EnvIndex;
m_EnvIndex = envIdx; if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
RequestEnvironment(envIdx); {
} // 异步:解码后台线程,完成后 OnUpdate 里换绑
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" : m_EnvIndex = envIdx;
"(switch = compute pre-integrate, ms-level)"); RequestEnvironment(envIdx);
}
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
"(switch = compute pre-integrate, ms-level)");
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug) // 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames(); const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView(); int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(), if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size()))) static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}else
{ {
renderer.SetDebugView(view); ImGui::ColorEdit4("ClearColor", glm::value_ptr(GetScene().GetSettings().ClearColor));
} }
} }
@@ -343,6 +351,10 @@ namespace
ImGui::Text("instances: %u main / %u shadow", ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(), renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount()); renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames(); const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs(); const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i) for (size_t i = 0; i < names.size() && i < times.size(); ++i)
+3 -1
View File
@@ -41,7 +41,9 @@ namespace Engine::ECS {
// ---- 场景级设置 ---- // ---- 场景级设置 ----
struct Settings { struct Settings {
glm::vec4 ambientColor{0.07f, 0.07f, 0.09f, 0.0f}; glm::vec4 ClearColor = {0.1, 0.1, 0.1, 1.0};
glm::vec4 AmbientColor{0.07f, 0.07f, 0.09f, 0.0f};
bool DrawSkybox = true;
}; };
Settings& GetSettings() { return m_Settings; } Settings& GetSettings() { return m_Settings; }
@@ -109,6 +109,7 @@ namespace Engine {
// 双视图剔除后的实例数(遥测) // 双视图剔除后的实例数(遥测)
u32 GetMainInstanceCount() const; u32 GetMainInstanceCount() const;
u32 GetShadowInstanceCount() const; u32 GetShadowInstanceCount() const;
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
// 垂直同步开关(present mode 切换会重建 swapchain), // 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前) // 只应在帧 UI 构建段调用(Render 之前)
+12 -1
View File
@@ -33,7 +33,11 @@ namespace Engine {
}; };
constexpr u32 kMaxLights = 4; constexpr u32 kMaxLights = 4;
constexpr u32 kMaxInstances = 16384; // 实例 SSBO 容量(矩阵数) // 实例 SSBO 容量(矩阵数)。四视图(main + 3 级联)共享同一预算:
// 需求 = Σ各视图剔除后实例数,大场景斜光时阴影实例可膨胀到主视图
// 的 2-3 倍。65536 = 4MB/槽位,覆盖 ~800 实体 × 79 mesh 全级联;
// 再往上就该上 LOD / GPU culling,而不是继续加预算
constexpr u32 kMaxInstances = 65536;
constexpr u32 kMaxCascades = 3; // CSM 级联数 constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长 constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
@@ -69,6 +73,9 @@ namespace Engine {
struct FrameSceneData { struct FrameSceneData {
UniformBufferObject ubo{}; UniformBufferObject ubo{};
glm::vec4 ClearColor{0.1, 0.1, 0.1, 1.0};
bool DrawSkybox = true;
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view))) // 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push) //(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
glm::mat4 skyInvVP{1.0f}; glm::mat4 skyInvVP{1.0f};
@@ -90,6 +97,10 @@ namespace Engine {
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零 bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏) int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警
bool instancesTruncated = false;
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测) f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
}; };
+9 -8
View File
@@ -94,9 +94,10 @@ namespace Engine::Passes
ZoneScopedN("Skybox"); ZoneScopedN("Skybox");
const vk::CommandBuffer cmd = ctx.cmd; const vk::CommandBuffer cmd = ctx.cmd;
const glm::vec4& cc = m_Frame.ClearColor;
vk::ClearValue clear; vk::ClearValue clear;
clear.color = vk::ClearColorValue( clear.color = vk::ClearColorValue(
std::array<float, 4>{0.1f, 0.1f, 0.1f, 1.0f}); std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
vk::RenderingAttachmentInfo color; vk::RenderingAttachmentInfo color;
color.setImageView(ctx.View(m_HDRColor)) color.setImageView(ctx.View(m_HDRColor))
@@ -112,13 +113,13 @@ namespace Engine::Passes
cmd.beginRendering(renderingInfo); cmd.beginRendering(renderingInfo);
if (m_Env) { if (m_Env && m_Frame.DrawSkybox) {
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写) // 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
std::array<vk::ShaderStageFlagBits, 2> stages = { const std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex, vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment vk::ShaderStageFlagBits::eFragment
}; };
std::array<vk::ShaderEXT, 2> shaders = {m_Vert, m_Frag}; const std::array<vk::ShaderEXT, 2> shaders = {m_Vert, m_Frag};
cmd.bindShadersEXT(stages, shaders); cmd.bindShadersEXT(stages, shaders);
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
@@ -149,12 +150,12 @@ namespace Engine::Passes
cmd.setVertexInputEXT({}, {}); cmd.setVertexInputEXT({}, {});
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF; constexpr vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask); cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE); cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE); cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f, const vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width), static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height), static_cast<float>(ctx.extent.height),
0.0f, 1.0f); 0.0f, 1.0f);
@@ -170,8 +171,8 @@ namespace Engine::Passes
.setDstAlphaBlendFactor(vk::BlendFactor::eZero) .setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd); .setAlphaBlendOp(vk::BlendOp::eAdd);
// ArrayProxyNoTemporaries 拒绝临时量:具名局部变量传入 // ArrayProxyNoTemporaries 拒绝临时量:具名局部变量传入
vk::Bool32 blendEnable = VK_FALSE; constexpr vk::Bool32 blendEnable = VK_FALSE;
vk::ColorComponentFlags colorWriteMask = constexpr vk::ColorComponentFlags colorWriteMask =
vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eB |
+5
View File
@@ -215,6 +215,11 @@ namespace Engine
return m_SceneRenderer->GetMainInstanceCount(); return m_SceneRenderer->GetMainInstanceCount();
} }
bool Renderer::GetInstancesTruncated() const
{
return m_SceneRenderer->Frame().instancesTruncated;
}
u32 Renderer::GetShadowInstanceCount() const u32 Renderer::GetShadowInstanceCount() const
{ {
return m_SceneRenderer->GetShadowInstanceCount(); return m_SceneRenderer->GetShadowInstanceCount();
+8 -4
View File
@@ -166,9 +166,11 @@ namespace Engine
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height); ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height);
} }
ubo.ambientColor = scene.GetSettings().ambientColor; ubo.ambientColor = scene.GetSettings().AmbientColor;
// ---- 运行时开关/调试视图落帧(各 pass 与 frag 只读契约字段)---- // ---- 运行时开关/调试视图落帧(各 pass 与 frag 只读契约字段)----
m_Frame.ClearColor = scene.GetSettings().ClearColor;
m_Frame.DrawSkybox = scene.GetSettings().DrawSkybox;
m_Frame.shadowsEnabled = m_ShadowsEnabled; m_Frame.shadowsEnabled = m_ShadowsEnabled;
m_Frame.taaEnabled = m_TAAEnabled; m_Frame.taaEnabled = m_TAAEnabled;
m_Frame.bloomEnabled = m_BloomEnabled; m_Frame.bloomEnabled = m_BloomEnabled;
@@ -363,11 +365,13 @@ namespace Engine
m_LastProjNoJitter = ubo.projNoJitter; m_LastProjNoJitter = ubo.projNoJitter;
++m_FrameIndex; ++m_FrameIndex;
// 截断状态每帧落遥测(ImGui 可见)——"只告警一次"的日志对
// 大场景不够:预算是动态穿越的,用户必须实时看到截断发生
m_Frame.instancesTruncated = instanceTruncated;
if (instanceTruncated && !m_TruncationWarned) { if (instanceTruncated && !m_TruncationWarned) {
// 只告警一次(每帧告警会刷屏)
m_TruncationWarned = true; m_TruncationWarned = true;
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated", ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated "
kMaxInstances); "(见 Scene Debug 面板红字)", kMaxInstances);
} }
m_Frame.collectMs = std::chrono::duration<f32, std::milli>( m_Frame.collectMs = std::chrono::duration<f32, std::milli>(