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
This commit is contained in:
2026-10-07 01:50:04 +08:00
parent a941207ae3
commit cc837bc605
71 changed files with 8198 additions and 548 deletions
+450 -256
View File
@@ -10,6 +10,7 @@
#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>
@@ -18,6 +19,7 @@
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <numeric>
#include <tracy/Tracy.hpp>
@@ -31,17 +33,37 @@ namespace Engine
namespace
{
// 相机裁剪范围(UE 编辑器视口语义:near = 10cm;far 拉远到
// "无限"级——UE 的 far clip 默认就是不裁剪,深度精度由 near 主导,
// far 取多大都不损近处精度)。阴影距离独立(UE DirectionalLight
// 的 shadow distance 同构):级联分割只覆盖 kShadowDistance,
// 更远无阴影,避免级联跟着 far 拉爆近处阴影分辨率
constexpr f32 kCameraNear = 0.1f;
constexpr f32 kCameraFar = 10000.0f;
// 相机裁剪范围不再走常量:每实体相机自带 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)
{
@@ -53,134 +75,20 @@ namespace Engine
}
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)
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir,
const f32 camNear, const f32 camFar)
{
constexpr u32 N = kMaxCascades;
// ---- 1. 实用分割:均匀与对数分割按 λ 插值(视图深度,正值)----
//(分割覆盖 [near, kShadowDistance];kCameraFar 只管主投影裁剪)
//(分割覆盖 [camNear, kShadowDistance];camFar 只管主投影裁剪)
f32 splits[N];
for (u32 i = 1; i <= N; ++i) {
const f32 t = static_cast<f32>(i) / N;
const f32 uniform = kCameraNear + (kShadowDistance - kCameraNear) * t;
const f32 log = kCameraNear * glm::pow(kShadowDistance / kCameraNear, t);
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) {
@@ -203,14 +111,14 @@ namespace Engine
? 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 d0 = (c == 0) ? camNear : splits[c - 1];
const f32 d1 = splits[c];
// 视图深度 → 视锥棱边参数:棱边上 z_view 线性(near/far 角点
// 都在各自平面上),farC 反投影自主投影 far 面,故 t 相对
// kCameraFar 归一;splits 只覆盖 [near, kShadowDistance],
// camFar 归一;splits 只覆盖 [camNear, kShadowDistance],
// t 必然远小于 1 → 自然截出阴影范围内的切片
const f32 t0 = (d0 - kCameraNear) / (kCameraFar - kCameraNear);
const f32 t1 = (d1 - kCameraNear) / (kCameraFar - kCameraNear);
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) {
@@ -258,6 +166,297 @@ namespace Engine
}
}
#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)
{
@@ -303,19 +502,34 @@ namespace Engine
const f32 aspect = static_cast<f32>(targetExtent.width) /
static_cast<f32>(targetExtent.height);
ubo.view = camera->GetView(*camTransform);
ubo.proj = camera->GetProjection(aspect, kCameraNear, kCameraFar);
// 投影参数(模式/fov/near/far/orthoSize)由相机自身承载:
// 编辑器形态 = 编辑器相机,游戏形态 = primary 相机实体
ubo.proj = camera->GetProjection(aspect);
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
ubo.projNoJitter = ubo.proj;
// 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。
// NDC 偏移 = 像素偏移 × 2 / 尺寸。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;
ubo.proj[2][0] += jx * 2.0f / static_cast<f32>(targetExtent.width);
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height);
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 语义):场景环境
@@ -353,6 +567,13 @@ namespace Engine
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;
@@ -422,111 +643,37 @@ namespace Engine
collected[0].params.x < 0.5f) {
shadowDir = glm::vec3(collected[0].direction);
}
ComputeCascades(shadowDir);
ComputeCascades(shadowDir, camera->GetNearZ(), camera->GetFarZ());
ubo.cameraPos = glm::vec4(camTransform->GetWorldPosition(), 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; // 组件禁用 = 暂停采样(播放头不动)
if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL)
const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return;
const auto& clips = model->GetClips();
// ---- 2.5 骨骼动画(调度收口 Animation::AnimationSystem:推进
// 播放头/crossfade + 姿态求值 + palette 产出。与可见性无关:
// 播放头恒走,否则被剔除的实体回屏时动画跳变)----
// 渲染侧零动画逻辑:收集段只查 frame.entitySkinBases(查不到 =
// identity 槽 = 非蒙皮静态;蒙皮实体由 AnimationSystem 兜底
// rest-pose palette,无动画组件也成立)
m_AnimSystem.Update(scene, assets, dt, m_Frame);
// 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;
});
#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
@@ -576,10 +723,12 @@ namespace Engine
}
m_CurWorlds.emplace_back(entKey, t.World());
// palette 基址(第 2.5 节预采样;无动画实体 = identity 槽)
// palette 基址(AnimationSystem 预采样:动画实体=当帧
// 姿态、蒙皮无动画实体=rest 兜底;查不到=identity 槽,
// 只可能是非蒙皮静态模型)
u32 skinBase = kIdentityPaletteSlot;
if (const auto it = m_EntitySkinBase.find(entKey);
it != m_EntitySkinBase.end()) {
if (const auto it = m_Frame.entitySkinBases.find(entKey);
it != m_Frame.entitySkinBases.end()) {
skinBase = it->second;
}
@@ -606,12 +755,38 @@ namespace Engine
}
if (!anyVis) return;
for (auto& mesh : model->GetMeshes()) {
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 =
@@ -634,20 +809,38 @@ namespace Engine
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);
// 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];
}
auto& lodSlots = s.lookup[slot];
u32 batchIdx = lodSlots[lod];
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;
(*lodSlots)[lod] = batchIdx;
// first 是占位:真实基址在 appendView 聚合时
// 回填(收集序下 batch 实例散布,见链表注释)
s.result.batches.push_back({mesh.get(), 0, 0, lod});
s.result.batches.push_back(
{mesh.get(), resolved, 0, 0, lod});
s.batchDistSum.push_back(0.0f);
s.batchHead.push_back(ViewScratch::kNoInstance);
}
@@ -668,9 +861,10 @@ namespace Engine
};
// 透明材质 → 独立队列(主视图,混合绘制 + 排序;
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)
const bool isTransparentMesh =
mesh->GetMaterial()->IsTransparent();
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)。
// 队列归属按 resolve 后的实际材质:同一 mesh 可因
// 覆盖不同而分属不透明/透明两队列
const bool isTransparentMesh = resolved->IsTransparent();
ViewScratch& mainTarget = isTransparentMesh
? m_TranslucentScratch : m_MainScratch;
@@ -851,16 +1045,16 @@ namespace Engine
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());
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.mesh->GetMaterial() : nullptr;
Material* mat = byMaterial ? b.material : nullptr;
bool newGroup = view.indirectGroups.empty();
if (!newGroup) {