perf: CSM 每级联独立剔除 + 光向 caster pad

- 场景剔除从 [主|阴影] 两视图扩展为 [主|级联0|级联1|级联2] 四视图:
  每级联只画自己盒内的 batch,消除原先 3× 全量阴影 draw 的浪费
- 实例数组拼接为四段,batch.first 加段基址保持段内连续;
  ViewScratch 分桶收集复用容量,稳态零堆分配
- ComputeCascades 沿光方向扩展级联深度范围(pad=30)——切片盒外
  caster(高树/城墙)投影进级联的影子不会被剔除裁掉,per-cascade
  剔除正确性的前提;只扩 z 不缩 XY,分辨率不受影响
- 遥测 shadow 计数 = 各级联求和
- SunTemple 1056 mesh 全量加载,validation 零警告
This commit is contained in:
2026-09-23 14:59:08 +08:00
parent c54f60c5ea
commit 0ed7b4c5b8
4 changed files with 87 additions and 36 deletions
+24 -4
View File
@@ -41,7 +41,12 @@ namespace Engine {
// ---- 遥测 ---- // ---- 遥测 ----
u32 GetMainInstanceCount() const { return m_Frame.mainView.instanceCount; } u32 GetMainInstanceCount() const { return m_Frame.mainView.instanceCount; }
u32 GetShadowInstanceCount() const { return m_Frame.shadowView.instanceCount; } u32 GetShadowInstanceCount() const
{
u32 sum = 0;
for (const auto& c : m_Frame.shadowCascades) sum += c.instanceCount;
return sum;
}
f32 GetCollectMs() const { return m_Frame.collectMs; } f32 GetCollectMs() const { return m_Frame.collectMs; }
private: private:
@@ -52,9 +57,24 @@ namespace Engine {
FrameSceneData m_Frame; FrameSceneData m_Frame;
// batch 聚合查找表(mesh → batch 下标),每帧 clear 复用桶 // ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
std::unordered_map<Mesh*, u32> m_MainLookup; struct ViewScratch {
std::unordered_map<Mesh*, u32> m_ShadowLookup; CullResult result;
std::vector<glm::mat4> worlds;
std::vector<glm::mat4> prevWorlds;
std::unordered_map<Mesh*, u32> lookup;
void Clear()
{
result.batches.clear();
result.instanceCount = 0;
worlds.clear();
prevWorlds.clear();
lookup.clear();
}
};
ViewScratch m_MainScratch;
ViewScratch m_CascScratch[kMaxCascades];
// ---- TAA 时域状态 ---- // ---- TAA 时域状态 ----
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样) u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
+3 -1
View File
@@ -71,7 +71,9 @@ namespace Engine {
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵 // 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
std::vector<glm::mat4> prevInstanceModels; std::vector<glm::mat4> prevInstanceModels;
CullResult mainView; // 相机视锥 → Forward pass CullResult mainView; // 相机视锥 → Forward pass
CullResult shadowView; // 光源视锥 → Shadow pass(相机外实体仍投影子) // 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
CullResult shadowCascades[kMaxCascades];
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测) f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
}; };
+1 -1
View File
@@ -92,7 +92,7 @@ namespace Engine::Passes
0, sizeof(glm::mat4), 0, sizeof(glm::mat4),
&m_Frame.ubo.lightSpace[c]); &m_Frame.ubo.lightSpace[c]);
for (const auto& batch : m_Frame.shadowView.batches) { for (const auto& batch : m_Frame.shadowCascades[c].batches) {
batch.mesh->Draw(cmd, batch.count, batch.first); batch.mesh->Draw(cmd, batch.count, batch.first);
} }
+59 -30
View File
@@ -100,6 +100,13 @@ namespace Engine
mx = glm::max(mx, lv); mx = glm::max(mx, lv);
} }
// 沿光方向扩展深度范围:切片盒外的 caster(如高树、城墙)
// 也能投影子进本级联——per-cascade 剔除的正确性前提。
// 只扩 z 不缩 XY,阴影分辨率不受影响
constexpr f32 kCasterPad = 30.0f;
mn.z -= kCasterPad;
mx.z += kCasterPad;
// 纹素对齐:X/Y 边界量化到纹素网格(消除相机移动时的阴影边缘闪烁) // 纹素对齐:X/Y 边界量化到纹素网格(消除相机移动时的阴影边缘闪烁)
const f32 texelX = (mx.x - mn.x) / static_cast<f32>(kCascadeRes); const f32 texelX = (mx.x - mn.x) / static_cast<f32>(kCascadeRes);
const f32 texelY = (mx.y - mn.y) / static_cast<f32>(kCascadeRes); const f32 texelY = (mx.y - mn.y) / static_cast<f32>(kCascadeRes);
@@ -179,28 +186,28 @@ namespace Engine
ComputeCascades(shadowDir); ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camera.GetPosition(), 1.0f); ubo.cameraPos = glm::vec4(camera.GetPosition(), 1.0f);
// ---- 3. GPUDriven:单趟收集 + 双视图剔除 ---- // ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 每个实例对相机视锥、光源视锥分别做包围球测试: // 每个实例的包围球对 4 个视锥分别测试:
// 进相机视锥 → 主视图 batch(Forward 画) // 相机视锥 → 主视图(Forward 画)
// 进光源视锥 → 阴影 batch(Shadow 画)——相机外实体仍可投影子 // 各级联光视锥 → 各自的阴影 batch(Shadow 按 region 各画各的)
// 实例矩阵数组 = [主视图段 | 阴影视图段],batch.first 为全局下标。 // 级联盒沿光方向加了 caster pad(见 ComputeCascades):
// 哈希聚合保证各视图内 batch 区间连续。 // 盒外 caster 投进级联的影子不会被剔除裁掉。
// 收集按视图分桶,最后拼接成 [主段|级联0|级联1|级联2],
// batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear(); m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear(); m_Frame.prevInstanceModels.clear();
m_Frame.mainView.batches.clear(); m_MainScratch.Clear();
m_Frame.shadowView.batches.clear(); for (auto& s : m_CascScratch) s.Clear();
m_Frame.mainView.instanceCount = 0; u32 totalInstances = 0;
m_Frame.shadowView.instanceCount = 0;
m_MainLookup.clear();
m_ShadowLookup.clear();
bool instanceTruncated = false; bool instanceTruncated = false;
// 剔除平面提取按 [0,1] 深度约定——相机 proj 与 lightSpace 都已 // 剔除平面提取按 [0,1] 深度约定——相机 proj 与 lightSpace 都已
// 做 [0,1] 重映射;原始 [-1,1] 矩阵的 near 平面公式不同,勿用。 // 做 [0,1] 重映射;原始 [-1,1] 矩阵的 near 平面公式不同,勿用
// 光源视锥用最远级联(覆盖全阴影范围)
const Frustum mainFrustum = Frustum::FromMatrix(ubo.proj * ubo.view); const Frustum mainFrustum = Frustum::FromMatrix(ubo.proj * ubo.view);
const Frustum shadowFrustum = Frustum cascFrustum[kMaxCascades];
Frustum::FromMatrix(ubo.lightSpace[kMaxCascades - 1]); for (u32 c = 0; c < kMaxCascades; ++c) {
cascFrustum[c] = Frustum::FromMatrix(ubo.lightSpace[c]);
}
scene.Each<TransformComponent, MeshRendererComponent>( scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) { [&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
@@ -228,33 +235,55 @@ namespace Engine
glm::vec3(t.world * glm::vec4(bounds.center, 1.0f)); glm::vec3(t.world * glm::vec4(bounds.center, 1.0f));
const f32 worldRadius = bounds.radius * worldScale; const f32 worldRadius = bounds.radius * worldScale;
auto tryAppend = [&](CullResult& view, const Frustum& frustum, auto tryAppend = [&](ViewScratch& s, const Frustum& frustum) {
std::unordered_map<Mesh*, u32>& lookup) {
if (!frustum.IntersectsSphere(worldCenter, worldRadius)) { if (!frustum.IntersectsSphere(worldCenter, worldRadius)) {
return; return;
} }
if (m_Frame.instanceModels.size() >= kMaxInstances) { if (totalInstances >= kMaxInstances) {
instanceTruncated = true; instanceTruncated = true;
return; return;
} }
const auto [it, inserted] = lookup.emplace( const auto [it, inserted] = s.lookup.emplace(
mesh.get(), static_cast<u32>(view.batches.size())); mesh.get(), static_cast<u32>(s.worlds.size()));
if (inserted) { if (inserted) {
view.batches.push_back({mesh.get(), s.result.batches.push_back({mesh.get(),
static_cast<u32>(m_Frame.instanceModels.size()), 0}); static_cast<u32>(s.worlds.size()), 0});
} }
view.batches[it->second].count++; s.result.batches[it->second].count++;
view.instanceCount++; s.result.instanceCount++;
// 两个数组平行 push:同下标 = 同一实例的当前/上一帧姿态 s.worlds.push_back(t.world);
m_Frame.instanceModels.push_back(t.world); s.prevWorlds.push_back(prevWorld);
m_Frame.prevInstanceModels.push_back(prevWorld); ++totalInstances;
}; };
tryAppend(m_Frame.mainView, mainFrustum, m_MainLookup); tryAppend(m_MainScratch, mainFrustum);
tryAppend(m_Frame.shadowView, shadowFrustum, m_ShadowLookup); for (u32 c = 0; c < kMaxCascades; ++c) {
tryAppend(m_CascScratch[c], cascFrustum[c]);
}
} }
}); });
// ---- 4. 拼接:[主段|级联0|级联1|级联2],batch.first 加段基址 ----
auto appendView = [&](CullResult& dst, ViewScratch& s) {
const u32 base = static_cast<u32>(m_Frame.instanceModels.size());
dst.batches.clear();
dst.batches.reserve(s.result.batches.size());
for (auto& b : s.result.batches) {
b.first += base;
dst.batches.push_back(b);
}
dst.instanceCount = s.result.instanceCount;
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(),
s.worlds.begin(), s.worlds.end());
m_Frame.prevInstanceModels.insert(m_Frame.prevInstanceModels.end(),
s.prevWorlds.begin(),
s.prevWorlds.end());
};
appendView(m_Frame.mainView, m_MainScratch);
for (u32 c = 0; c < kMaxCascades; ++c) {
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
}
// 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出) // 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出)
m_PrevWorlds.swap(m_CurWorlds); m_PrevWorlds.swap(m_CurWorlds);
m_CurWorlds.clear(); m_CurWorlds.clear();