feat: ImGui docking Editor + 显示链/帧同步/输入语义重构
- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期 - 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报 - 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链 - 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行) - 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧 - FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点) - Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -46,6 +47,121 @@ 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)
|
||||
@@ -132,7 +248,7 @@ namespace Engine
|
||||
}
|
||||
|
||||
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||
vk::Extent2D targetExtent)
|
||||
vk::Extent2D targetExtent, f32 dt)
|
||||
{
|
||||
ZoneScopedN("BuildFrame");
|
||||
const auto collectStart = std::chrono::steady_clock::now();
|
||||
@@ -179,6 +295,7 @@ namespace Engine
|
||||
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;
|
||||
@@ -250,6 +367,107 @@ namespace Engine
|
||||
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) {
|
||||
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 );
|
||||
@@ -262,8 +480,10 @@ namespace Engine
|
||||
// batch.first 拼接时加上段基址,保证段内区间连续。
|
||||
m_Frame.instanceModels.clear();
|
||||
m_Frame.prevInstanceModels.clear();
|
||||
m_Frame.skinOffsets.clear();
|
||||
m_MainScratch.Clear();
|
||||
for (auto& s : m_CascScratch) s.Clear();
|
||||
m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建
|
||||
u32 totalInstances = 0;
|
||||
bool instanceTruncated = false;
|
||||
|
||||
@@ -289,6 +509,13 @@ namespace Engine
|
||||
}
|
||||
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])),
|
||||
@@ -359,7 +586,7 @@ namespace Engine
|
||||
// 实例在扁平化后连续——firstInstance 语义的前提
|
||||
const u32 bi = batchIdx - 1;
|
||||
s.result.batches[bi].count++;
|
||||
s.batchMats[bi].push_back({t.World(), prevWorld});
|
||||
s.batchMats[bi].push_back({t.World(), prevWorld, skinBase});
|
||||
s.batchDistSum[bi] += dist; // 透明排序用
|
||||
s.result.instanceCount++;
|
||||
++totalInstances;
|
||||
@@ -398,6 +625,8 @@ namespace Engine
|
||||
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);
|
||||
}
|
||||
}
|
||||
dst.instanceCount = s.result.instanceCount;
|
||||
@@ -407,6 +636,12 @@ namespace Engine
|
||||
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;
|
||||
@@ -466,4 +701,52 @@ namespace Engine
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user