Files
HelloVulkan/Engine/src/Renderer/SceneRenderer.cpp
T
atdunbg cc837bc605 Add animation controller system, material assets and skeleton visualization
- .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
2026-10-07 01:50:04 +08:00

1081 lines
53 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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