- .animctrl state machine asset (params/transitions/blend trees, 1D+2D directional blending) with Unity Animator-style editor panel: state list, transitions, params, directional pad preview driving live CrossFade on the selected entity - Split animation evaluation out of SceneRenderer into AnimationSystem/AnimationEvaluator; skin palettes with rest-pose fallback and blend-weight epsilon filtering - MaterialAsset (.material) with reload-safe preview tile pool (DropMaterial clears dirty refs before instance destruction) - EditorGizmoPass skeleton wires + viewport joint overlay; camera projection rework (baked Vulkan Y-flip, ortho support) - Editor/game build split via HELLOVULKAN_EDITOR option
1081 lines
53 KiB
C++
1081 lines
53 KiB
C++
//
|
||
// Created by atdunbg on 2026/9/23.
|
||
//
|
||
|
||
#include <Engine/Renderer/SceneRenderer.h>
|
||
#include <Engine/ECS/Scene.h>
|
||
#include <Engine/ECS/Components.h>
|
||
#include <Engine/Renderer/Camera.h>
|
||
#include <Engine/Renderer/EditorCamera.h>
|
||
#include <Engine/Renderer/Frustum.h>
|
||
#include <Engine/Renderer/Model.h>
|
||
#include <Engine/Renderer/Mesh.h>
|
||
#include <Engine/Renderer/MaterialAsset.h>
|
||
#include <Engine/Assets/AssetManager.h>
|
||
#include <Engine/Core/Log.h>
|
||
|
||
#include <glm/glm.hpp>
|
||
#include <glm/gtc/matrix_transform.hpp>
|
||
#include <glm/gtc/quaternion.hpp>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <numeric>
|
||
|
||
#include <tracy/Tracy.hpp>
|
||
|
||
#include <chrono>
|
||
#include <limits>
|
||
|
||
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<f32>(base);
|
||
r += f * static_cast<f32>(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<f32>(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<f32>::max());
|
||
glm::vec3 mx( std::numeric_limits<f32>::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<f32>(kCascadeRes);
|
||
const f32 texelY = (mx.y - mn.y) / static_cast<f32>(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<u32>(icons.size()), 6, kMaxGizmoIconVerts) ||
|
||
!Fit(static_cast<u32>(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<GizmoQuadVertex>& 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<u32>(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<f32>(s) / kRibbonSegsPerEdge;
|
||
const f32 t1 = static_cast<f32>(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<u32>(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<TransformComponent, LightComponent>(
|
||
[&](Entity e, TransformComponent& t, LightComponent& light)
|
||
{
|
||
if (!light.active) return;
|
||
const u32 bits = static_cast<u32>(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<TransformComponent, CameraComponent>(
|
||
[&](Entity e, TransformComponent& t, CameraComponent& cam)
|
||
{
|
||
if (!cam.active) return;
|
||
const u32 bits = static_cast<u32>(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<TransformComponent, SkyboxComponent>(
|
||
[&](Entity e, TransformComponent& t, SkyboxComponent& sb)
|
||
{
|
||
if (!sb.active) return;
|
||
const u32 bits = static_cast<u32>(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<TransformComponent, SkyLightComponent>(
|
||
[&](Entity e, TransformComponent& t, SkyLightComponent& sl)
|
||
{
|
||
if (!sl.active) return;
|
||
const u32 bits = static_cast<u32>(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<f32>(targetExtent.width) /
|
||
static_cast<f32>(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<u32>(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<f32>(targetExtent.width);
|
||
const f32 sy = 2.0f / static_cast<f32>(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<ECS::SkyboxComponent>().each())
|
||
{
|
||
if (sb.active)
|
||
{
|
||
m_Frame.DrawSkybox = true;
|
||
m_Frame.envPath = sb.envPath;
|
||
break;
|
||
}
|
||
}
|
||
for (auto&& [e, sl] :
|
||
scene.GetRegistry().view<ECS::SkyLightComponent>().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<TransformComponent, LightComponent>(
|
||
[&](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<f32>(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<TransformComponent, MeshRendererComponent>(
|
||
[&](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<u32>(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<u32>(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<u32, kMeshLodCount>* 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<u32>(s.matVariants.size()));
|
||
const u64 key =
|
||
(static_cast<u64>(mesh->GetSlot()) << 32) |
|
||
static_cast<u64>(vIt->second);
|
||
lodSlots = &s.ovrLookup[key];
|
||
}
|
||
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
|
||
u32 batchIdx = (*lodSlots)[lod];
|
||
if (batchIdx == 0) {
|
||
batchIdx = static_cast<u32>(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<u32>(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<u32>(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<u32> 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<InstanceBatch> sortedBatches;
|
||
std::vector<ViewScratch::InstanceMatrices> sortedInstances;
|
||
std::vector<u32> sortedNext;
|
||
std::vector<u32> sortedHead;
|
||
std::vector<f32> 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<u32>(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<f32, std::milli>(
|
||
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<u64>(static_cast<VkBuffer>(
|
||
a.mesh->GetVertexBuffer()));
|
||
const u64 vb = reinterpret_cast<u64>(static_cast<VkBuffer>(
|
||
b.mesh->GetVertexBuffer()));
|
||
if (va != vb) return va < vb;
|
||
if (byMaterial && a.material != b.material) {
|
||
return reinterpret_cast<u64>(a.material) <
|
||
reinterpret_cast<u64>(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<u32>(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
|