// // Created by atdunbg on 2026/9/23. // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Engine { using namespace ECS; // TransformComponent / Entity 等直接使用 namespace { // 相机裁剪范围(与 BuildFrame 中 GetProjection 的参数一致) constexpr f32 kCameraNear = 0.1f; constexpr f32 kCameraFar = 100.0f; // 实用分割插值因子(0=均匀,1=对数;0.6 是业界常用值) constexpr f32 kCascadeLambda = 0.6f; // Halton 低差异序列(TAA 子像素抖动) f32 Halton(u32 index, u32 base) { f32 f = 1.0f, r = 0.0f; while (index > 0) { f /= static_cast(base); r += f * static_cast(index % base); index /= base; } return r; } // ---- 骨骼动画采样(BuildFrame 第 2.5 节专用)---- // 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。 // 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵—— // FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢 void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot, glm::vec3& scl) { pos = glm::vec3(m[3]); scl = {glm::length(glm::vec3(m[0])), glm::length(glm::vec3(m[1])), glm::length(glm::vec3(m[2]))}; const glm::mat3 r(glm::vec3(m[0]) / scl.x, glm::vec3(m[1]) / scl.y, glm::vec3(m[2]) / scl.z); rot = glm::quat_cast(r); } // 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。 // key 数通常几十个,线性扫描对 cache 更友好 glm::vec3 SampleKeys(const std::vector& keys, f32 t, const glm::vec3& rest) { if (keys.empty()) return rest; if (t <= keys.front().time) return keys.front().value; for (size_t i = 0; i + 1 < keys.size(); ++i) { if (t < keys[i + 1].time) { const f32 a = keys[i].time, b = keys[i + 1].time; return glm::mix(keys[i].value, keys[i + 1].value, b > a ? (t - a) / (b - a) : 0.0f); } } return keys.back().value; } glm::vec3 SampleKeys(const std::vector& keys, f32 t, const glm::vec3& rest) { if (keys.empty()) return rest; if (t <= keys.front().time) return keys.front().value; for (size_t i = 0; i + 1 < keys.size(); ++i) { if (t < keys[i + 1].time) { const f32 a = keys[i].time, b = keys[i + 1].time; return glm::mix(keys[i].value, keys[i + 1].value, b > a ? (t - a) / (b - a) : 0.0f); } } return keys.back().value; } glm::quat SampleKeys(const std::vector& keys, f32 t, const glm::quat& rest) { if (keys.empty()) return rest; if (t <= keys.front().time) return keys.front().value; for (size_t i = 0; i + 1 < keys.size(); ++i) { if (t < keys[i + 1].time) { const f32 a = keys[i].time, b = keys[i + 1].time; return glm::slerp(keys[i].value, keys[i + 1].value, b > a ? (t - a) / (b - a) : 0.0f); } } return keys.back().value; } // 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)。 // touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或原样) void SampleLocalPose(const Model& model, const AnimationClip& clip, f32 time, std::vector& pos, std::vector& rot, std::vector& scl, std::vector& touched) { const auto& nodes = model.GetNodes(); pos.resize(nodes.size()); rot.resize(nodes.size()); scl.resize(nodes.size()); touched.assign(nodes.size(), 0); for (u32 i = 0; i < nodes.size(); ++i) { DecomposeTRS(nodes[i].localTransform, pos[i], rot[i], scl[i]); } if (clip.duration <= 0.0f) return; const f32 t = glm::mod(time, clip.duration); for (const auto& ch : clip.channels) { if (ch.nodeIdx >= nodes.size()) continue; touched[ch.nodeIdx] = 1; if (!ch.positions.empty()) pos[ch.nodeIdx] = SampleKeys(ch.positions, t, pos[ch.nodeIdx]); if (!ch.rotations.empty()) rot[ch.nodeIdx] = SampleKeys(ch.rotations, t, rot[ch.nodeIdx]); if (!ch.scales.empty()) scl[ch.nodeIdx] = SampleKeys(ch.scales, t, scl[ch.nodeIdx]); } } // 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算) void ComposeGlobals(const Model& model, const std::vector& pos, const std::vector& rot, const std::vector& scl, const std::vector& touched, std::vector& globals) { const auto& nodes = model.GetNodes(); globals.resize(nodes.size()); for (u32 i = 0; i < nodes.size(); ++i) { const glm::mat4 local = touched[i] ? glm::translate(glm::mat4(1.0f), pos[i]) * glm::mat4_cast(rot[i]) * glm::scale(glm::mat4(1.0f), scl[i]) : nodes[i].localTransform; globals[i] = nodes[i].parent >= 0 ? globals[static_cast(nodes[i].parent)] * local : local; } } } void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir) { constexpr u32 N = kMaxCascades; // ---- 1. 实用分割:均匀与对数分割按 λ 插值(视图深度,正值)---- f32 splits[N]; for (u32 i = 1; i <= N; ++i) { const f32 t = static_cast(i) / N; const f32 uniform = kCameraNear + (kCameraFar - kCameraNear) * t; const f32 log = kCameraNear * glm::pow(kCameraFar / kCameraNear, t); splits[i - 1] = glm::mix(uniform, log, kCascadeLambda); } for (u32 c = 0; c < N; ++c) { m_Frame.ubo.cascadeSplits[c] = splits[c]; } // ---- 2. 全视锥 8 角反投影到世界(NDC z: 0=近 1=远,Vulkan [0,1])---- const glm::mat4 invVP = glm::inverse(m_Frame.ubo.projNoJitter * m_Frame.ubo.view); glm::vec3 nearC[4], farC[4]; const glm::vec2 ndcXY[4] = {{-1,-1},{1,-1},{-1,1},{1,1}}; for (u32 i = 0; i < 4; ++i) { glm::vec4 p = invVP * glm::vec4(ndcXY[i], 0.0f, 1.0f); nearC[i] = glm::vec3(p / p.w); p = invVP * glm::vec4(ndcXY[i], 1.0f, 1.0f); farC[i] = glm::vec3(p / p.w); } // ---- 3. 每级联:切片角点 → 光空间 AABB 拟合 → 纹素对齐 ---- const glm::vec3 up = glm::abs(shadowDir.y) > 0.999f ? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f); for (u32 c = 0; c < N; ++c) { const f32 d0 = (c == 0) ? kCameraNear : splits[c - 1]; const f32 d1 = splits[c]; const f32 t0 = (d0 - kCameraNear) / (kCameraFar - kCameraNear); const f32 t1 = (d1 - kCameraNear) / (kCameraFar - kCameraNear); glm::vec3 slice[8]; for (u32 i = 0; i < 4; ++i) { slice[i] = glm::mix(nearC[i], farC[i], t0); slice[4 + i] = glm::mix(nearC[i], farC[i], t1); } // 光视图:位于切片中心,沿光方向看 glm::vec3 center(0.0f); for (const auto& p : slice) center += p; center *= (1.0f / 8.0f); const glm::mat4 lightView = glm::lookAt(center, center + shadowDir, up); glm::vec3 mn( std::numeric_limits::max()); glm::vec3 mx( std::numeric_limits::lowest()); for (const auto& p : slice) { const glm::vec3 lv = glm::vec3(lightView * glm::vec4(p, 1.0f)); mn = glm::min(mn, lv); 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(kCascadeRes); const f32 texelY = (mx.y - mn.y) / static_cast(kCascadeRes); mn.x = glm::floor(mn.x / texelX) * texelX; mn.y = glm::floor(mn.y / texelY) * texelY; mx.x = mn.x + glm::ceil((mx.x - mn.x) / texelX) * texelX; mx.y = mn.y + glm::ceil((mx.y - mn.y) / texelY) * texelY; // 光视图空间里 z 为负(看向 -Z);近远距离取正 glm::mat4 lightProj = glm::ortho(mn.x, mx.x, mn.y, mx.y, -mx.z, -mn.z); // Vulkan clip 深度 [0,1](ortho 的 w_clip=1,偏移折进 z 平移分量) lightProj[2][2] *= 0.5f; lightProj[3][2] = lightProj[3][2] * 0.5f + 0.5f; m_Frame.ubo.lightSpace[c] = lightProj * lightView; } } bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, vk::Extent2D targetExtent, f32 dt) { ZoneScopedN("BuildFrame"); const auto collectStart = std::chrono::steady_clock::now(); // ---- 0. TAA 时域状态先行(velocity = 上一帧姿态与相机)---- UniformBufferObject& ubo = m_Frame.ubo; ubo.prevView = m_LastView; ubo.prevProj = m_LastProjNoJitter; // ---- 1. 相机:渲染源按显示模式分派(UE 语义)。编辑器形态 // Scene view 恒用编辑器相机(视口 UI 状态,空场景可用); // 游戏形态用场景 primary 相机实体(游戏逻辑所有)---- ECS::TransformComponent* camTransform = nullptr; const Camera* camera = nullptr; if (m_DisplayMode == kDisplayViewport) { camTransform = &m_EditorCamera.GetTransform(); camera = &m_EditorCamera.GetController(); } else if (auto* cameraComp = scene.GetPrimaryCamera(&camTransform)) { camera = &cameraComp->camera; } if (!camera || !camTransform) { // 游戏形态无 primary 相机:本帧跳过(安全网) return false; // 拾取请求未消费,留待下一帧重试 } // 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。 // 放在相机校验之后:跳帧时请求保留,不丢点击 m_Frame.pickRequest = m_PendingPick; m_PendingPick.active = false; // 选中实体落帧(持续状态,不做一次性清除) m_Frame.selectedEntityBits = m_PendingSelected; const f32 aspect = static_cast(targetExtent.width) / static_cast(targetExtent.height); ubo.view = camera->GetView(*camTransform); ubo.proj = camera->GetProjection(aspect, 0.1f, 100.0f); // 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity) ubo.projNoJitter = ubo.proj; // 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。 // NDC 偏移 = 像素偏移 × 2 / 尺寸。TAA 关闭时停抖(直通画面静止) if (m_TAAEnabled) { const u32 idx = static_cast(m_FrameIndex % 8); const f32 jx = Halton(idx + 1, 2) - 0.5f; const f32 jy = Halton(idx + 1, 3) - 0.5f; ubo.proj[2][0] += jx * 2.0f / static_cast(targetExtent.width); ubo.proj[2][1] += jy * 2.0f / static_cast(targetExtent.height); } ubo.ambientColor = scene.GetSettings().AmbientColor; // ---- 运行时开关/调试视图落帧(各 pass 与 frag 只读契约字段)---- m_Frame.ClearColor = scene.GetSettings().ClearColor; m_Frame.DrawSkybox = scene.GetSettings().DrawSkybox; m_Frame.shadowsEnabled = m_ShadowsEnabled; m_Frame.taaEnabled = m_TAAEnabled; m_Frame.bloomEnabled = m_BloomEnabled; m_Frame.lodEnabled = m_LodEnabled; m_Frame.debugView = m_DebugView; m_Frame.displayMode = m_DisplayMode; ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f; // 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass; // 反投影远平面点 = 相机相对世界方向,frag normalize 后采样 cube m_Frame.skyInvVP = glm::inverse(ubo.projNoJitter * glm::mat4(glm::mat3(ubo.view))); // ---- 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]; f32 shadowKey[kMaxLights]; // 排序键:0=castShadow 方向光 1=方向光 2=点光 u32 collectedCount = 0; scene.Each( [&](Entity, TransformComponent& t, LightComponent& light) { if (!light.active) return; // 组件禁用 = 不参与光照 if (collectedCount >= kMaxLights) return; auto& dst = collected[collectedCount++]; dst.color = light.color; dst.params = glm::vec4( light.type == LightComponent::Type::Point ? 1.0f : 0.0f, light.radius, 0.0f, 0.0f); // 排序键:castShadow 方向光最优先(CSM 阴影光源契约),其余按类型 shadowKey[collectedCount - 1] = (light.type == LightComponent::Type::Directional && light.castShadow) ? 0.0f : (light.type == LightComponent::Type::Directional ? 1.0f : 2.0f); if (light.type == LightComponent::Type::Point) { dst.position = glm::vec4(t.GetPosition(), 1.0f); dst.direction = glm::vec4(0.0f); } else { glm::vec3 dir = glm::length(light.direction) > 0.0f ? glm::normalize(light.direction) : glm::vec3(0.0f); dst.direction = glm::vec4(dir, 0.0f); dst.position = glm::vec4(0.0f); } }); // 排序(稳定):castShadow 方向光 → 方向光 → 点光 // 同时排序 shadowKey 保持键值对应 for (u32 i = 1; i < collectedCount; ++i) { // 插入排序(n≤4) LightData ld = collected[i]; f32 key = shadowKey[i]; u32 j = i; while (j > 0 && shadowKey[j - 1] > key) { collected[j] = collected[j - 1]; shadowKey[j] = shadowKey[j - 1]; --j; } collected[j] = ld; shadowKey[j] = key; } 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); // ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走, // 否则被剔除的实体回屏时动画跳变)---- // palette 槽 0 固定 identity:静态模型/无动画实例的蒙皮直通 m_Frame.skinPalettes.clear(); m_Frame.skinPalettes.push_back(glm::mat4(1.0f)); m_Frame.skinOffsets.clear(); m_Frame.skinPaletteTruncated = false; m_EntitySkinBase.clear(); scene.Each( [&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) { if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动) if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL) const Model* model = assets.GetModel(mr.model); if (!model || !model->HasAnimations()) return; const auto& clips = model->GetClips(); // clip 下标合法性(模型资产可能已换) if (anim.clipIndex < 0 || anim.clipIndex >= static_cast(clips.size())) { anim.clipIndex = 0; anim.fadeFrom = -1; if (clips.empty()) return; } if (anim.fadeFrom >= static_cast(clips.size())) { anim.fadeFrom = -1; } // ---- 播放头推进(wrap/clamp)---- if (anim.playing && anim.speed != 0.0f) { const f32 d = dt * anim.speed; anim.time += d; if (anim.fadeFrom >= 0) anim.fadeFromTime += d; } const auto wrapTime = [&](f32 t, const AnimationClip& c) { if (c.duration <= 0.0f) return 0.0f; return anim.loop ? glm::mod(t, c.duration) : glm::clamp(t, 0.0f, c.duration - 1e-3f); }; const AnimationClip& cur = clips[anim.clipIndex]; anim.time = wrapTime(anim.time, cur); const AnimationClip* src = nullptr; f32 srcTime = 0.0f; f32 wCur = 1.0f; // 当前 clip 权重(crossfade 0→1) if (anim.fadeFrom >= 0) { // 淡出计时(暂停时冻结,与播放头一致) if (anim.playing) anim.fadeElapsed += dt; const f32 f = glm::min( anim.fadeElapsed / glm::max(anim.fadeDuration, 1e-4f), 1.0f); wCur = f * f * (3.0f - 2.0f * f); // smoothstep if (f >= 1.0f) { anim.fadeFrom = -1; // 过渡完成 } else { src = &clips[anim.fadeFrom]; anim.fadeFromTime = wrapTime(anim.fadeFromTime, *src); srcTime = anim.fadeFromTime; } } // ---- 采样 + 局部 TRS 层混合 → global → palette ---- SampleLocalPose(*model, cur, anim.time, m_PosePosA, m_PoseRotA, m_PoseSclA, m_TouchedA); if (src) { SampleLocalPose(*model, *src, srcTime, m_PosePosB, m_PoseRotB, m_PoseSclB, m_TouchedB); const size_t n = m_PosePosA.size(); for (size_t i = 0; i < n; ++i) { // touched = 两层轨道的并集(重建集合) m_TouchedA[i] = static_cast( m_TouchedA[i] | m_TouchedB[i]); m_PosePosA[i] = glm::mix(m_PosePosB[i], m_PosePosA[i], wCur); m_PoseSclA[i] = glm::mix(m_PoseSclB[i], m_PoseSclA[i], wCur); // 旋转必须 slerp(lerp 单位四元数会走样) m_PoseRotA[i] = glm::slerp(m_PoseRotB[i], m_PoseRotA[i], wCur); } } ComposeGlobals(*model, m_PosePosA, m_PoseRotA, m_PoseSclA, m_TouchedA, m_Globals); // palette bump 分配(按实体,不按模型去重——同模型两实体 // 可不同播放状态)。预算打满 → 该实体退回绑定姿势 const auto& bones = model->GetBones(); const u32 base = static_cast(m_Frame.skinPalettes.size()); if (base + bones.size() > kMaxSkinMatrices) { m_Frame.skinPaletteTruncated = true; if (!m_SkinTruncWarned) { m_SkinTruncWarned = true; ENGINE_WARN("GPUDriven: skin palette full ({}), " "animated entities fall back to bind pose", kMaxSkinMatrices); } return; } for (const auto& b : bones) { const u32 ni = b.nodeIdx < m_Globals.size() ? b.nodeIdx : 0; m_Frame.skinPalettes.push_back(m_Globals[ni] * b.invBind); } const u32 entKey = static_cast(entt::to_integral(entity)); m_EntitySkinBase[entKey] = base; }); // ---- 3. GPUDriven:单趟收集 + 多视图剔除 ---- // 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true ); // 每个实例的包围球对 4 个视锥分别测试: // 相机视锥 → 主视图(Forward 画) // 各级联光视锥 → 各自的阴影 batch(Shadow 按 region 各画各的) // 级联盒沿光方向加了 caster pad(见 ComputeCascades): // 盒外 caster 投进级联的影子不会被剔除裁掉。 // 收集按视图分桶,最后拼接成 [主段|级联0|级联1|级联2], // batch.first 拼接时加上段基址,保证段内区间连续。 m_Frame.instanceModels.clear(); m_Frame.prevInstanceModels.clear(); m_Frame.skinOffsets.clear(); m_Frame.instanceEntityIds.clear(); m_MainScratch.Clear(); for (auto& s : m_CascScratch) s.Clear(); m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建 u32 totalInstances = 0; bool instanceTruncated = false; // 剔除平面提取按 [0,1] 深度约定——相机 proj 与 lightSpace 都已 // 做 [0,1] 重映射;原始 [-1,1] 矩阵的 near 平面公式不同,勿用 const Frustum mainFrustum = Frustum::FromMatrix(ubo.proj * ubo.view); Frustum cascFrustum[kMaxCascades]; for (u32 c = 0; c < kMaxCascades; ++c) { cascFrustum[c] = Frustum::FromMatrix(ubo.lightSpace[c]); } scene.Each( [&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) { if (!mr.active) return; // 组件禁用 = 不渲染不拾取 if (!mr.model.Valid()) return; // 未指派 = 无效句柄(Get 对无效 FATAL) Model* model = assets.GetModel(mr.model); if (!model) return; // TAA velocity:本实例的上一帧世界矩阵(首帧/新实体 = 当前 // 矩阵 → 速度为 0,不产生假运动) const u32 entKey = static_cast(entt::to_integral(entity)); glm::mat4 prevWorld = t.World(); if (auto it = m_PrevWorlds.find(entKey); it != m_PrevWorlds.end()) { prevWorld = it->second; } m_CurWorlds.emplace(entKey, t.World()); // palette 基址(第 2.5 节预采样;无动画实体 = identity 槽) u32 skinBase = kIdentityPaletteSlot; if (auto it = m_EntitySkinBase.find(entKey); it != m_EntitySkinBase.end()) { skinBase = it->second; } // 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放) const f32 worldScale = glm::max( glm::length(glm::vec3(t.World()[0])), glm::max(glm::length(glm::vec3(t.World()[1])), glm::length(glm::vec3(t.World()[2])))); // ---- 实体级粗剔除(联合包围球):四视图全部看不到 → // 跳过整个 mesh 循环(多 mesh 模型的大头优化)---- const Mesh::Bounds& mb = model->GetBounds(); const glm::vec3 modelCenter = glm::vec3(t.World() * glm::vec4(mb.center, 1.0f)); const f32 modelRadius = mb.radius * worldScale; const bool mainVis = mainFrustum.IntersectsSphere(modelCenter, modelRadius); bool cascVis[kMaxCascades]; bool anyVis = mainVis; for (u32 c = 0; c < kMaxCascades; ++c) { cascVis[c] = cascFrustum[c].IntersectsSphere(modelCenter, modelRadius); anyVis |= cascVis[c]; } if (!anyVis) return; for (auto& mesh : model->GetMeshes()) { const auto& bounds = mesh->GetBounds(); const glm::vec3 worldCenter = glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f)); const f32 worldRadius = bounds.radius * worldScale; // LOD 选级:屏占比近似 = radius/距离(主视图按实例距离; // 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0 const f32 dist = glm::length(worldCenter - camTransform->GetPosition()); u32 mainLod = 0; if (m_LodEnabled) { if (dist >= worldRadius * 20.0f) { mainLod = 2; } else if (dist >= worldRadius * 10.0f) { mainLod = 1; } } auto tryAppend = [&](ViewScratch& s, const Frustum& frustum, u32 lod) { if (!frustum.IntersectsSphere(worldCenter, worldRadius)) { return; } if (totalInstances >= kMaxInstances) { instanceTruncated = true; return; } // batch 查找 = 数组寻址(Mesh::GetSlot 槽位 + LOD 槽) // batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch) const u32 slot = mesh->GetSlot(); if (slot >= s.lookup.size()) { s.lookup.resize(slot + 1); } auto& lodSlots = s.lookup[slot]; u32 batchIdx = lodSlots[lod]; if (batchIdx == 0) { batchIdx = static_cast(s.result.batches.size()) + 1; lodSlots[lod] = batchIdx; s.result.batches.push_back({mesh.get(), 0, 0, lod}); s.batchMats.emplace_back(); s.batchDistSum.push_back(0.0f); } // 实例归入自己的 batch(mesh 分组),保证同 batch // 实例在扁平化后连续——firstInstance 语义的前提 const u32 bi = batchIdx - 1; s.result.batches[bi].count++; s.batchMats[bi].push_back( {t.World(), prevWorld, skinBase, entKey}); s.batchDistSum[bi] += dist; // 透明排序用 s.result.instanceCount++; ++totalInstances; }; // 透明材质 → 独立队列(主视图,混合绘制 + 排序; // 不进阴影——alpha 阴影复杂度高收益低,注释后续演进) const bool isTransparentMesh = mesh->GetMaterial()->IsTransparent(); ViewScratch& mainTarget = isTransparentMesh ? m_TranslucentScratch : m_MainScratch; tryAppend(mainTarget, mainFrustum, mainLod); if (!isTransparentMesh) { for (u32 c = 0; c < kMaxCascades; ++c) { if (!cascVis[c]) continue; // 粗剔除已判不可见 // 级联降级:越远的级联覆盖范围越大,用越低的 LOD //(阴影本来就是低频信息,误差可接受);开关关闭 = 恒 LOD0 tryAppend(m_CascScratch[c], cascFrustum[c], m_LodEnabled ? std::min(c, kMeshLodCount - 1) : 0u); } } } }); // ---- 4. 拼接:[主段|级联0|级联1|级联2],batch 内实例连续扁平化 ---- auto appendView = [&](CullResult& dst, ViewScratch& s) { u32 base = static_cast(m_Frame.instanceModels.size()); dst.batches.clear(); dst.batches.reserve(s.result.batches.size()); for (size_t i = 0; i < s.result.batches.size(); ++i) { InstanceBatch b = s.result.batches[i]; b.first = base; // 扁平化后的连续区间基址 base += b.count; dst.batches.push_back(b); for (const auto& m : s.batchMats[i]) { m_Frame.instanceModels.push_back(m.cur); m_Frame.prevInstanceModels.push_back(m.prev); // 与实例序平行对齐(shader 按 gl_InstanceIndex 取) m_Frame.skinOffsets.push_back(m.skinBase); // 实体位模式同序对齐(HitProxy id pass 消费) m_Frame.instanceEntityIds.push_back(m.entityBits); } } dst.instanceCount = s.result.instanceCount; }; appendView(m_Frame.mainView, m_MainScratch); for (u32 c = 0; c < kMaxCascades; ++c) { appendView(m_Frame.shadowCascades[c], m_CascScratch[c]); } // ---- 4.5 MDI 打包(已下线:移动视角下存在几何消失缺陷,待隔离修复)---- // PackIndirect(m_Frame.mainView, true); // for (u32 c = 0; c < kMaxCascades; ++c) { // PackIndirect(m_Frame.shadowCascades[c], false); // } // ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排 { auto& tb = m_TranslucentScratch.result.batches; if (tb.size() > 1) { std::vector order(tb.size()); std::iota(order.begin(), order.end(), 0); std::sort(order.begin(), order.end(), [&](u32 a, u32 b) { return m_TranslucentScratch.batchDistSum[a] / tb[a].count > m_TranslucentScratch.batchDistSum[b] / tb[b].count; }); CullResult sorted; sorted.instanceCount = m_TranslucentScratch.result.instanceCount; std::vector sortedMats; std::vector sortedDist; for (const u32 i : order) { sorted.batches.push_back(tb[i]); sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]); for (auto& m : m_TranslucentScratch.batchMats[i]) { sortedMats.push_back(std::move(m)); } } m_TranslucentScratch.result = std::move(sorted); m_TranslucentScratch.batchDistSum = std::move(sortedDist); // batchMats 按新分组的 count 顺序切分重装(保留容量) m_TranslucentScratch.batchMats.assign( m_TranslucentScratch.result.batches.size(), {}); u32 offset = 0; for (size_t i = 0; i < m_TranslucentScratch.result.batches.size(); ++i) { const u32 n = m_TranslucentScratch.result.batches[i].count; m_TranslucentScratch.batchMats[i].assign( std::make_move_iterator(sortedMats.begin() + offset), std::make_move_iterator(sortedMats.begin() + offset + n)); offset += n; } } } appendView(m_Frame.translucentView, m_TranslucentScratch); // ---- 5.5 选中描边:从扁平实例表筛出选中实体的单实例绘制 ---- //(不透明 + 透明队列都查——选中玻璃也要有描边)。 // 无选中时保持空表,SelectionMask pass 只 clear 不绘制 m_Frame.selectionDraws.clear(); if (m_Frame.selectedEntityBits != 0) { auto collect = [&](const CullResult& view) { for (const auto& b : view.batches) { for (u32 i = 0; i < b.count; ++i) { const u32 idx = b.first + i; if (m_Frame.instanceEntityIds[idx] == m_Frame.selectedEntityBits) { m_Frame.selectionDraws.push_back( SelectionDraw{b.mesh, idx, b.lod}); } } } }; collect(m_Frame.mainView); collect(m_Frame.translucentView); } // 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出) m_PrevWorlds.swap(m_CurWorlds); m_CurWorlds.clear(); m_LastView = ubo.view; m_LastProjNoJitter = ubo.projNoJitter; ++m_FrameIndex; // 截断状态每帧落遥测(ImGui 可见)——"只告警一次"的日志对 // 大场景不够:预算是动态穿越的,用户必须实时看到截断发生 m_Frame.instancesTruncated = instanceTruncated; if (instanceTruncated && !m_TruncationWarned) { m_TruncationWarned = true; ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated " "(见 Scene Debug 面板红字)", kMaxInstances); } m_Frame.collectMs = std::chrono::duration( std::chrono::steady_clock::now() - collectStart).count(); return true; } void SceneRenderer::PackIndirect(CullResult& view, bool byMaterial) { view.indirectCommands.clear(); view.indirectGroups.clear(); if (view.batches.empty()) return; // 组键排序:megabuffer(VB 句柄)→ 材质(可选)→ mesh 槽位。 // 同 Model 的全部 mesh 共享同一 megabuffer,材质是主分组轴; // 槽位兜底保证排序稳定(组内顺序无语义,防逐帧抖动) std::sort(view.batches.begin(), view.batches.end(), [byMaterial](const InstanceBatch& a, const InstanceBatch& b) { const u64 va = reinterpret_cast(static_cast( a.mesh->GetVertexBuffer())); const u64 vb = reinterpret_cast(static_cast( b.mesh->GetVertexBuffer())); if (va != vb) return va < vb; if (byMaterial && a.mesh->GetMaterial() != b.mesh->GetMaterial()) { return reinterpret_cast(a.mesh->GetMaterial()) < reinterpret_cast(b.mesh->GetMaterial()); } return a.mesh->GetSlot() < b.mesh->GetSlot(); }); for (const auto& b : view.batches) { const vk::Buffer vb = b.mesh->GetVertexBuffer(); Material* mat = byMaterial ? b.mesh->GetMaterial() : nullptr; bool newGroup = view.indirectGroups.empty(); if (!newGroup) { const IndirectGroup& g = view.indirectGroups.back(); newGroup = g.mesh->GetVertexBuffer() != vb || g.material != mat; } if (newGroup) { view.indirectGroups.push_back({b.mesh, mat, static_cast(view.indirectCommands.size()), 0}); } view.indirectCommands.push_back({ b.mesh->GetLodIndexCount(b.lod), b.count, b.mesh->GetGlobalFirstIndex(b.lod), b.mesh->GetVertexOffset(), b.first}); ++view.indirectGroups.back().commandCount; } } } // namespace Engine