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
+59 -30
View File
@@ -100,6 +100,13 @@ namespace Engine
mx = glm::max(mx, lv);
}
// 沿光方向扩展深度范围:切片盒外的 caster(如高树、城墙)
// 也能投影子进本级联——per-cascade 剔除的正确性前提。
// 只扩 z 不缩 XY,阴影分辨率不受影响
constexpr f32 kCasterPad = 30.0f;
mn.z -= kCasterPad;
mx.z += kCasterPad;
// 纹素对齐:X/Y 边界量化到纹素网格(消除相机移动时的阴影边缘闪烁)
const f32 texelX = (mx.x - mn.x) / static_cast<f32>(kCascadeRes);
const f32 texelY = (mx.y - mn.y) / static_cast<f32>(kCascadeRes);
@@ -179,28 +186,28 @@ namespace Engine
ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camera.GetPosition(), 1.0f);
// ---- 3. GPUDriven:单趟收集 + 双视图剔除 ----
// 每个实例对相机视锥、光源视锥分别做包围球测试:
// 进相机视锥 → 主视图 batch(Forward 画)
// 进光源视锥 → 阴影 batch(Shadow 画)——相机外实体仍可投影子
// 实例矩阵数组 = [主视图段 | 阴影视图段],batch.first 为全局下标。
// 哈希聚合保证各视图内 batch 区间连续。
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 每个实例的包围球对 4 个视锥分别测试:
// 相机视锥 → 主视图(Forward 画)
// 各级联光视锥 → 各自的阴影 batch(Shadow 按 region 各画各的)
// 级联盒沿光方向加了 caster pad(见 ComputeCascades):
// 盒外 caster 投进级联的影子不会被剔除裁掉。
// 收集按视图分桶,最后拼接成 [主段|级联0|级联1|级联2],
// batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear();
m_Frame.mainView.batches.clear();
m_Frame.shadowView.batches.clear();
m_Frame.mainView.instanceCount = 0;
m_Frame.shadowView.instanceCount = 0;
m_MainLookup.clear();
m_ShadowLookup.clear();
m_MainScratch.Clear();
for (auto& s : m_CascScratch) s.Clear();
u32 totalInstances = 0;
bool instanceTruncated = false;
// 剔除平面提取按 [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 shadowFrustum =
Frustum::FromMatrix(ubo.lightSpace[kMaxCascades - 1]);
Frustum cascFrustum[kMaxCascades];
for (u32 c = 0; c < kMaxCascades; ++c) {
cascFrustum[c] = Frustum::FromMatrix(ubo.lightSpace[c]);
}
scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
@@ -228,33 +235,55 @@ namespace Engine
glm::vec3(t.world * glm::vec4(bounds.center, 1.0f));
const f32 worldRadius = bounds.radius * worldScale;
auto tryAppend = [&](CullResult& view, const Frustum& frustum,
std::unordered_map<Mesh*, u32>& lookup) {
auto tryAppend = [&](ViewScratch& s, const Frustum& frustum) {
if (!frustum.IntersectsSphere(worldCenter, worldRadius)) {
return;
}
if (m_Frame.instanceModels.size() >= kMaxInstances) {
if (totalInstances >= kMaxInstances) {
instanceTruncated = true;
return;
}
const auto [it, inserted] = lookup.emplace(
mesh.get(), static_cast<u32>(view.batches.size()));
const auto [it, inserted] = s.lookup.emplace(
mesh.get(), static_cast<u32>(s.worlds.size()));
if (inserted) {
view.batches.push_back({mesh.get(),
static_cast<u32>(m_Frame.instanceModels.size()), 0});
s.result.batches.push_back({mesh.get(),
static_cast<u32>(s.worlds.size()), 0});
}
view.batches[it->second].count++;
view.instanceCount++;
// 两个数组平行 push:同下标 = 同一实例的当前/上一帧姿态
m_Frame.instanceModels.push_back(t.world);
m_Frame.prevInstanceModels.push_back(prevWorld);
s.result.batches[it->second].count++;
s.result.instanceCount++;
s.worlds.push_back(t.world);
s.prevWorlds.push_back(prevWorld);
++totalInstances;
};
tryAppend(m_Frame.mainView, mainFrustum, m_MainLookup);
tryAppend(m_Frame.shadowView, shadowFrustum, m_ShadowLookup);
tryAppend(m_MainScratch, mainFrustum);
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(每帧重建,销毁的实体自动清出)
m_PrevWorlds.swap(m_CurWorlds);
m_CurWorlds.clear();