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