// // 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 #include #include namespace Engine { using namespace ECS; // TransformComponent / Entity 等直接使用 namespace { // 相机裁剪范围不再走常量:每实体相机自带 near/far(Camera.h // 默认值 0.1/10000,UE 语义:near 主导深度精度,far 拉远到 // "无限"级)。阴影距离独立(UE DirectionalLight 的 shadow // distance 同构):级联分割只覆盖 kShadowDistance,更远无阴影, // 避免级联跟着 far 拉爆近处阴影分辨率 constexpr f32 kShadowDistance = 100.0f; // 实用分割插值因子(0=均匀,1=对数;0.6 是业界常用值) constexpr f32 kCascadeLambda = 0.6f; #ifdef HELLO_EDITOR // ---- Editor gizmo 收集常量(CollectEditorGizmos 专用)---- // 图标屏幕占比(相对视口高):恒定屏幕尺寸 = dist·tanHalf·frac constexpr f32 kIconScreenFrac = 0.04f; // 代理棱边屏宽(像素):HitProxy 可点选的 ribbon 宽度 constexpr f32 kProxyWidthPx = 4.0f; // 视锥线框可视远平面截断:far clip(10000m)全尺寸线框横贯场景 // 无信息量,可视化统一用固定短距离(渲染语义不变,纯显示) constexpr f32 kGizmoFrustumVisFar = 3.0f; // 视锥棱 ribbon 细分段数(恒定屏宽的折线逼近) constexpr u32 kRibbonSegsPerEdge = 12; // 点光选中时的衰减半径圆段数 constexpr u32 kRadiusCircleSegs = 24; // billboard 形状 id(gizmo_billboard.frag 的 SDF 分支约定) constexpr f32 kShapeDirLight = 0.0f; constexpr f32 kShapePointLight = 1.0f; constexpr f32 kShapeCamera = 2.0f; constexpr f32 kShapeSkybox = 3.0f; constexpr f32 kShapeSkyLight = 4.0f; #endif // 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; } } void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir, const f32 camNear, const f32 camFar) { constexpr u32 N = kMaxCascades; // ---- 1. 实用分割:均匀与对数分割按 λ 插值(视图深度,正值)---- //(分割覆盖 [camNear, kShadowDistance];camFar 只管主投影裁剪) f32 splits[N]; for (u32 i = 1; i <= N; ++i) { const f32 t = static_cast(i) / N; const f32 uniform = camNear + (kShadowDistance - camNear) * t; const f32 log = camNear * glm::pow(kShadowDistance / camNear, 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) ? camNear : splits[c - 1]; const f32 d1 = splits[c]; // 视图深度 → 视锥棱边参数:棱边上 z_view 线性(near/far 角点 // 都在各自平面上),farC 反投影自主投影 far 面,故 t 相对 // camFar 归一;splits 只覆盖 [camNear, kShadowDistance], // t 必然远小于 1 → 自然截出阴影范围内的切片 const f32 t0 = (d0 - camNear) / (camFar - camNear); const f32 t1 = (d1 - camNear) / (camFar - camNear); 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; } } #ifdef HELLO_EDITOR // ---- Editor gizmo 收集(编辑形态专属,只读场景写 editor 段)---- void SceneRenderer::CollectEditorGizmos(ECS::Scene& scene, const glm::vec3& camPos, const glm::vec3& camRight, const glm::vec3& camUp, f32 aspect, f32 viewHeightPx, f32 viewTanHalf) { auto& icons = m_Frame.gizmoIconVerts; auto& wires = m_Frame.gizmoWireVerts; auto& proxies = m_Frame.gizmoProxyVerts; icons.clear(); wires.clear(); proxies.clear(); constexpr f32 kTwoPi = 6.28318530718f; // viewTanHalf:编辑器视角相机的 tan(fov/2)(BuildFrame 传入), // 只服务 billboard 恒定屏占比与 proxy ribbon 恒定像素宽——与 // 相机实体自己的 fov 无关 const f32 tanHalf = viewTanHalf; const bool anySelected = m_Frame.selectedEntityValid; const u32 selBits = m_Frame.selectedEntityBits; auto IsSel = [&](u32 bits) { return anySelected && bits == selBits; }; // 预算哨兵(一次性 WARN 复用截断标志语义) bool truncated = false; auto Fit = [](u32 cur, u32 add, u32 cap) { return cur + add <= cap; }; // 图标 billboard:面向相机 quad(2 三角)+ 同角点进代理流(可点选) auto EmitIcon = [&](const glm::vec3& pos, f32 shape, const glm::vec4& color, u32 bits, f32 scaleMul) { if (!Fit(static_cast(icons.size()), 6, kMaxGizmoIconVerts) || !Fit(static_cast(proxies.size()), 6, kMaxGizmoProxyVerts)) { truncated = true; return; } const f32 dist = std::max(glm::length(pos - camPos), 0.1f); const f32 half = dist * tanHalf * kIconScreenFrac * scaleMul; const glm::vec3 r = camRight * half; const glm::vec3 u = camUp * half; const glm::vec3 p00 = pos - r - u, p10 = pos + r - u; const glm::vec3 p11 = pos + r + u, p01 = pos - r + u; const auto push = [&](std::vector& v, const glm::vec3& p, f32 lu, f32 lv) { v.push_back({p, color, glm::vec4(shape, lu, lv, 0.0f), bits}); }; // 三角表:v00 v10 v11 / v00 v11 v01(uv 对应角点) push(icons, p00, -1, -1); push(icons, p10, 1, -1); push(icons, p11, 1, 1); push(icons, p00, -1, -1); push(icons, p11, 1, 1); push(icons, p01, -1, 1); // 代理壳颜色占位(拾取 frag 只写 entityId) const glm::vec4 any{1.0f}; const auto pushP = [&](const glm::vec3& p) { proxies.push_back({p, any, glm::vec4(shape, 0, 0, 0), bits}); }; pushP(p00); pushP(p10); pushP(p11); pushP(p00); pushP(p11); pushP(p01); }; // 线段(LineList 成对 push) auto EmitLine = [&](const glm::vec3& a, const glm::vec3& b, const glm::vec4& color) { if (!Fit(static_cast(wires.size()), 2, kMaxGizmoWireVerts)) { truncated = true; return; } wires.push_back({a, color}); wires.push_back({b, color}); }; // 拾取代理 ribbon:棱边 ab 细分 N 段,每段以段中点为基准的面向 // 相机 quad(半宽恒定 ~kProxyWidthPx 屏幕像素)。线段在 R32Uint // 光栅化里点不中,quad 才可拾取 auto EmitRibbon = [&](const glm::vec3& a, const glm::vec3& b, u32 bits) { const glm::vec3 ab = b - a; const f32 len = glm::length(ab); if (len < 1e-4f) return; const glm::vec3 dir = ab / len; for (u32 s = 0; s < kRibbonSegsPerEdge; ++s) { const f32 t0 = static_cast(s) / kRibbonSegsPerEdge; const f32 t1 = static_cast(s + 1) / kRibbonSegsPerEdge; const glm::vec3 m = a + ab * (0.5f * (t0 + t1)); // 段中点 const glm::vec3 toCam = camPos - m; const f32 dist = std::max(glm::length(toCam), 0.1f); glm::vec3 side = glm::cross(dir, toCam / dist); const f32 sideLen = glm::length(side); if (!(sideLen > 1e-6f) || !(std::isfinite(side.x) && std::isfinite(side.y) && std::isfinite(side.z))) { side = camUp; // 棱正对相机退化:用屏幕 up 兜底 } else { side /= sideLen; } const f32 half = dist * tanHalf * (kProxyWidthPx / viewHeightPx); const glm::vec3 halfDir = dir * (len / kRibbonSegsPerEdge * 0.5f); const glm::vec3 halfSide = side * half; if (!Fit(static_cast(proxies.size()), 6, kMaxGizmoProxyVerts)) { truncated = true; return; } const glm::vec4 any{1.0f}; const glm::vec3 p0 = m - halfDir - halfSide; const glm::vec3 p1 = m + halfDir - halfSide; const glm::vec3 p2 = m + halfDir + halfSide; const glm::vec3 p3 = m - halfDir + halfSide; proxies.push_back({p0, any, glm::vec4(0, 0, 0, 0), bits}); proxies.push_back({p1, any, glm::vec4(0, 0, 0, 0), bits}); proxies.push_back({p2, any, glm::vec4(0, 0, 0, 0), bits}); proxies.push_back({p0, any, glm::vec4(0, 0, 0, 0), bits}); proxies.push_back({p2, any, glm::vec4(0, 0, 0, 0), bits}); proxies.push_back({p3, any, glm::vec4(0, 0, 0, 0), bits}); } }; // 相机实体视锥线框(near 真值 / 远平面可视截断)+ 棱 ribbon。 // 几何取实体相机自己的 fov/near/orthoSize(Inspector 参数改动 // 即时反映到线框),aspect 用编辑器视口比例(可视化近似) auto EmitFrustum = [&](const ECS::TransformComponent& t, const ECS::CameraComponent& cc, const glm::vec4& lineColor, u32 bits) { const glm::mat4 W = t.World(); const auto Xform = [&](f32 x, f32 y, f32 z) { return glm::vec3(W * glm::vec4(x, y, z, 1.0f)); }; // 本地 -Z 为视线方向(Camera 单一事实源约定) const f32 camNear = cc.camera.GetNearZ(); // 可视远平面不小于 near 的 1.5 倍:用户把 near 拖大时防止 // 远矩形反而落在 near 之前(线框翻转) const f32 visFar = glm::max(kGizmoFrustumVisFar, camNear * 1.5f); f32 nH = 0.0f, vH = 0.0f; if (cc.camera.GetProjectionMode() == Camera::ProjectionMode::Orthographic) { // 正交视锥 = 长方体:near/远矩形同宽高(无透视收缩) nH = cc.camera.GetOrthoSize(); vH = nH; } else { const f32 tanHalfEnt = std::tan(glm::radians(cc.camera.GetFovDeg()) * 0.5f); nH = tanHalfEnt * camNear; vH = tanHalfEnt * visFar; } const f32 nW = nH * aspect; const f32 vW = vH * aspect; const glm::vec3 n0 = Xform(-nW, -nH, -camNear); const glm::vec3 n1 = Xform(nW, -nH, -camNear); const glm::vec3 n2 = Xform(nW, nH, -camNear); const glm::vec3 n3 = Xform(-nW, nH, -camNear); const glm::vec3 f0 = Xform(-vW, -vH, -visFar); const glm::vec3 f1 = Xform(vW, -vH, -visFar); const glm::vec3 f2 = Xform(vW, vH, -visFar); const glm::vec3 f3 = Xform(-vW, vH, -visFar); // near 矩形 + 可视远矩形 + 4 棱 + 上轴刻度 EmitLine(n0, n1, lineColor); EmitLine(n1, n2, lineColor); EmitLine(n2, n3, lineColor); EmitLine(n3, n0, lineColor); EmitLine(f0, f1, lineColor); EmitLine(f1, f2, lineColor); EmitLine(f2, f3, lineColor); EmitLine(f3, f0, lineColor); EmitLine(n0, f0, lineColor); EmitLine(n1, f1, lineColor); EmitLine(n2, f2, lineColor); EmitLine(n3, f3, lineColor); EmitLine(Xform(0.0f, nH, -camNear), Xform(0.0f, nH + 0.3f, -camNear), lineColor); // 12 棱的拾取 ribbon(图标 quad 之外的可点选面积) EmitRibbon(n0, n1, bits); EmitRibbon(n1, n2, bits); EmitRibbon(n2, n3, bits); EmitRibbon(n3, n0, bits); EmitRibbon(f0, f1, bits); EmitRibbon(f1, f2, bits); EmitRibbon(f2, f3, bits); EmitRibbon(f3, f0, bits); EmitRibbon(n0, f0, bits); EmitRibbon(n1, f1, bits); EmitRibbon(n2, f2, bits); EmitRibbon(n3, f3, bits); }; // 灯光:方向光 = 太阳盘 + 照射方向箭头;点光 = 光晕盘 //(选中追加衰减半径三正交圆,UE 语义——常显是视觉噪音) scene.Each( [&](Entity e, TransformComponent& t, LightComponent& light) { if (!light.active) return; const u32 bits = static_cast(entt::to_integral(e)); const glm::vec3 pos = t.GetWorldPosition(); const glm::vec3 rgb(light.color); const glm::vec4 color = IsSel(bits) ? kGizmoSelectedColor : glm::vec4(rgb * (0.6f + 0.4f * light.color.a), 1.0f); if (light.type == LightComponent::Type::Point) { EmitIcon(pos, kShapePointLight, color, bits, 1.0f); } else { EmitIcon(pos, kShapeDirLight, color, bits, 1.1f); glm::vec3 dir = light.direction; if (glm::length(dir) > 1e-4f) { dir = glm::normalize(dir); const glm::vec4 lineColor = IsSel(bits) ? kGizmoSelectedColor : glm::vec4(rgb, 0.9f); EmitLine(pos, pos + dir * 0.6f, lineColor); } } if (IsSel(bits) && light.type == LightComponent::Type::Point) { const glm::vec4 ring = glm::vec4(rgb, 0.35f); const f32 R = light.radius; for (u32 s = 0; s < kRadiusCircleSegs; ++s) { const f32 a0 = kTwoPi * s / kRadiusCircleSegs; const f32 a1 = kTwoPi * (s + 1) / kRadiusCircleSegs; const glm::vec3 d0(std::cos(a0), std::sin(a0), 0.0f); const glm::vec3 d1(std::cos(a1), std::sin(a1), 0.0f); EmitLine(pos + d0 * R, pos + d1 * R, ring); // XY EmitLine(pos + glm::vec3(d0.x, 0, d0.y) * R, pos + glm::vec3(d1.x, 0, d1.y) * R, ring); // XZ EmitLine(pos + glm::vec3(0, d0.y, d0.x) * R, pos + glm::vec3(0, d1.y, d1.x) * R, ring); // YZ } } }); // 相机实体:图标 + 视锥线框(primary 提亮放大) scene.Each( [&](Entity e, TransformComponent& t, CameraComponent& cam) { if (!cam.active) return; const u32 bits = static_cast(entt::to_integral(e)); const bool sel = IsSel(bits); const glm::vec4 color = sel ? kGizmoSelectedColor : glm::vec4(cam.primary ? 1.0f : 0.55f, 0.8f, 1.0f, 1.0f); EmitIcon(t.GetWorldPosition(), kShapeCamera, color, bits, cam.primary ? 1.3f : 1.0f); EmitFrustum(t, cam, sel ? kGizmoSelectedColor : glm::vec4(0.55f, 0.8f, 1.0f, 0.85f), bits); }); // 环境组件:位置无关天体的空间锚点标记 scene.Each( [&](Entity e, TransformComponent& t, SkyboxComponent& sb) { if (!sb.active) return; const u32 bits = static_cast(entt::to_integral(e)); EmitIcon(t.GetWorldPosition(), kShapeSkybox, IsSel(bits) ? kGizmoSelectedColor : glm::vec4(0.45f, 0.55f, 0.9f, 1.0f), bits, 1.0f); }); scene.Each( [&](Entity e, TransformComponent& t, SkyLightComponent& sl) { if (!sl.active) return; const u32 bits = static_cast(entt::to_integral(e)); EmitIcon(t.GetWorldPosition(), kShapeSkyLight, IsSel(bits) ? kGizmoSelectedColor : glm::vec4(0.5f, 0.75f, 0.95f, 1.0f), bits, 1.0f); }); if (truncated && !m_GizmoTruncWarned) { m_GizmoTruncWarned = true; ENGINE_WARN("SceneRenderer: editor gizmo vertex budget " "exceeded, clamped"); } } #endif 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 状态,空场景可用); // 游戏形态(kDisplayDirect 全屏预览或 PIE 内嵌播放)用 // 场景 primary 相机实体(游戏逻辑所有)---- ECS::TransformComponent* camTransform = nullptr; const Camera* camera = nullptr; if (m_DisplayMode == kDisplayViewport && !m_GameView) { camTransform = &m_EditorCamera.GetTransform(); camera = &m_EditorCamera.GetController(); } else { if (auto* cameraComp = scene.GetPrimaryCamera(&camTransform)) { camera = &cameraComp->camera; } if (!camera || !camTransform) { // 游戏形态无 primary 相机:回退编辑器相机渲染(视口有 // 橙色提示引导补相机)。绝不能跳帧——跳过 submit 会漏 // 掉本槽位 fence 的 signal,下一轮该槽位 WaitFence 永 // 久阻塞 = 整个渲染死锁(空场景 Play 卡死的根因) camTransform = &m_EditorCamera.GetTransform(); camera = &m_EditorCamera.GetController(); } } // 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝) m_Frame.pickRequest = m_PendingPick; m_PendingPick.active = false; // 选中实体落帧(持续状态,不做一次性清除)。 // 有效性与位值分离:0 号实体(场景首个创建)位模式 = 0 m_Frame.selectedEntityValid = m_PendingSelected.has_value(); m_Frame.selectedEntityBits = m_PendingSelected.value_or(0); const f32 aspect = static_cast(targetExtent.width) / static_cast(targetExtent.height); ubo.view = camera->GetView(*camTransform); // 投影参数(模式/fov/near/far/orthoSize)由相机自身承载: // 编辑器形态 = 编辑器相机,游戏形态 = primary 相机实体 ubo.proj = camera->GetProjection(aspect); // 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity) ubo.projNoJitter = ubo.proj; // 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。 // NDC 偏移 = 像素偏移 × 2 / 尺寸。TAA 关闭时停抖(直通画面静止)。 // 抖动分量必须按投影模式选(w_clip 语义不同): // 透视 w_clip = -z_eye → 加 z 列([2][x]),被 w 除后 NDC 偏移 // 恒定(z_eye 约掉);正交 w_clip = 1 → z 列偏移会随视图深度 // 放大(far=10000 时抖动放大上万倍 = 切正交后画面狂震的根因), // 必须加平移列([3][x]),与 w=1 相乘才是恒定 NDC 偏移 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; const f32 sx = 2.0f / static_cast(targetExtent.width); const f32 sy = 2.0f / static_cast(targetExtent.height); if (camera->GetProjectionMode() == Camera::ProjectionMode::Orthographic) { ubo.proj[3][0] += jx * sx; ubo.proj[3][1] += jy * sy; } else { ubo.proj[2][0] += jx * sx; ubo.proj[2][1] += jy * sy; } } // ---- 环境组件落帧(UE SkyLight/Skybox Actor 语义):场景环境 // 是实体组件而非全局设置。多实例取第一个 active(entt // 视图序 = 组件创建序,首个命中稳定);无组件场景兜底 = // 无天空 + 旧 Settings 默认环境光(空场景清屏可渲染)---- m_Frame.DrawSkybox = false; m_Frame.envPath.clear(); ubo.ambientColor = glm::vec4{0.07f, 0.07f, 0.09f, 0.0f}; for (auto&& [e, sb] : scene.GetRegistry().view().each()) { if (sb.active) { m_Frame.DrawSkybox = true; m_Frame.envPath = sb.envPath; break; } } for (auto&& [e, sl] : scene.GetRegistry().view().each()) { if (sl.active) { ubo.ambientColor = sl.ambientColor; break; } } // ---- 运行时开关/调试视图落帧(各 pass 与 frag 只读契约字段)---- m_Frame.ClearColor = scene.GetSettings().ClearColor; 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; #ifdef HELLO_EDITOR // 骨骼可视化设置(ImGui 经 Renderer 门面 → 此处落帧) m_Frame.showBoneWires = m_ShowBoneWires; m_Frame.boneAllNodes = m_BoneAllNodes; m_Frame.boneJointPx = m_BoneJointPx; m_Frame.pieActive = m_GameView; // PIE 态骨骼可视化整体停产 #endif 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.GetWorldPosition(), 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, camera->GetNearZ(), camera->GetFarZ()); ubo.cameraPos = glm::vec4(camTransform->GetWorldPosition(), 1.0f); // ---- 2.5 骨骼动画(调度收口 Animation::AnimationSystem:推进 // 播放头/crossfade + 姿态求值 + palette 产出。与可见性无关: // 播放头恒走,否则被剔除的实体回屏时动画跳变)---- // 渲染侧零动画逻辑:收集段只查 frame.entitySkinBases(查不到 = // identity 槽 = 非蒙皮静态;蒙皮实体由 AnimationSystem 兜底 // rest-pose palette,无动画组件也成立) m_AnimSystem.Update(scene, assets, dt, m_Frame); #ifdef HELLO_EDITOR // ---- 2.7 Editor gizmo 收集(编辑形态专属;纯 CPU 只读场景)。 // billboard right/up 从视图矩阵行向量提取(相机无缩放)---- if (m_DisplayMode == kDisplayViewport && !m_GameView) { CollectEditorGizmos(scene, camTransform->GetWorldPosition(), glm::vec3(ubo.view[0][0], ubo.view[1][0], ubo.view[2][0]), glm::vec3(ubo.view[0][1], ubo.view[1][1], ubo.view[2][1]), aspect, static_cast(targetExtent.height), std::tan(glm::radians( m_EditorCamera.GetController().GetFovDeg()) * 0.5f)); } else { m_Frame.gizmoIconVerts.clear(); m_Frame.gizmoWireVerts.clear(); m_Frame.gizmoProxyVerts.clear(); // 骨骼可视化数组与 gizmo 同批清(AnimationSystem.Update 开头 // 也会清,此处双保险:PIE 态/全屏直渲绝无残留绘制) m_Frame.boneWireVerts.clear(); m_Frame.boneJoints.clear(); } #endif // ---- 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,不产生假运动)。m_PrevWorlds 是 sorted vector, // binary_search 比 unordered_map::find 更 cache 友好且无 rehash const u32 entKey = static_cast(entt::to_integral(entity)); glm::mat4 prevWorld = t.World(); auto prevIt = std::lower_bound( m_PrevWorlds.begin(), m_PrevWorlds.end(), entKey, [](const auto& p, u32 k) { return p.first < k; }); if (prevIt != m_PrevWorlds.end() && prevIt->first == entKey) { prevWorld = prevIt->second; } m_CurWorlds.emplace_back(entKey, t.World()); // palette 基址(AnimationSystem 预采样:动画实体=当帧 // 姿态、蒙皮无动画实体=rest 兜底;查不到=identity 槽, // 只可能是非蒙皮静态模型) u32 skinBase = kIdentityPaletteSlot; if (const auto it = m_Frame.entitySkinBases.find(entKey); it != m_Frame.entitySkinBases.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; const auto& meshes = model->GetMeshes(); for (size_t meshIdx = 0; meshIdx < meshes.size(); ++meshIdx) { auto& mesh = meshes[meshIdx]; 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; // ---- 实体级材质覆盖 resolve(UE Material Slot)---- // 覆盖路径命中已加载资产 → 用其 GPU 材质;未加载/ // 解析失败回退内建(一次性告警)。收集期只查缓存 // 不建 GPU 资源——加载点收敛在场景预载(游戏形态 // 唯一加载点)与编辑器指派(UI 只是编辑接口) Material* resolved = mesh->GetMaterial(); if (!mr.materialOverrides.empty()) { if (const auto oIt = mr.materialOverrides.find( static_cast(meshIdx)); oIt != mr.materialOverrides.end()) { if (MaterialAsset* asset = assets.FindMaterialAsset(oIt->second); asset != nullptr) { resolved = asset->GetMaterial(); } else if (m_WarnedMaterialMisses.insert( oIt->second).second) { ENGINE_WARN("GPUDriven: material asset " "'{}' not loaded, falling back " "to built-in", oIt->second); } } } // LOD 选级:屏占比近似 = radius/距离(主视图按实例距离; // 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0 const f32 dist = glm::length(worldCenter - camTransform->GetWorldPosition()); 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 槽);覆盖材质 = // hash 寻址(键含材质变体:同 mesh 不同覆盖天然 // 分批,同覆盖多实体仍合并——材质资产指针仅帧内 // 有效,变体表每帧随 ViewScratch::Clear 清空) const bool builtin = (resolved == mesh->GetMaterial()); std::array* lodSlots = nullptr; if (builtin) { const u32 slot = mesh->GetSlot(); if (slot >= s.lookup.size()) { s.lookup.resize(slot + 1); } lodSlots = &s.lookup[slot]; } else { const auto [vIt, inserted] = s.matVariants.emplace( resolved, static_cast(s.matVariants.size())); const u64 key = (static_cast(mesh->GetSlot()) << 32) | static_cast(vIt->second); lodSlots = &s.ovrLookup[key]; } // batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch) u32 batchIdx = (*lodSlots)[lod]; if (batchIdx == 0) { batchIdx = static_cast(s.result.batches.size()) + 1; (*lodSlots)[lod] = batchIdx; // first 是占位:真实基址在 appendView 聚合时 // 回填(收集序下 batch 实例散布,见链表注释) s.result.batches.push_back( {mesh.get(), resolved, 0, 0, lod}); s.batchDistSum.push_back(0.0f); s.batchHead.push_back(ViewScratch::kNoInstance); } // 实例归入自己的 batch(mesh 分组):收集序散布, // 组内连接用链表维护(O(1) push,无 per-batch 分配) const u32 bi = batchIdx - 1; s.result.batches[bi].count++; const u32 ii = static_cast(s.instances.size()); s.instances.push_back( {t.World(), prevWorld, skinBase, entKey}); s.instanceNext.push_back(s.batchHead[bi]); s.batchHead[bi] = ii; // 头插:链序 = push 逆序(无妨, // 实例对齐性由 (矩阵,id) 成对保证) s.batchDistSum[bi] += dist; // 透明排序用 s.result.instanceCount++; ++totalInstances; }; // 透明材质 → 独立队列(主视图,混合绘制 + 排序; // 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)。 // 队列归属按 resolve 后的实际材质:同一 mesh 可因 // 覆盖不同而分属不透明/透明两队列 const bool isTransparentMesh = resolved->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); // 链表遍历:收集序下同 batch 实例散布在扁平数组各处 //(多实体共享 mesh 交错插入),按组聚合拷成连续区间 //——firstInstance 连续语义的前提 for (u32 idx = s.batchHead[i]; idx != ViewScratch::kNoInstance; idx = s.instanceNext[idx]) { const auto& m = s.instances[idx]; 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. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排。 // 实例按链表逐 batch 搬运(收集序散布,不能按 (first,count) 切片); // 搬完的新数组天然按 batch 连续 → 链表确定性重建(顺序遍历) { 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; }); // 临时构建(容量保留由 swap 进 m_TranslucentScratch 复用) std::vector sortedBatches; std::vector sortedInstances; std::vector sortedNext; std::vector sortedHead; std::vector sortedDist; sortedBatches.reserve(tb.size()); sortedHead.reserve(tb.size()); sortedDist.reserve(tb.size()); // 同场景实例总数不变,capacity 可直接复用(reserve 不浪费) sortedInstances.reserve(m_TranslucentScratch.instances.size()); sortedNext.reserve(m_TranslucentScratch.instances.size()); for (const u32 i : order) { // 新 batch first = 已搬过去的新 instances 数量 InstanceBatch b = tb[i]; b.first = static_cast(sortedInstances.size()); sortedBatches.push_back(b); sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]); // 链表逐实例搬运;新布局按 batch 连续 → next = 顺序+1。 // 新实例下标 = b.first + n(n = 本 batch 已搬数量), // 前一个实例的 next 指向它 u32 prev = ViewScratch::kNoInstance; u32 n = 0; for (u32 idx = m_TranslucentScratch.batchHead[i]; idx != ViewScratch::kNoInstance; idx = m_TranslucentScratch.instanceNext[idx]) { sortedInstances.push_back(std::move( m_TranslucentScratch.instances[idx])); sortedNext.push_back(ViewScratch::kNoInstance); if (prev != ViewScratch::kNoInstance) { sortedNext[prev] = b.first + n; } prev = b.first + n; ++n; } sortedHead.push_back( n > 0 ? b.first : ViewScratch::kNoInstance); } m_TranslucentScratch.result.batches = std::move(sortedBatches); m_TranslucentScratch.batchHead = std::move(sortedHead); m_TranslucentScratch.instanceNext = std::move(sortedNext); m_TranslucentScratch.batchDistSum = std::move(sortedDist); m_TranslucentScratch.instances = std::move(sortedInstances); // instanceCount 不变(重排不丢实例) } } appendView(m_Frame.translucentView, m_TranslucentScratch); // ---- 5.5 选中描边:从扁平实例表筛出选中实体的单实例绘制 ---- //(不透明 + 透明队列都查——选中玻璃也要有描边)。 // 无选中时保持空表,SelectionMask pass 只 clear 不绘制 m_Frame.selectionDraws.clear(); if (m_Frame.selectedEntityValid) { 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(每帧重建,销毁的实体自动清出)。 // sort 后 m_CurWorlds 升序 → 下帧 binary_search 才能正确工作。 // stable_sort 防御性选择:实体销毁/新建导致 key 集合变化时序不变; // 实测 std::sort 对 u32 key 已足够稳定且更快 std::sort(m_CurWorlds.begin(), m_CurWorlds.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); 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.material != b.material) { return reinterpret_cast(a.material) < reinterpret_cast(b.material); } return a.mesh->GetSlot() < b.mesh->GetSlot(); }); for (const auto& b : view.batches) { const vk::Buffer vb = b.mesh->GetVertexBuffer(); Material* mat = byMaterial ? b.material : 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