阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx
- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对), 指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis 行走基朝向与跨骨架去重、根运动通道 - 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与 字符冲量判定 - 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、 不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖 - 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增 ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target - 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板 - 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
This commit is contained in:
@@ -27,6 +27,13 @@ target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
|
||||
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
|
||||
target_link_libraries(ImGuizmo PUBLIC imgui)
|
||||
|
||||
# ufbx(FBX 全版本导入,vendored 单文件 C 库):assimp 的 FBX importer
|
||||
# 只认 FBX 2011+,Mixamo 及大量网上资产是 FBX 6.1 binary(二进制布局
|
||||
# 与 7.x 在对象 ID 层不兼容,assimp DOM 直接拒读)。ufbx 官方支持
|
||||
# FBX 5.x/6.x/7.x 全版本,引擎的 .fbx 解析统一走它(glTF/OBJ 仍 assimp)
|
||||
add_library(ufbx STATIC ${CMAKE_SOURCE_DIR}/Lib/ufbx/ufbx.c)
|
||||
target_include_directories(ufbx PUBLIC ${CMAKE_SOURCE_DIR}/Lib/ufbx)
|
||||
|
||||
# Tracy(CPU/GPU 帧分析)
|
||||
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
|
||||
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
|
||||
@@ -80,6 +87,7 @@ target_link_libraries(Engine PUBLIC
|
||||
spdlog::spdlog
|
||||
stb
|
||||
assimp::assimp
|
||||
ufbx
|
||||
meshoptimizer
|
||||
Jolt::Jolt
|
||||
yaml-cpp::yaml-cpp
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#define HELLOVULKAN_ANIMATIONATTACH_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -51,6 +52,16 @@ namespace Engine {
|
||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath);
|
||||
|
||||
// 纯算法核心(headless 探针可直测):轨道重映射进 dstNodes
|
||||
// 坐标系,产出的 clips 追加到 outExtra。幂等/命名冲突检查
|
||||
// 基于 existingClips。AttachClipsToModel 只是
|
||||
// "取 nodes/clips + 回填" 的薄封装
|
||||
AttachResult AttachClipsToNodes(
|
||||
const std::vector<ModelNode>& dstNodes,
|
||||
const std::vector<AnimationClip>& existingClips,
|
||||
ParsedModel&& src, const std::string& sourcePath,
|
||||
std::vector<AnimationClip>& outExtra);
|
||||
|
||||
} // namespace Animation
|
||||
|
||||
|
||||
|
||||
@@ -64,15 +64,19 @@ namespace Engine::Animation {
|
||||
// outGlobals 尺寸 = model.GetNodes().size(),父序 DFS 累乘。
|
||||
// (crossfade 双层 = EvaluateLayers 的 count==2 特例,同一套
|
||||
// 加权混合数学,消掉历史双份代码)
|
||||
// inPlace = 丢弃动画子树根节点的位移(Unity in-place 语义:
|
||||
// 根运动开关关闭,原地播放,旋转保留)
|
||||
void Evaluate(const Model& model, const AnimationClip& cur,
|
||||
f32 curTime, const CrossfadeState& xf,
|
||||
std::vector<glm::mat4>& outGlobals);
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace = false);
|
||||
|
||||
// N 层加权混合(BlendTree / 状态机过渡的通用入口):
|
||||
// 逐层采样局部 TRS → 权重归一化混合(pos/scl lerp、rot 渐进
|
||||
// slerp)→ global 累乘。全空/全零权重 = rest 姿态
|
||||
void EvaluateLayers(const Model& model, const PoseLayer* layers,
|
||||
u32 count, std::vector<glm::mat4>& outGlobals);
|
||||
u32 count, std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace = false);
|
||||
|
||||
// 绑定姿势:全节点 untouched = localTransform 原样累乘。
|
||||
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
//
|
||||
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
|
||||
//
|
||||
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
|
||||
// 不同源(英文/Mixamo/UE/Blender/MMD 日文)。Unity Humanoid 的解法
|
||||
// 是把任意命名骨架映射到标准人形槽位(Avatar bone mapping),动画
|
||||
// 经槽位转换后即可驱动任意人形角色。
|
||||
// 架构分层(对齐 Unity Mecanim / UE IK Retargeter 的做法):
|
||||
//
|
||||
// 这里实现其中枢:名字归一化 → 槽位识别(关键词表,en/Mixamo/MMD)
|
||||
// → 源/目标骨架间生成 node 下标映射对。同名直映是映射的特例。
|
||||
// ┌─ standard ─ 标准人形拓扑:HumanoidSlot 52 槽(主干 6 + 臂链
|
||||
// │ 4×2 + 指骨 15×2 + 腿链 3×2 + 趾 2),带侧别/镜像/
|
||||
// │ 指骨结构化访问。重定向的"标准空间"。
|
||||
// ├─ dialects ─ 命名方言数据表:各来源(Mixamo/Rigify/UE/MMD)的
|
||||
// │ "槽位 → 归一化骨名"候选模式,纯数据无特判。命名
|
||||
// │ 习惯差异全部收敛在这层(Unity 把 token 规则编成
|
||||
// │ 数据表、UE 用链名映射,同思想:识别=查表)。
|
||||
// └─ mapper ── 归一化(NormalizeLower → side 抽取 → 分隔符剥离)
|
||||
// + 查表识别(MatchHumanoidSlot)+ 跨骨架配对
|
||||
// (AutoMapHumanoid:同名直映优先,余下槽位一对一)。
|
||||
//
|
||||
// 动画重定向本身在 AnimationAttach:src 动画经映射对烘焙到 dst
|
||||
// 骨架(两遍式 T-pose 参考搬运),本模块只负责"名字 → 槽位 → 配对"。
|
||||
//
|
||||
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
|
||||
// 不同源。同名直映是映射的特例(同源拆分资产零损耗)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_HUMANOIDRIG_H
|
||||
@@ -16,13 +27,24 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
// 标准人形槽位(最小主干集,不含手指/表情——主干驱动即可行走)
|
||||
// ---- standard:标准人形槽位(Unity Avatar 52 骨拓扑)----
|
||||
//
|
||||
// 布局保证算术可推导(镜像/指骨反查都走位运算友好的分段):
|
||||
// [0..5] 主干:Hips→Spine→Chest→UpperChest→Neck→Head
|
||||
// [6..9] 左臂链:Shoulder→UpperArm→LowerArm→Hand
|
||||
// [10..24] 左指骨:5 指 × 3 段(Thumb/Index/Middle/Ring/Pinky,
|
||||
// 段 1=近节 → 3=末节,与 Mixamo HandThumb1-3、Rigify
|
||||
// .01-.03、UE Proximal/Distal 对齐)
|
||||
// [25..28] 左腿链:UpperLeg→LowerLeg→Foot→Toes
|
||||
// [29..51] 右侧镜像(+23 偏移,镜像布局与左完全同构)
|
||||
enum class HumanoidSlot : u32 {
|
||||
Hips = 0,
|
||||
Spine,
|
||||
@@ -34,38 +56,113 @@ namespace Engine::Animation {
|
||||
LeftUpperArm,
|
||||
LeftLowerArm,
|
||||
LeftHand,
|
||||
LeftThumb1, // 指骨区起点(FingerSlot 算术依赖连续布局)
|
||||
LeftThumb2,
|
||||
LeftThumb3,
|
||||
LeftIndex1,
|
||||
LeftIndex2,
|
||||
LeftIndex3,
|
||||
LeftMiddle1,
|
||||
LeftMiddle2,
|
||||
LeftMiddle3,
|
||||
LeftRing1,
|
||||
LeftRing2,
|
||||
LeftRing3,
|
||||
LeftPinky1,
|
||||
LeftPinky2,
|
||||
LeftPinky3,
|
||||
LeftUpperLeg,
|
||||
LeftLowerLeg,
|
||||
LeftFoot,
|
||||
LeftToes,
|
||||
RightShoulder,
|
||||
RightUpperArm,
|
||||
RightLowerArm,
|
||||
RightHand,
|
||||
LeftUpperLeg,
|
||||
LeftLowerLeg,
|
||||
LeftFoot,
|
||||
RightThumb1,
|
||||
RightThumb2,
|
||||
RightThumb3,
|
||||
RightIndex1,
|
||||
RightIndex2,
|
||||
RightIndex3,
|
||||
RightMiddle1,
|
||||
RightMiddle2,
|
||||
RightMiddle3,
|
||||
RightRing1,
|
||||
RightRing2,
|
||||
RightRing3,
|
||||
RightPinky1,
|
||||
RightPinky2,
|
||||
RightPinky3,
|
||||
RightUpperLeg,
|
||||
RightLowerLeg,
|
||||
RightFoot,
|
||||
RightToes,
|
||||
Count
|
||||
};
|
||||
|
||||
// 槽位侧别(左右对称骨骼的识别结果)
|
||||
enum class Side : u8 { None, Left, Right };
|
||||
|
||||
// 槽位显示名("Hips"/"LeftUpperArm"...;调试映射表用)
|
||||
// 指别(0=拇指 … 4=小指;方言表里 Mixamo pinky / UE little 同槽)
|
||||
enum class FingerKind : u32 { Thumb = 0, Index, Middle, Ring, Pinky };
|
||||
|
||||
// ---- standard:槽位结构化访问 ----
|
||||
|
||||
// 槽位侧别(主干槽返回 None;镜像侧与枚举名一致)
|
||||
Side SideOfSlot(HumanoidSlot slot);
|
||||
|
||||
// 左右镜像(主干槽原样返回)
|
||||
HumanoidSlot MirrorSlot(HumanoidSlot slot);
|
||||
|
||||
// 指骨槽位合成:(side, 指, 段 1..3) → 槽位
|
||||
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg);
|
||||
|
||||
// 指骨槽位反解(非指骨槽位返回 false)
|
||||
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
|
||||
u32& seg);
|
||||
|
||||
// 槽位显示名(Unity 语义:"Hips"/"LeftUpperArm"/"LeftHandThumb2"...;
|
||||
// 调试映射表/Inspector 明细行用)
|
||||
const char* HumanoidSlotName(HumanoidSlot slot);
|
||||
|
||||
// 名字归一化 + 槽位识别。
|
||||
// ---- mapper:名字归一化 + 槽位识别 ----
|
||||
|
||||
// 名字归一化 + 查方言表识别槽位。
|
||||
// "mixamorig:LeftForeArm" → {LowerArm, Left};"左ひじ" → {LowerArm, Left};
|
||||
// "Upper Arm_L" → {UpperArm, Left}。识别失败 ok=false。
|
||||
// "Upper Arm_L" → {UpperArm, Left};"DEF-thumb.01.L" → {Thumb1, Left};
|
||||
// "Thumb Proximal_R" → {Thumb1, Right}。识别失败 ok=false。
|
||||
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||
bool& ok);
|
||||
|
||||
// 跨骨架自动映射:返回 (srcNodeIdx, dstNodeIdx) 对列表。
|
||||
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位匹配;
|
||||
// 一槽多候选时取语义关键词优先级表中的顺序(先高后低)。
|
||||
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位一对一配对;
|
||||
// 同槽多候选时取方言表内的优先序(先高后低)。
|
||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes);
|
||||
|
||||
// 估计源/目标骨架的根对齐旋转(rest 姿态 basis 对齐,src 世界 →
|
||||
// dst 世界)。跨骨架 retarget 时两套骨架常不同朝向(kwy DummyDoll
|
||||
// Z-up vs X Bot Y-up),增量变换须经该旋转共轭才能跨空间传递。
|
||||
// up = 头 - 双脚中点;right = 右臂 - 左臂(去 up 分量);
|
||||
// fwd = up × right,脚趾朝向消歧。槽位不全(缺头/双脚/双臂)
|
||||
// 或 rest 退化时返回 identity。
|
||||
glm::quat EstimateHumanoidRootAlignment(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes);
|
||||
|
||||
// 估计骨架的 T-pose 参考姿势(每节点**世界**旋转)。
|
||||
// 人形动画资产的摆动量以 T-pose 为零位;rest 是 A-pose(臂下垂)
|
||||
// 时,旋转增量直接搬运会过摆(臂越过中线)。此处把臂链
|
||||
// (upper_arm 子树)绕肩刚体摆到水平侧向作为参考,其余骨取 rest
|
||||
// (手指/趾走 rest 零位局部增量通道,见 AnimationAttach——指根
|
||||
// 扇形编码在 rest 平移里,不参与世界系零位搬运);rest 本为
|
||||
// T-pose 的资产摆动量≈0 自动退化。行走基不可用时全 rest。
|
||||
std::vector<glm::quat> EstimateHumanoidTPose(
|
||||
const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& restGlobals);
|
||||
|
||||
} // namespace Engine::Animation
|
||||
|
||||
#endif // HELLOVULKAN_HUMANOIDRIG_H
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// LogRing.h:进程级日志环形缓冲(编辑器 Log Panel 的数据源)。
|
||||
//
|
||||
// RingSink 注册进默认 logger 的 sinks 后,所有日志(引擎 + 脚本经
|
||||
// ScriptLog/ENGINE_* 打印的)自动落缓冲;面板按 version 增量感知、
|
||||
// Snapshot 整体拉取。头文件设施(sink_it_ 内联),无 cpp。
|
||||
//
|
||||
#ifndef HELLOVULKAN_LOGRING_H
|
||||
#define HELLOVULKAN_LOGRING_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <spdlog/details/log_msg.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Debug {
|
||||
|
||||
struct LogEntry {
|
||||
spdlog::level::level_enum level;
|
||||
std::string msg; // 整行(已按 ring pattern 格式化,无色码)
|
||||
};
|
||||
|
||||
class LogRingBuffer {
|
||||
public:
|
||||
static constexpr size_t kCap = 2048;
|
||||
|
||||
// 门控:启动期系统初始化日志(Vulkan/UBO/IBL...)不进面板,
|
||||
// 与 Unity Console 语义一致(只收运行期游戏侧日志)。
|
||||
// Application::Run 进入主循环时开闸
|
||||
bool IsEnabled() const {
|
||||
return m_Enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
void SetEnabled(bool v) {
|
||||
m_Enabled.store(v, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Push(spdlog::level::level_enum lvl, std::string msg) {
|
||||
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||
if (m_Entries.size() < kCap) {
|
||||
m_Entries.push_back({lvl, std::move(msg)});
|
||||
} else {
|
||||
m_Entries[m_Head] = {lvl, std::move(msg)};
|
||||
m_Head = (m_Head + 1) % kCap; // 满后覆写最旧
|
||||
}
|
||||
++m_Version;
|
||||
}
|
||||
|
||||
// 快照(满环时按时间序重排)
|
||||
std::vector<LogEntry> Snapshot() const {
|
||||
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||
if (m_Entries.size() < kCap) {
|
||||
return m_Entries;
|
||||
}
|
||||
std::vector<LogEntry> out;
|
||||
out.reserve(kCap);
|
||||
out.insert(out.end(), m_Entries.begin() + static_cast<long>(m_Head),
|
||||
m_Entries.end());
|
||||
out.insert(out.end(), m_Entries.begin(),
|
||||
m_Entries.begin() + static_cast<long>(m_Head));
|
||||
return out;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::lock_guard<std::mutex> lk(m_Mutex);
|
||||
m_Entries.clear();
|
||||
m_Head = 0;
|
||||
++m_Version;
|
||||
}
|
||||
|
||||
u64 Version() const {
|
||||
return m_Version.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_Mutex;
|
||||
std::vector<LogEntry> m_Entries;
|
||||
size_t m_Head = 0;
|
||||
std::atomic<u64> m_Version{0};
|
||||
std::atomic<bool> m_Enabled{false};
|
||||
};
|
||||
|
||||
inline LogRingBuffer& LogRing() {
|
||||
static LogRingBuffer inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// spdlog sink 适配:log() 整行入环。pattern 由装配端设置(无色码)
|
||||
class RingSink : public spdlog::sinks::base_sink<std::mutex> {
|
||||
protected:
|
||||
void sink_it_(const spdlog::details::log_msg& msg) override {
|
||||
if (!LogRing().IsEnabled()) {
|
||||
return; // 启动期:只落终端/文件,不进面板
|
||||
}
|
||||
spdlog::memory_buf_t formatted;
|
||||
base_sink<std::mutex>::formatter_->format(msg, formatted);
|
||||
std::string line(formatted.data(), formatted.size());
|
||||
while (!line.empty() &&
|
||||
(line.back() == '\n' || line.back() == '\r')) {
|
||||
line.pop_back();
|
||||
}
|
||||
LogRing().Push(msg.level, std::move(line));
|
||||
}
|
||||
void flush_() override {}
|
||||
};
|
||||
|
||||
} // namespace Engine::Debug
|
||||
|
||||
#endif //HELLOVULKAN_LOGRING_H
|
||||
@@ -37,29 +37,27 @@ namespace Engine
|
||||
void AddAllocation(u64 bytes);
|
||||
void AddDeallocation(u64 bytes);
|
||||
|
||||
// ---- 采样进程真实内存 ----
|
||||
void SampleProcessMemory();
|
||||
|
||||
// ---- 打印 ----
|
||||
void Print() const;
|
||||
void PrintDetailed() const;
|
||||
void PrintComparison() const;
|
||||
// ---- 只读快照(面板/遥测消费)----
|
||||
struct Snapshot
|
||||
{
|
||||
u64 textureCount = 0;
|
||||
u64 textureBytes = 0;
|
||||
u64 meshCount = 0;
|
||||
u64 meshBytes = 0;
|
||||
u64 bufferCount = 0;
|
||||
u64 bufferBytes = 0;
|
||||
u64 memoryBlockBytes = 0; // MemoryAllocator 已分配块
|
||||
u64 memoryUsedBytes = 0; // 块内实际占用
|
||||
u64 frameAllocation = 0; // 本帧新增(BeginFrame 清零)
|
||||
u64 frameDeallocation = 0; // 本帧释放
|
||||
u64 totalAllocation = 0;
|
||||
u64 totalDeallocation = 0;
|
||||
};
|
||||
Snapshot TakeSnapshot() const;
|
||||
|
||||
private:
|
||||
Stats() = default;
|
||||
|
||||
struct ProcessMemory
|
||||
{
|
||||
u64 VmSize = 0; // /proc/self/status VmSize
|
||||
u64 Rss = 0; // /proc/self/status VmRSS
|
||||
u64 Pss = 0; // smaps_rollup
|
||||
u64 SharedClean = 0;
|
||||
u64 PrivateDirty = 0;
|
||||
u64 PrivateClean = 0;
|
||||
};
|
||||
|
||||
ProcessMemory ReadProcessMemory() const;
|
||||
|
||||
mutable std::mutex m_Mutex;
|
||||
|
||||
// 资源计数
|
||||
@@ -81,9 +79,6 @@ namespace Engine
|
||||
u64 m_FrameDeallocation = 0;
|
||||
u64 m_TotalAllocation = 0;
|
||||
u64 m_TotalDeallocation = 0;
|
||||
|
||||
// 进程快照
|
||||
ProcessMemory m_ProcessMem{};
|
||||
};
|
||||
|
||||
#define STATS Engine::Stats::Get()
|
||||
|
||||
@@ -480,6 +480,9 @@ namespace Engine::ECS {
|
||||
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
|
||||
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
|
||||
f32 gravityScale = 2.0f;
|
||||
// 形状附加偏移:叠加在脚底约定(胶囊底端=原点)之上,模型
|
||||
// pivot 不在脚底时对齐视觉网格用(Y+ 胶囊上抬)
|
||||
glm::vec3 shapeOffset{0.0f};
|
||||
|
||||
bool operator==(const CharacterControllerComponent&) const = default;
|
||||
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
|
||||
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
|
||||
//
|
||||
// 角色间碰撞(Jolt 设计行为):CharacterVirtual 彼此默认穿透,需经
|
||||
// CharacterVsCharacterCollision 显式互查。PhysicsWorld 持有一份
|
||||
// CharacterVsCharacterCollisionSimple 并注入每个角色(SetCharacterCollision),
|
||||
// 使角色扫掠时也撞其它角色——进/出列表由本类构造/析构自动维护。
|
||||
//
|
||||
// 速度语义(游戏逻辑职责分工):
|
||||
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
|
||||
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
|
||||
@@ -36,13 +41,21 @@ namespace Engine {
|
||||
f32 mass = 70.0f;
|
||||
f32 maxSlopeAngle = 45.0f; // 度
|
||||
f32 stepHeight = 0.4f; // 台阶攀爬高
|
||||
// 形状附加偏移(相对"胶囊底端钉实体原点"的脚底约定):
|
||||
// 模型 pivot 不在脚底时对齐用(Y+ 上抬)
|
||||
glm::vec3 shapeOffset{0.0f};
|
||||
|
||||
bool operator==(const Params&) const = default;
|
||||
};
|
||||
|
||||
CharacterController(JPH::PhysicsSystem& system, const Params& params,
|
||||
const glm::vec3& position);
|
||||
~CharacterController() = default; // Ref 引用计数自动释放
|
||||
// 析构:先从角色间碰撞列表摘除(防悬垂指针),Ref 随即释放
|
||||
~CharacterController();
|
||||
|
||||
// 注入角色间碰撞列表(PhysicsWorld 单份共享):登记本角色并打开
|
||||
// 互查。构造后由 PhysicsWorld 创建时调用一次;列表须比本对象活得久
|
||||
void SetCharacterCollision(JPH::CharacterVsCharacterCollisionSimple& list);
|
||||
|
||||
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
|
||||
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
|
||||
@@ -59,6 +72,16 @@ namespace Engine {
|
||||
|
||||
const Params& GetParams() const { return m_Params; }
|
||||
|
||||
// ---- 碰撞事件支持(PhysicsWorld 合成 Enter/Exit 用)----
|
||||
// 本步活跃接触列表(CharacterVirtual 直通;含对方体 ID/命中点/
|
||||
// 法线/距离)。CharacterVirtual 非刚体,Jolt 的 ContactListener
|
||||
// 不会为它回调——由 PhysicsWorld 读本列表对比出事件边沿
|
||||
const JPH::CharacterVirtual::ContactList& GetActiveContacts() const;
|
||||
// 上一次 Update 扫掠"之前"的速度(撞击强度判定)。Jolt 在扫掠
|
||||
// 中会抵消朝向墙面/陡面的速度分量(CancelVelocityTowardsSteepSlopes),
|
||||
// 扫掠后再读 GetLinearVelocity 往往已归零——撞墙判定必须用这里
|
||||
glm::vec3 GetSweepVelocity() const;
|
||||
|
||||
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
|
||||
// (脚本在物理位姿/脚底原点处画胶囊线框)
|
||||
void DebugDraw(JPH::DebugRenderer& renderer) const;
|
||||
@@ -67,6 +90,8 @@ namespace Engine {
|
||||
Params m_Params;
|
||||
JPH::Ref<JPH::CharacterVirtual> m_Character;
|
||||
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
|
||||
JPH::CharacterVsCharacterCollisionSimple* m_VsChar = nullptr; // 非拥有
|
||||
JPH::Vec3 m_SweepVelocity{0.0f, 0.0f, 0.0f}; // 扫掠前速度快照
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -102,6 +102,9 @@ namespace Engine {
|
||||
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
|
||||
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
|
||||
glm::vec3 GetCharacterPosition(u32 entityBits) const;
|
||||
// 角色"撞击强度"速度(上一次扫掠前的速度):撞墙/碎墙等判定用它,
|
||||
// 而非 GetCharacterVelocity——后者已被 Jolt 抵消掉朝向墙面的分量
|
||||
glm::vec3 GetCharacterImpactVelocity(u32 entityBits) const;
|
||||
|
||||
private:
|
||||
// 创建时缓存的参数(变更检测 → 重建 body)
|
||||
@@ -138,7 +141,8 @@ namespace Engine {
|
||||
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
|
||||
void SyncCharacters();
|
||||
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
|
||||
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)
|
||||
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)+ 合成角色
|
||||
// 碰撞事件(CharacterVirtual 不进 ContactListener,见函数注释)
|
||||
void UpdateCharacters(f32 dt);
|
||||
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
||||
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
||||
@@ -165,9 +169,16 @@ namespace Engine {
|
||||
std::unordered_map<u32, ConstraintRecord> m_Constraints;
|
||||
|
||||
// ---- 角色(entityBits → CharacterVirtual 封装)----
|
||||
// 角色间碰撞互查表:Jolt CharacterVirtual 默认彼此穿透,须显式注入
|
||||
// CharacterVsCharacterCollisionSimple 才互撞。声明在 m_Characters
|
||||
// 之前(析构更晚)——角色析构时会回表 Remove,列表须后于角色释放
|
||||
Scope<JPH::CharacterVsCharacterCollisionSimple> m_VsCharacterCollision;
|
||||
struct CharacterEntry {
|
||||
std::unique_ptr<CharacterController> cc;
|
||||
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
|
||||
// 上一步活跃接触的"对方实体"集合:本步对比出角色碰撞
|
||||
// Enter/Exit(CharacterVirtual 非刚体,ContactListener 不收)
|
||||
std::unordered_set<u32> contacts;
|
||||
};
|
||||
std::unordered_map<u32, CharacterEntry> m_Characters;
|
||||
|
||||
|
||||
@@ -47,7 +47,13 @@ namespace Engine {
|
||||
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
|
||||
struct MaterialParams {
|
||||
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
|
||||
// 默认 = 电介质表面推荐值(Unity/UE 同款):
|
||||
// metallic = 0 皮肤/头发/布料等非金属的正确值
|
||||
// roughness = 1.0 因子语义是"乘贴图",1.0 = 贴图值直用(同 glTF
|
||||
// roughnessFactor 缺省);配合兜底 MR 纹理 G=128
|
||||
// 得 effective roughness ≈ 0.5,不再是 ×0.5 后的
|
||||
// 0.25 假高光(ufbx 路径不写 factors,全靠这里兜底)
|
||||
glm::vec4 factors{0.0f, 1.0f, 0.0f, 0.0f};
|
||||
// glTF baseColorFactor:rgb 乘进 albedo,a 作 BLEND 透明度
|
||||
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
// glTF 表面扩展(KHR_materials_ior / transmission / volume /
|
||||
@@ -55,7 +61,14 @@ namespace Engine {
|
||||
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
|
||||
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
|
||||
glm::vec4 surface{1.5f, 0.0f, 1.0f, 0.0f};
|
||||
// 杂项语义:x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
|
||||
// 杂项语义:
|
||||
// x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
|
||||
// y = UV 左右镜像(u → 1-u,1 = 开启)
|
||||
// z = UV 上下镜像(v → 1-v,1 = 开启)
|
||||
// w = 保留
|
||||
// 镜像作用于材质全部纹理槽,叠加在每槽 UV 变换(uvA/uvT)之后:
|
||||
// 用于纠正源资产 UV 朝向(部分角色 rip 的 UV 与 glTF 上左原点
|
||||
// 约定相反,贴图整体翻转)。材质级语义,非单槽
|
||||
glm::vec4 misc{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
// glTF emissiveFactor:自发光颜色(rgb;黑 = 不发光)。emissive
|
||||
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
|
||||
@@ -63,8 +76,10 @@ namespace Engine {
|
||||
glm::vec4 emissiveFactor{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
|
||||
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
|
||||
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在本引擎
|
||||
// v-up 顶点 UV 空间,换算推导见 Model.cpp glTF 分支注释)。
|
||||
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在 glTF
|
||||
// 规范 UV 空间(v=0 = 图像顶);顶点 UV 经 aiProcess_FlipUVs 已
|
||||
// 归一至该空间,A/t 即规范式 off + R(rot)·S(scale)·uv 的摊平,
|
||||
// 换算推导见 Model.cpp glTF 分支注释)。
|
||||
// uvA[slot] = (a11, a12, a21, a22)(行主 2×2),uvT[slot] = (tx, ty, 0, 0)。
|
||||
// 长度必须 = Material::kSlotCount;std140 vec4 数组步长 16B 与
|
||||
// CPU 布局天然对齐
|
||||
@@ -72,6 +87,11 @@ namespace Engine {
|
||||
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
|
||||
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}};
|
||||
glm::vec4 uvT[6] = {};
|
||||
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
|
||||
// rgb = 叠加色(与 baseColor 同款直乘/直用,不做空间换算),
|
||||
// a = 强度(0 = 关闭,mix 恒等无副作用)。alpha 遮罩来自贴图本身,
|
||||
// 无 alpha 或 alpha 恒 1 的贴图开启后即等效整体染色
|
||||
glm::vec4 alphaOverlay{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
class Material {
|
||||
|
||||
@@ -47,8 +47,9 @@ namespace Engine {
|
||||
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
|
||||
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
|
||||
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
|
||||
// glb 内嵌纹理无独立文件路径 → 对 glb 模型 Create Override 时
|
||||
// 纹理槽为空(回退默认纹理),参数/透明照常可编辑。
|
||||
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
|
||||
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
|
||||
// 材质可正常继承
|
||||
struct MaterialAssetData {
|
||||
std::array<std::string, Material::kSlotCount> texPaths;
|
||||
MaterialParams params;
|
||||
|
||||
@@ -137,6 +137,12 @@ namespace Engine
|
||||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||
static ParsedModel ParseFromDisk(const std::string& path);
|
||||
|
||||
// 资产显示名(编辑器 UI / clip 命名的唯一来源):取文件 stem;
|
||||
// stem 无信息量(空 / 导入资产库的 "asset")时读同目录 pathname
|
||||
// 文件取原始资产 basename,再回退父目录名。避免 UI 直接显示
|
||||
// 哈希路径(用户看到的是 id)
|
||||
static std::string AssetDisplayName(const std::string& path);
|
||||
|
||||
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||
// 解析失败返回 nullptr。
|
||||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
|
||||
@@ -31,6 +31,10 @@ namespace Engine {
|
||||
|
||||
class Device;
|
||||
|
||||
// 显存估算(按格式字节宽与 mip 链逐级累加;BC 压缩按 4x4 块计)——
|
||||
// 分配器台账与 Texture 资源台账共用同一算法,两边永不漂移
|
||||
u64 EstimateImageBytes(vk::Format fmt, u32 w, u32 h, u32 depth, u32 mips);
|
||||
|
||||
class MemoryAllocator {
|
||||
public:
|
||||
explicit MemoryAllocator(Device& device);
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
//
|
||||
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
|
||||
//
|
||||
// 轨道映射两级:
|
||||
// ① 同名直映——同源拆分资产(角色/动画同一骨架导出)零损耗;
|
||||
// ② Humanoid 槽位重定向——名字不同源的组装资产(角色 FBX +
|
||||
// 动画 FBX 各自制作),经 Unity Avatar 语义的标准人形槽位配对,
|
||||
// 逐 key 烘焙"rest→anim 的增量变换"到目标骨架局部空间:
|
||||
// 平移 out.pos = dstRestPos + (srcPos - srcRestPos)
|
||||
// 旋转 Δ = srcRot * inv(srcRestRot);out.rot = Δ * dstRestRot
|
||||
// (Δ 父空间左乘 = 骨骼自身摆动在两套骨架间等价传递)
|
||||
// 烘焙后 clip 即目标骨架空间数据,评估器零改动。
|
||||
// 五段管线(映射表一次构建后**冻结**,逐 clip 只读消费,杜绝循环内
|
||||
// 惰性登记/反向表重建):
|
||||
//
|
||||
// AssetManager 是 facade:取模型 + 解析磁盘,纯算法在这里。
|
||||
// ① 资产导入(Model.cpp) clip 命名与重定向解耦(文件 stem 真名)
|
||||
// ② 映射表(HumanoidRig) 同名直映 / 52 槽配对 → 冻结 Plan
|
||||
// ③ 根运动通道 骨架上方"承载节点"(未识别的映射骨祖先)
|
||||
// 取最深者 → dst 根,世界增量 Δ·RdstRest
|
||||
// ④ 关节烘焙 两遍式 T-pose 参考搬运(见下数学);手指/
|
||||
// 趾与主干同通道(Unity/UE 语义:指骨即普通
|
||||
// 骨骼,无独立局部增量通道——局部系约定免疫)
|
||||
//
|
||||
// 关键不变量(本次重写的核心,此前版本违反即整体崩坏):
|
||||
// * Plan 构建后不可变;不存在 clip 循环内的 emplace 登记
|
||||
// * 每个 dst 节点至多一条轨道(owner 去重)——杜绝同节点重复轨道
|
||||
// * "承载节点"= 未识别的骨骼祖先(骨架上方),恒不参与同名直搬;
|
||||
// 判定基于**槽位识别**而非"所有映射骨祖先"(后者会把 hips/spine
|
||||
// 等内部骨全部误判成承载节点 → 大半骨架走烘焙而叶走直搬 → 局部
|
||||
// 化基准错位、全身崩坏,即用户反馈"全部是崩溃的"的根因)
|
||||
// * 仅"跨骨架对"(两侧骨名不同)走烘焙;同名对零损耗原样直搬
|
||||
// * 未动画的 src 骨不发旋转键(目标保持 rest,不被 T-pose 零位拉走)
|
||||
//
|
||||
// 数学(旋转,两遍式):
|
||||
// 零位 = src 侧 T-pose 参考 RsT(EstimateHumanoidTPose,两侧对称)
|
||||
// 世界增量 dW = A·(Rsrc(t)·RsT⁻¹)·A⁻¹
|
||||
// 目标世界 RwI = dW·RdT(dst)
|
||||
// 键 = inv(Rw[父])·RwI(父用 anim 后世界旋转)
|
||||
// rest 即 T-pose 的资产自动退化(dW 恒等 → 与旧直搬等价);A 为
|
||||
// src 世界 → dst 世界的行走基对齐(EstimateHumanoidRootAlignment)
|
||||
//
|
||||
// 数学(平移):
|
||||
// Root : dstRest + A·(srcPos - srcRest)·unitScale(整体尺度)
|
||||
// Joint : dstRest + M·(srcPos - srcRest)·ratio(per-骨长比)
|
||||
// M = inv(dstParW)·A·srcParW(局部增量系转换),退化取 unitScale
|
||||
//
|
||||
// 纯算法在 AttachClipsToNodes(headless 可直测);
|
||||
// AttachClipsToModel 是 facade:取模型 nodes/clips + 回填。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationAttach.h>
|
||||
@@ -28,11 +53,15 @@
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 kNoNode = ~0u;
|
||||
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
// mat4 → TRS 分解(rest 局部变换;引擎 node 无 shear)
|
||||
struct TRS {
|
||||
glm::vec3 pos{};
|
||||
@@ -47,8 +76,7 @@ namespace Engine::Animation {
|
||||
const glm::vec3 colX(m[0]);
|
||||
const glm::vec3 colY(m[1]);
|
||||
const glm::vec3 colZ(m[2]);
|
||||
t.scl = {glm::length(colX), glm::length(colY),
|
||||
glm::length(colZ)};
|
||||
t.scl = {glm::length(colX), glm::length(colY), glm::length(colZ)};
|
||||
if (t.scl.x > 1e-8f && t.scl.y > 1e-8f && t.scl.z > 1e-8f) {
|
||||
t.rot = glm::quat_cast(glm::mat3(
|
||||
colX / t.scl.x, colY / t.scl.y, colZ / t.scl.z));
|
||||
@@ -56,14 +84,336 @@ namespace Engine::Animation {
|
||||
return t;
|
||||
}
|
||||
|
||||
// 世界矩阵 → 旋转(列归一化抗缩放;退化列取 identity)
|
||||
glm::quat RotOf(const glm::mat4& m)
|
||||
{
|
||||
const glm::vec3 cx(m[0]);
|
||||
const glm::vec3 cy(m[1]);
|
||||
const glm::vec3 cz(m[2]);
|
||||
if (glm::length(cx) < 1e-8f || glm::length(cy) < 1e-8f ||
|
||||
glm::length(cz) < 1e-8f) {
|
||||
return kIdentity;
|
||||
}
|
||||
return glm::quat_cast(glm::mat3(
|
||||
glm::normalize(cx), glm::normalize(cy), glm::normalize(cz)));
|
||||
}
|
||||
|
||||
// rest 全骨架世界矩阵(node 表父先子后,直接链乘)
|
||||
std::vector<glm::mat4> RestGlobals(const std::vector<ModelNode>& nodes)
|
||||
{
|
||||
std::vector<glm::mat4> g(nodes.size());
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
const i32 p = nodes[i].parent;
|
||||
g[i] = (p >= 0 ? g[static_cast<size_t>(p)]
|
||||
: glm::mat4(1.0f)) *
|
||||
nodes[i].localTransform;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
// ---- 键采样(烘焙用;线性/slerp,rest 兜底)----
|
||||
glm::vec3 SampleV(const std::vector<PosKey>& ks, float t,
|
||||
const glm::vec3& def)
|
||||
{
|
||||
if (ks.empty()) return def;
|
||||
if (t <= ks.front().time) return ks.front().value;
|
||||
for (size_t i = 1; i < ks.size(); ++i) {
|
||||
if (t <= ks[i].time) {
|
||||
const f32 a = (t - ks[i - 1].time) /
|
||||
(ks[i].time - ks[i - 1].time);
|
||||
return glm::mix(ks[i - 1].value, ks[i].value, a);
|
||||
}
|
||||
}
|
||||
return ks.back().value;
|
||||
}
|
||||
|
||||
glm::vec3 SampleV(const std::vector<SclKey>& ks, float t,
|
||||
const glm::vec3& def)
|
||||
{
|
||||
if (ks.empty()) return def;
|
||||
if (t <= ks.front().time) return ks.front().value;
|
||||
for (size_t i = 1; i < ks.size(); ++i) {
|
||||
if (t <= ks[i].time) {
|
||||
const f32 a = (t - ks[i - 1].time) /
|
||||
(ks[i].time - ks[i - 1].time);
|
||||
return glm::mix(ks[i - 1].value, ks[i].value, a);
|
||||
}
|
||||
}
|
||||
return ks.back().value;
|
||||
}
|
||||
|
||||
glm::quat SampleQ(const std::vector<RotKey>& ks, float t,
|
||||
const glm::quat& def)
|
||||
{
|
||||
if (ks.empty()) return def;
|
||||
if (t <= ks.front().time) return ks.front().value;
|
||||
for (size_t i = 1; i < ks.size(); ++i) {
|
||||
if (t <= ks[i].time) {
|
||||
const f32 a = (t - ks[i - 1].time) /
|
||||
(ks[i].time - ks[i - 1].time);
|
||||
return glm::slerp(ks[i - 1].value, ks[i].value, a);
|
||||
}
|
||||
}
|
||||
return ks.back().value;
|
||||
}
|
||||
|
||||
// 源 clip 于时刻 t 的全骨架世界矩阵(层级 T·R·S 链乘)
|
||||
std::vector<glm::mat4> SrcWorldAt(
|
||||
const std::vector<ModelNode>& nodes,
|
||||
const std::vector<const AnimChannel*>& byNode, float t)
|
||||
{
|
||||
std::vector<glm::mat4> world(nodes.size());
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
const TRS rest = Decompose(nodes[i].localTransform);
|
||||
const AnimChannel* ch =
|
||||
i < byNode.size() ? byNode[i] : nullptr;
|
||||
const glm::vec3 p =
|
||||
ch ? SampleV(ch->positions, t, rest.pos) : rest.pos;
|
||||
const glm::quat r =
|
||||
ch ? SampleQ(ch->rotations, t, rest.rot) : rest.rot;
|
||||
const glm::vec3 s =
|
||||
ch ? SampleV(ch->scales, t, rest.scl) : rest.scl;
|
||||
const glm::mat4 local = glm::translate(glm::mat4(1.0f), p) *
|
||||
glm::mat4_cast(r) *
|
||||
glm::scale(glm::mat4(1.0f), s);
|
||||
const i32 pi = nodes[i].parent;
|
||||
world[i] = (pi >= 0 ? world[static_cast<size_t>(pi)]
|
||||
: glm::mat4(1.0f)) * local;
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
bool ChannelEmpty(const AnimChannel& ch)
|
||||
{
|
||||
return ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty();
|
||||
}
|
||||
|
||||
// 槽位识别(未命中 → Count)
|
||||
HumanoidSlot SlotAt(const std::vector<ModelNode>& nodes, u32 i)
|
||||
{
|
||||
Side side = Side::None;
|
||||
bool ok = false;
|
||||
const HumanoidSlot s = MatchHumanoidSlot(nodes[i].name, side, ok);
|
||||
return ok ? s : HumanoidSlot::Count;
|
||||
}
|
||||
|
||||
// ---- 轨道归类 ----
|
||||
enum class Kind : u8 {
|
||||
Skip, // 无对应(道具/辅助骨)
|
||||
Direct, // 同名直搬(同源拆分资产,原样回放)
|
||||
Joint, // 跨骨架关节烘焙(含手指/趾:统一世界增量通道)
|
||||
Root, // 根运动(骨架上方承载节点 → dst 根)
|
||||
};
|
||||
|
||||
struct SrcSlot {
|
||||
Kind kind = Kind::Skip;
|
||||
u32 dst = kNoNode;
|
||||
// 平移烘焙元数据
|
||||
glm::quat M{kIdentity}; // inv(dstParW)·A·srcParW
|
||||
glm::vec3 dstRestPos{};
|
||||
f32 posScale = 1.0f; // 骨长比(Root 用 unitScale)
|
||||
};
|
||||
|
||||
// 冻结的重定向计划(构建后不可变)
|
||||
struct Plan {
|
||||
std::vector<SrcSlot> bySrc; // index = src node
|
||||
std::vector<u32> carriers; // 承载节点(骨架上方,浅→深)
|
||||
std::vector<glm::quat> srcTPose;
|
||||
std::vector<glm::quat> dstTPose;
|
||||
glm::quat align{kIdentity}; // src 世界 → dst 世界
|
||||
f32 unitScale = 1.0f; // src→dst 整体尺度
|
||||
u32 dstRoot = kNoNode;
|
||||
bool needsBake = false;
|
||||
};
|
||||
|
||||
// 骨长尺度(hips→head 世界距离;槽位不全 → 0)
|
||||
f32 SkeletonSpan(const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& g)
|
||||
{
|
||||
i32 hips = -1;
|
||||
i32 head = -1;
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
const HumanoidSlot slot = SlotAt(nodes, i);
|
||||
if (slot == HumanoidSlot::Hips && hips < 0) {
|
||||
hips = static_cast<i32>(i);
|
||||
} else if (slot == HumanoidSlot::Head && head < 0) {
|
||||
head = static_cast<i32>(i);
|
||||
}
|
||||
}
|
||||
if (hips < 0 || head < 0) return 0.0f;
|
||||
return glm::length(glm::vec3(g[static_cast<size_t>(head)][3]) -
|
||||
glm::vec3(g[static_cast<size_t>(hips)][3]));
|
||||
}
|
||||
|
||||
// 构建冻结计划:归类每个 src 轨道 + 预计算几何补偿元数据
|
||||
Plan BuildPlan(const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
{
|
||||
Plan pl;
|
||||
pl.bySrc.assign(srcNodes.size(), SrcSlot{});
|
||||
|
||||
// dst 根(node 0 即 DFS 首节点)
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
if (dstNodes[i].parent < 0) {
|
||||
pl.dstRoot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 跨骨架配对(AutoMapHumanoid:同名直映 + 槽位一对一)
|
||||
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
|
||||
std::vector<u8> slotBone(srcNodes.size(), 0);
|
||||
for (const auto& [si, di] : pairs) {
|
||||
(void)di;
|
||||
if (SlotAt(srcNodes, si) != HumanoidSlot::Count) {
|
||||
slotBone[si] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 承载节点 = 已识别槽位骨的祖先里**未识别**者(骨架上方根
|
||||
// 运动承载链)。判定必须基于槽位识别而非"所有映射骨祖先"
|
||||
// ——后者把 hips/spine/… 全部内部骨误判进来,导致大半骨架
|
||||
// 走烘焙、叶走直搬,局部化基准错位(整体崩坏根因)
|
||||
std::vector<u8> isCarrier(srcNodes.size(), 0);
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
if (!slotBone[si]) continue;
|
||||
for (i32 p = srcNodes[si].parent; p >= 0;
|
||||
p = srcNodes[static_cast<size_t>(p)].parent) {
|
||||
const u32 pu = static_cast<u32>(p);
|
||||
if (!slotBone[pu]) isCarrier[pu] = 1;
|
||||
}
|
||||
}
|
||||
for (u32 i = 0; i < srcNodes.size(); ++i) {
|
||||
if (isCarrier[i]) pl.carriers.push_back(i);
|
||||
}
|
||||
|
||||
// owner:每个 dst 至多一条轨道(跨骨架对优先占用)
|
||||
std::vector<u32> owner(dstNodes.size(), kNoNode);
|
||||
|
||||
// ① 跨骨架对分类(含手指/趾,统一 → Joint 世界增量烘焙)
|
||||
std::vector<std::pair<u32, u32>> directPairs; // (si,di) 同名
|
||||
for (const auto& [si, di] : pairs) {
|
||||
if (isCarrier[si]) continue; // 承载节点另归
|
||||
if (srcNodes[si].name == dstNodes[di].name) {
|
||||
directPairs.emplace_back(si, di); // 待 ② 定夺
|
||||
continue;
|
||||
}
|
||||
if (owner[di] != kNoNode) continue; // 已被占
|
||||
SrcSlot s;
|
||||
s.dst = di;
|
||||
// 手指/趾与主干同走世界增量烘焙——Unity/UE 语义:指骨即
|
||||
// 普通骨骼,无独立通道。世界增量对骨骼局部轴向约定免疫:
|
||||
// 此前的"指根局部增量"以手骨局部系为基准做共轭,而
|
||||
// Mixamo/Rigify/UE 的手骨轴向约定互异(沿 +X vs 沿 +Y),
|
||||
// 共轭后弯曲轴被拧转 → X Bot 指弯向手背、kwy 指呈爪状
|
||||
//(用户实测)。世界增量只用两侧世界旋转 + 行走基对齐 A
|
||||
s.kind = Kind::Joint;
|
||||
owner[di] = si;
|
||||
pl.bySrc[si] = s;
|
||||
}
|
||||
|
||||
// ② 同名对 → 直搬(仅当目标未被跨骨架对占用;承载节点已排除)
|
||||
for (const auto& [si, di] : directPairs) {
|
||||
if (owner[di] != kNoNode) continue;
|
||||
SrcSlot s;
|
||||
s.kind = Kind::Direct;
|
||||
s.dst = di;
|
||||
pl.bySrc[si] = s;
|
||||
owner[di] = si;
|
||||
}
|
||||
|
||||
// ③ 承载节点 → 根运动(恒不直搬)。仅当 dst 根未被映射骨占用
|
||||
// 时才需要独立通道:dst 根若本身是映射骨(Mixamo 的
|
||||
// mixamorig:Hips 即根),其绝对世界增量已含承载链运动,
|
||||
// 独立通道反而与之重复
|
||||
if (pl.dstRoot != kNoNode && owner[pl.dstRoot] == kNoNode) {
|
||||
for (const u32 c : pl.carriers) {
|
||||
SrcSlot s;
|
||||
s.kind = Kind::Root;
|
||||
s.dst = pl.dstRoot;
|
||||
pl.bySrc[c] = s;
|
||||
}
|
||||
}
|
||||
|
||||
// 是否需要几何烘焙(纯同名资产走快速直搬路径,零开销)
|
||||
for (const auto& s : pl.bySrc) {
|
||||
if (s.kind == Kind::Joint || s.kind == Kind::Root) {
|
||||
pl.needsBake = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pl.needsBake) return pl;
|
||||
|
||||
// ---- 几何补偿元数据(A / T-pose / 平移尺度)----
|
||||
const auto srcG = RestGlobals(srcNodes);
|
||||
const auto dstG = RestGlobals(dstNodes);
|
||||
pl.align = EstimateHumanoidRootAlignment(srcNodes, dstNodes);
|
||||
pl.srcTPose = EstimateHumanoidTPose(srcNodes, srcG);
|
||||
pl.dstTPose = EstimateHumanoidTPose(dstNodes, dstG);
|
||||
const f32 srcSpan = SkeletonSpan(srcNodes, srcG);
|
||||
const f32 dstSpan = SkeletonSpan(dstNodes, dstG);
|
||||
pl.unitScale = srcSpan > 1e-6f ? dstSpan / srcSpan : 1.0f;
|
||||
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
SrcSlot& s = pl.bySrc[si];
|
||||
if (s.kind == Kind::Skip) continue;
|
||||
const u32 di = s.dst;
|
||||
|
||||
// 平移元数据
|
||||
const i32 sp = srcNodes[si].parent;
|
||||
const i32 dp = dstNodes[di].parent;
|
||||
const glm::quat srcParW =
|
||||
sp >= 0 ? RotOf(srcG[static_cast<size_t>(sp)]) : kIdentity;
|
||||
const glm::quat dstParW =
|
||||
dp >= 0 ? RotOf(dstG[static_cast<size_t>(dp)]) : kIdentity;
|
||||
s.M = glm::inverse(dstParW) * pl.align * srcParW;
|
||||
s.dstRestPos = Decompose(dstNodes[di].localTransform).pos;
|
||||
if (s.kind == Kind::Root) {
|
||||
s.posScale = pl.unitScale;
|
||||
} else {
|
||||
const f32 srcLen =
|
||||
glm::length(Decompose(srcNodes[si].localTransform).pos);
|
||||
const f32 dstLen =
|
||||
glm::length(s.dstRestPos);
|
||||
s.posScale = srcLen > 1e-6f ? dstLen / srcLen
|
||||
: pl.unitScale;
|
||||
}
|
||||
}
|
||||
return pl;
|
||||
}
|
||||
|
||||
// 唯一 clip 名(同名冲突逐轮加源文件名前缀)
|
||||
std::string UniqueName(const std::string& base,
|
||||
const std::string& stem,
|
||||
const std::vector<AnimationClip>& existing,
|
||||
const std::vector<AnimationClip>& pending)
|
||||
{
|
||||
const auto collides = [&](const std::string& n) {
|
||||
for (const auto& c : existing) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
for (const auto& c : pending) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
std::string name = base;
|
||||
while (collides(name)) name = stem + "_" + name;
|
||||
return name;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath)
|
||||
AttachResult AttachClipsToNodes(
|
||||
const std::vector<ModelNode>& dstNodes,
|
||||
const std::vector<AnimationClip>& existingClips,
|
||||
ParsedModel&& src, const std::string& sourcePath,
|
||||
std::vector<AnimationClip>& outExtra)
|
||||
{
|
||||
AttachResult res;
|
||||
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
||||
for (const auto& c : target.GetClips()) {
|
||||
for (const auto& c : existingClips) {
|
||||
if (c.sourcePath == sourcePath) {
|
||||
return res;
|
||||
}
|
||||
@@ -73,150 +423,215 @@ namespace Engine::Animation {
|
||||
return res;
|
||||
}
|
||||
|
||||
const std::vector<ModelNode>& dstNodes = target.GetNodes();
|
||||
const std::vector<ModelNode>& srcNodes = src.nodes;
|
||||
|
||||
// ① 同名直映表
|
||||
std::unordered_map<std::string, u32> nameToIdx;
|
||||
nameToIdx.reserve(dstNodes.size());
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
nameToIdx.emplace(dstNodes[i].name, i);
|
||||
}
|
||||
// ② Humanoid 槽位映射表(src nodeIdx → dst nodeIdx)
|
||||
std::unordered_map<u32, u32> retargetMap;
|
||||
std::unordered_map<u32, HumanoidSlot> retargetSlot;
|
||||
{
|
||||
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
|
||||
for (const auto& [si, di] : pairs) {
|
||||
retargetMap.emplace(si, di);
|
||||
// 槽位回查(明细行显示用):源骨骼名识别即可
|
||||
Side side;
|
||||
bool ok = false;
|
||||
retargetSlot.emplace(si, MatchHumanoidSlot(
|
||||
srcNodes[si].name, side, ok));
|
||||
}
|
||||
}
|
||||
// 重定向骨架的 rest(逐 nodeIdx 缓存,避免每 key 重分解)
|
||||
std::unordered_map<u32, TRS> dstRestCache;
|
||||
dstRestCache.reserve(retargetMap.size());
|
||||
for (const auto& [si, di] : retargetMap) {
|
||||
dstRestCache.emplace(si, Decompose(dstNodes[di].localTransform));
|
||||
}
|
||||
|
||||
const Plan pl = BuildPlan(srcNodes, dstNodes);
|
||||
const std::string stem =
|
||||
std::filesystem::path(sourcePath).stem().string();
|
||||
std::vector<AnimationClip> extra;
|
||||
extra.reserve(src.clips.size());
|
||||
|
||||
for (auto& clip : src.clips) {
|
||||
AnimationClip out;
|
||||
out.name = clip.name;
|
||||
out.name = UniqueName(clip.name, stem, existingClips, outExtra);
|
||||
out.duration = clip.duration;
|
||||
out.tps = clip.tps;
|
||||
out.sourcePath = sourcePath;
|
||||
// 同名冲突(内嵌 clip 或其它源):源文件名循环加前缀直到
|
||||
// 唯一。单轮前缀在"前缀名也撞"(如两次 attach 同名资产)时
|
||||
// 仍会产出重复名——Controller/脚本按 clip 名引用,重名即歧义
|
||||
{
|
||||
const auto collides = [&](const std::string& n) {
|
||||
for (const auto& c : target.GetClips()) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
for (const auto& c : extra) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (collides(out.name)) {
|
||||
out.name = stem + "_" + out.name;
|
||||
|
||||
// 每 clip 的 src 轨道索引表(按 nodeIdx)
|
||||
std::vector<const AnimChannel*> srcByNode(srcNodes.size(),
|
||||
nullptr);
|
||||
for (const auto& ch : clip.channels) {
|
||||
if (ch.nodeIdx < srcByNode.size() && !ChannelEmpty(ch)) {
|
||||
srcByNode[ch.nodeIdx] = &ch;
|
||||
}
|
||||
}
|
||||
for (auto& ch : clip.channels) {
|
||||
if (ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty()) {
|
||||
continue; // 空轨道无用
|
||||
|
||||
// ---- 本 clip 的根运动源:承载节点里最深且有轨道者 ----
|
||||
// (更深者世界变换已含其全部祖先运动,取一个即全包括)
|
||||
u32 rootSrc = kNoNode;
|
||||
for (const u32 c : pl.carriers) { // carriers 浅→深
|
||||
if (srcByNode[c] != nullptr) rootSrc = c;
|
||||
}
|
||||
|
||||
// ---- 目标节点 → 源轨道(可控节点,供烘焙局部化)----
|
||||
std::unordered_map<u32, u32> jointBakeDst; // dst → src(Joint/Root)
|
||||
std::unordered_map<u32, u32> directDst; // dst → src(Direct)
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
const SrcSlot& s = pl.bySrc[si];
|
||||
if (srcByNode[si] == nullptr) continue;
|
||||
switch (s.kind) {
|
||||
case Kind::Direct:
|
||||
directDst.emplace(s.dst, si);
|
||||
break;
|
||||
case Kind::Joint:
|
||||
jointBakeDst.emplace(s.dst, si);
|
||||
break;
|
||||
case Kind::Root:
|
||||
if (si == rootSrc) jointBakeDst.emplace(s.dst, si);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 旋转键:两遍式烘焙 ----
|
||||
// 采样时刻 = 全部需烘焙通道(Joint/Root)的旋转键时刻并集;
|
||||
// 逐 t 跑源全骨架 FK → 逐 dst 递推世界旋转 → 局部化写键
|
||||
std::unordered_map<u32, std::vector<RotKey>> baked;
|
||||
{
|
||||
std::vector<f32> times;
|
||||
for (u32 si = 0; si < srcNodes.size(); ++si) {
|
||||
const SrcSlot& s = pl.bySrc[si];
|
||||
const AnimChannel* ch = srcByNode[si];
|
||||
if (ch == nullptr) continue;
|
||||
const bool bakeThis =
|
||||
s.kind == Kind::Joint ||
|
||||
(s.kind == Kind::Root && si == rootSrc);
|
||||
if (!bakeThis || ch->rotations.empty()) continue;
|
||||
for (const auto& k : ch->rotations) times.push_back(k.time);
|
||||
}
|
||||
std::sort(times.begin(), times.end());
|
||||
times.erase(std::unique(times.begin(), times.end()),
|
||||
times.end());
|
||||
|
||||
if (!times.empty()) {
|
||||
std::vector<glm::quat> Rw(dstNodes.size(), kIdentity);
|
||||
for (const f32 t : times) {
|
||||
const auto Wsrc = SrcWorldAt(srcNodes, srcByNode, t);
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
const i32 pi = dstNodes[i].parent;
|
||||
const glm::quat Rpar =
|
||||
pi >= 0 ? Rw[static_cast<size_t>(pi)]
|
||||
: kIdentity;
|
||||
glm::quat RwI;
|
||||
const auto jb = jointBakeDst.find(i);
|
||||
if (jb != jointBakeDst.end()) {
|
||||
// 关节/根/手指/趾:世界增量搬运(对两侧
|
||||
// 骨骼的局部轴向约定免疫)
|
||||
const u32 si = jb->second;
|
||||
const glm::quat dW =
|
||||
pl.align *
|
||||
(RotOf(Wsrc[si]) *
|
||||
glm::inverse(pl.srcTPose
|
||||
[static_cast<size_t>(si)])) *
|
||||
glm::inverse(pl.align);
|
||||
RwI = dW * pl.dstTPose[static_cast<size_t>(i)];
|
||||
} else if (auto db = directDst.find(i);
|
||||
db != directDst.end()) {
|
||||
// 直搬骨:anim 后世界 = 父 × 源局部(供其子
|
||||
// 关节局部化用;自身轨道另走原样直搬)
|
||||
const u32 si = db->second;
|
||||
const glm::quat loc = SampleQ(
|
||||
srcByNode[si]->rotations, t,
|
||||
Decompose(srcNodes[si].localTransform).rot);
|
||||
RwI = Rpar * loc;
|
||||
} else {
|
||||
RwI = Rpar * Decompose(
|
||||
dstNodes[i].localTransform).rot;
|
||||
}
|
||||
Rw[static_cast<size_t>(i)] = RwI;
|
||||
if (jb != jointBakeDst.end()) {
|
||||
baked[i].push_back(
|
||||
{t, glm::inverse(Rpar) * RwI});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 组装轨道(每 dst 至多一条;owner 在 Plan 阶段已保证)----
|
||||
// 非 const 引用:同名直搬路径整段 move 源轨道(此后 src 不再
|
||||
// 被读——烘焙已在上面完成)
|
||||
for (auto& ch : clip.channels) {
|
||||
if (ChannelEmpty(ch)) continue;
|
||||
if (ch.nodeIdx >= srcNodes.size()) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
const SrcSlot& s = pl.bySrc[ch.nodeIdx];
|
||||
const std::string& boneName = srcNodes[ch.nodeIdx].name;
|
||||
|
||||
AnimChannel remapped;
|
||||
if (auto it = nameToIdx.find(boneName);
|
||||
it != nameToIdx.end()) {
|
||||
// ① 同名直搬
|
||||
remapped.nodeIdx = it->second;
|
||||
remapped.positions = std::move(ch.positions);
|
||||
remapped.rotations = std::move(ch.rotations);
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
if (s.kind == Kind::Skip) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
if (s.kind == Kind::Root && ch.nodeIdx != rootSrc) {
|
||||
continue; // 更深的承载节点已承载根运动,其余并入
|
||||
}
|
||||
|
||||
AnimChannel outCh;
|
||||
outCh.nodeIdx = s.dst;
|
||||
|
||||
if (s.kind == Kind::Direct) {
|
||||
// 同名直搬:原样(同资产体系局部空间同源)
|
||||
outCh.positions = std::move(ch.positions);
|
||||
outCh.rotations = std::move(ch.rotations);
|
||||
outCh.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(outCh));
|
||||
++res.channelsMatched;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 平移:局部增量 × 尺度
|
||||
const TRS srcRest =
|
||||
Decompose(srcNodes[ch.nodeIdx].localTransform);
|
||||
outCh.positions.reserve(ch.positions.size());
|
||||
for (const auto& k : ch.positions) {
|
||||
outCh.positions.push_back(
|
||||
{k.time, s.dstRestPos +
|
||||
(s.M * (k.value - srcRest.pos)) *
|
||||
s.posScale});
|
||||
}
|
||||
// 旋转:烘焙结果(多源汇一 dst 时共享同份)
|
||||
if (auto bk = baked.find(s.dst); bk != baked.end()) {
|
||||
outCh.rotations = bk->second;
|
||||
}
|
||||
// 缩放不搬运:跨骨架源的缩放轨道常承载单位换算
|
||||
// (kwy 根节点的 cm 补偿等),原样拷到目标会整体放大
|
||||
// 骨骼;目标保持自身 rest 缩放(人体动画一般无缩放)
|
||||
out.channels.push_back(std::move(outCh));
|
||||
++res.channelsRetargeted;
|
||||
|
||||
// ② 槽位重定向:增量烘焙到目标骨架局部空间。
|
||||
// 平移增量按骨长比缩放(|dstRest| / |srcRest|)——
|
||||
// 两资产单位/比例不同源(FBX 常为 cm、glTF 为 m)时
|
||||
// 不缩放会把模型拽飞;rest≈0 的根骨骼比例取 1
|
||||
const auto mapIt = retargetMap.find(ch.nodeIdx);
|
||||
if (mapIt == retargetMap.end()) {
|
||||
++res.channelsSkipped; // 两级都落不上(手指/道具等)
|
||||
continue;
|
||||
}
|
||||
const TRS srcRest = Decompose(
|
||||
srcNodes[ch.nodeIdx].localTransform);
|
||||
const TRS& dstRest = dstRestCache[ch.nodeIdx];
|
||||
remapped.nodeIdx = mapIt->second;
|
||||
const f32 srcLen = glm::length(srcRest.pos);
|
||||
const f32 ratio =
|
||||
(srcLen > 1e-6f) ? (glm::length(dstRest.pos) / srcLen)
|
||||
: 1.0f;
|
||||
|
||||
remapped.positions.reserve(ch.positions.size());
|
||||
for (const auto& k : ch.positions) {
|
||||
remapped.positions.push_back(
|
||||
{k.time, dstRest.pos +
|
||||
(k.value - srcRest.pos) * ratio});
|
||||
}
|
||||
remapped.rotations.reserve(ch.rotations.size());
|
||||
for (const auto& k : ch.rotations) {
|
||||
const glm::quat delta =
|
||||
k.value * glm::inverse(srcRest.rot);
|
||||
remapped.rotations.push_back(
|
||||
{k.time, delta * dstRest.rot});
|
||||
}
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
++res.channelsRetargeted;
|
||||
|
||||
// 明细行(同骨骼多 clip 去重)
|
||||
const bool newRow = std::none_of(
|
||||
res.retargetRows.begin(), res.retargetRows.end(),
|
||||
[&](const RetargetRow& r)
|
||||
{ return r.srcName == boneName; });
|
||||
if (newRow) {
|
||||
res.retargetRows.push_back(
|
||||
{HumanoidSlotName(retargetSlot[ch.nodeIdx]),
|
||||
boneName, dstNodes[mapIt->second].name});
|
||||
const bool newRow = std::none_of(
|
||||
res.retargetRows.begin(), res.retargetRows.end(),
|
||||
[&](const RetargetRow& r)
|
||||
{ return r.srcName == boneName; });
|
||||
if (newRow) {
|
||||
const HumanoidSlot slot = SlotAt(srcNodes, ch.nodeIdx);
|
||||
res.retargetRows.push_back(
|
||||
{s.kind == Kind::Root
|
||||
? "RootMotion"
|
||||
: HumanoidSlotName(slot),
|
||||
boneName, dstNodes[s.dst].name});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 全部轨道落不上 = 无可驱动骨骼,clip 不入表
|
||||
|
||||
if (out.channels.empty()) {
|
||||
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
|
||||
"bones in target (skipped)",
|
||||
clip.name);
|
||||
continue;
|
||||
}
|
||||
extra.push_back(std::move(out));
|
||||
outExtra.push_back(std::move(out));
|
||||
++res.clipsAdded;
|
||||
}
|
||||
|
||||
if (!outExtra.empty()) {
|
||||
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
|
||||
"({} named, {} retargeted, {} skipped)",
|
||||
sourcePath, res.clipsAdded, res.channelsMatched,
|
||||
res.channelsRetargeted, res.channelsSkipped);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath)
|
||||
{
|
||||
std::vector<AnimationClip> extra;
|
||||
AttachResult res = AttachClipsToNodes(
|
||||
target.GetNodes(), target.GetClips(), std::move(src),
|
||||
sourcePath, extra);
|
||||
if (!extra.empty()) {
|
||||
target.AppendClips(std::move(extra));
|
||||
}
|
||||
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
|
||||
"({} named, {} retargeted, {} skipped)",
|
||||
sourcePath, res.clipsAdded, res.channelsMatched,
|
||||
res.channelsRetargeted, res.channelsSkipped);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,25 +44,62 @@ namespace Engine::Animation {
|
||||
}
|
||||
}
|
||||
if (r <= 1e-4f || ch.empty()) return;
|
||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π),wrap 补全;方向子
|
||||
// ≤16 个,栈数组零堆分配)
|
||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π);方向子 ≤16 个,
|
||||
// 栈数组零堆分配)
|
||||
constexpr f32 kTwoPi = 6.2831853f;
|
||||
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
|
||||
u32 idx[16];
|
||||
f32 ang[16];
|
||||
constexpr u32 kMaxDir = 16;
|
||||
u32 idx[kMaxDir];
|
||||
f32 ang[kMaxDir];
|
||||
u32 nd = 0;
|
||||
for (u32 i = 0; i < ch.size() && nd < 16; ++i) {
|
||||
for (u32 i = 0; i < ch.size() && nd < kMaxDir; ++i) {
|
||||
if (glm::length(ch[i].pos) < 1e-4f) continue;
|
||||
idx[nd] = i;
|
||||
ang[nd] = glm::mod(glm::atan(ch[i].pos.y, ch[i].pos.x), kTwoPi);
|
||||
++nd;
|
||||
}
|
||||
if (nd == 0) return;
|
||||
// 环序自排序(插入排序,nd≤16 就地零堆分配):扇区数学假设
|
||||
// "数组相邻 = 角度环相邻",输入顺序不可信——编辑器实时改树
|
||||
// 不重跑 Load 期排序,乱序时右侧拖拽会配到错误的扇区邻居
|
||||
for (u32 i = 1; i < nd; ++i) {
|
||||
const u32 ki = idx[i];
|
||||
const f32 ai = ang[i];
|
||||
u32 j = i;
|
||||
while (j > 0 && ang[j - 1] > ai) {
|
||||
idx[j] = idx[j - 1];
|
||||
ang[j] = ang[j - 1];
|
||||
--j;
|
||||
}
|
||||
idx[j] = ki;
|
||||
ang[j] = ai;
|
||||
}
|
||||
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
|
||||
if (nd == 1) {
|
||||
// 单方向子:无扇区可插值,全权重归它(否则角度不等于
|
||||
// 该方向时落空退化成纯 idle)
|
||||
out.push_back({idx[0], r});
|
||||
return;
|
||||
}
|
||||
for (u32 i = 0; i < nd; ++i) {
|
||||
const u32 j = (i + 1) % nd;
|
||||
const f32 ai = ang[i];
|
||||
const f32 aj = (j == 0) ? ang[0] + kTwoPi : ang[j];
|
||||
if (theta >= ai && theta <= aj) {
|
||||
const f32 span = aj - ai;
|
||||
if (j == 0) {
|
||||
// wrap 扇区 [ai,2π)∪[0,ang0]:用 || 连接,&& 会让
|
||||
// theta ∈ [0,ang0) 落空(首方向不在 0° 时拖右侧无匹配)
|
||||
if (theta >= ai || theta <= ang[0]) {
|
||||
const f32 span = ang[0] + kTwoPi - ai;
|
||||
const f32 tt =
|
||||
theta >= ai ? theta - ai : theta + kTwoPi - ai;
|
||||
const f32 t = span > 1e-6f ? tt / span : 0.0f;
|
||||
const f32 wi = (1.0f - t) * r;
|
||||
const f32 wj = t * r;
|
||||
if (wi >= kMinBlendWeight) out.push_back({idx[i], wi});
|
||||
if (wj >= kMinBlendWeight)
|
||||
out.push_back({idx[0], wj});
|
||||
break;
|
||||
}
|
||||
} else if (theta >= ai && theta <= ang[j]) {
|
||||
const f32 span = ang[j] - ai;
|
||||
const f32 t = span > 1e-6f ? (theta - ai) / span : 0.0f;
|
||||
const f32 wi = (1.0f - t) * r;
|
||||
const f32 wj = t * r;
|
||||
|
||||
@@ -128,7 +128,8 @@ namespace Engine::Animation {
|
||||
void AnimationEvaluator::Evaluate(const Model& model,
|
||||
const AnimationClip& cur, f32 curTime,
|
||||
const CrossfadeState& xf,
|
||||
std::vector<glm::mat4>& outGlobals)
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace)
|
||||
{
|
||||
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
|
||||
// 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
|
||||
@@ -141,13 +142,14 @@ namespace Engine::Animation {
|
||||
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
|
||||
count = 2;
|
||||
}
|
||||
EvaluateLayers(model, layers, count, outGlobals);
|
||||
EvaluateLayers(model, layers, count, outGlobals, inPlace);
|
||||
}
|
||||
|
||||
void AnimationEvaluator::EvaluateLayers(const Model& model,
|
||||
const PoseLayer* layers,
|
||||
u32 count,
|
||||
std::vector<glm::mat4>& outGlobals)
|
||||
std::vector<glm::mat4>& outGlobals,
|
||||
bool inPlace)
|
||||
{
|
||||
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
|
||||
if (m_Layers.size() < count) {
|
||||
@@ -208,6 +210,24 @@ namespace Engine::Animation {
|
||||
acc += m_Sampled[l].weight;
|
||||
}
|
||||
}
|
||||
// ---- 根运动裁剪(inPlace,Unity "Apply Root Motion" 关闭同
|
||||
// 语义):动画子树根节点(touched 且父未被触碰)位移回
|
||||
// 静止值,旋转/缩放保留——跑动 clip 不再漂出实体锚点,
|
||||
// 位移归脚本/物理驱动。混合后统一裁剪,crossfade 双层
|
||||
// 与 BlendTree N 层一个入口全覆盖
|
||||
if (inPlace) {
|
||||
const auto& nodes = model.GetNodes();
|
||||
for (size_t k = 0; k < nodes.size(); ++k) {
|
||||
if (!m_A.touched[k]) continue;
|
||||
const i32 parent = nodes[k].parent;
|
||||
if (parent < 0 ||
|
||||
!m_A.touched[static_cast<size_t>(parent)]) {
|
||||
// FBX PreRotation 折在 localTransform 里,静止位移
|
||||
// 直接取平移列(不拆 TRS 重建,避免丢 PreRotation)
|
||||
m_A.pos[k] = glm::vec3(nodes[k].localTransform[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
ComposeGlobals(model, m_A, outGlobals);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,8 @@ namespace Engine::Animation {
|
||||
}
|
||||
|
||||
// ---- 姿态求值(双层混合在求值器内部)----
|
||||
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals);
|
||||
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals,
|
||||
!anim.applyRootMotion);
|
||||
PushPalette(entity, model);
|
||||
});
|
||||
|
||||
@@ -493,7 +494,8 @@ namespace Engine::Animation {
|
||||
m_Evaluator.EvaluateLayers(*model, m_LayerScratch.data(),
|
||||
static_cast<u32>(
|
||||
m_LayerScratch.size()),
|
||||
m_Globals);
|
||||
m_Globals,
|
||||
!anim.applyRootMotion);
|
||||
PushPalette(entity, model);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,52 +1,359 @@
|
||||
//
|
||||
// HumanoidRig 实现:关键词表 + 归一化 + 自动映射。
|
||||
// HumanoidRig 实现:standard / dialects / mapper 三层 + 行走基估计。
|
||||
//
|
||||
// 分层职责(Unity Avatar 语义,见头文件架构说明):
|
||||
// standard — 槽位结构化访问(侧别/镜像/指骨算术/显示名)
|
||||
// dialects — 命名方言数据表(主干统一表 + 指骨三约定展开表)
|
||||
// mapper — 归一化 + 查表识别 + 跨骨架配对
|
||||
// 行走基/T-pose 估计 — rest 姿态几何推导,与命名无关
|
||||
//
|
||||
|
||||
#include <Engine/Animation/HumanoidRig.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
// ==================================================================
|
||||
// standard:槽位结构化访问
|
||||
// (概念分层:公共 API 在 Engine::Animation 作用域,与头文件声明
|
||||
// 一致;真正的 C++ 内层 namespace 只用于 dialects/mapper 内部表)
|
||||
// ==================================================================
|
||||
|
||||
// ---- 名字归一化:拆 (body, side) ----
|
||||
//
|
||||
// 规则:
|
||||
// 1) 去 "mixamorig:" 等前缀
|
||||
// 2) 小写;分隔符(空格/_/.-:)一律删除
|
||||
// 3) left/right(Mixamo "LeftArm")与 左/右(MMD "左腕")统一
|
||||
// 抽成 side,body 里剔除;.L/.R、_L/_R 后缀同上
|
||||
//
|
||||
// body 用**精确相等**匹配关键词(非子串)——"lowerarm"
|
||||
// 不会误命中 "arm"
|
||||
Side SideOfSlot(HumanoidSlot slot)
|
||||
{
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
if (s < 6) return Side::None; // 主干
|
||||
return s < 29 ? Side::Left : Side::Right;
|
||||
}
|
||||
|
||||
Side ExtractSide(std::string& body)
|
||||
HumanoidSlot MirrorSlot(HumanoidSlot slot)
|
||||
{
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
// 布局:主干 [0..5] 无镜像;左区 [6..28] 与右区 [29..51]
|
||||
// 完全同构(4 臂 + 15 指 + 4 腿趾),偏移 23
|
||||
if (s < 6 || s >= static_cast<u32>(HumanoidSlot::Count)) {
|
||||
return slot;
|
||||
}
|
||||
return static_cast<HumanoidSlot>(s < 29 ? s + 23 : s - 23);
|
||||
}
|
||||
|
||||
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg)
|
||||
{
|
||||
// 布局:LeftThumb1=10 起,5 指 × 3 段连续;右侧同构 +23
|
||||
const u32 base =
|
||||
side == Side::Left ? 10 : 10 + 23;
|
||||
return static_cast<HumanoidSlot>(
|
||||
base + static_cast<u32>(kind) * 3 + (seg - 1));
|
||||
}
|
||||
|
||||
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
|
||||
u32& seg)
|
||||
{
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
const bool inL = s >= 10 && s <= 24;
|
||||
const bool inR = s >= 33 && s <= 47;
|
||||
if (!inL && !inR) return false;
|
||||
side = inL ? Side::Left : Side::Right;
|
||||
const u32 rel = s - (inL ? 10u : 33u);
|
||||
kind = static_cast<FingerKind>(rel / 3);
|
||||
seg = rel % 3 + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* HumanoidSlotName(HumanoidSlot slot)
|
||||
{
|
||||
static constexpr std::array<const char*, 52> kNames{
|
||||
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
|
||||
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
||||
"LeftThumb1", "LeftThumb2", "LeftThumb3",
|
||||
"LeftIndex1", "LeftIndex2", "LeftIndex3",
|
||||
"LeftMiddle1", "LeftMiddle2", "LeftMiddle3",
|
||||
"LeftRing1", "LeftRing2", "LeftRing3",
|
||||
"LeftPinky1", "LeftPinky2", "LeftPinky3",
|
||||
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot", "LeftToes",
|
||||
"RightShoulder", "RightUpperArm", "RightLowerArm",
|
||||
"RightHand",
|
||||
"RightThumb1", "RightThumb2", "RightThumb3",
|
||||
"RightIndex1", "RightIndex2", "RightIndex3",
|
||||
"RightMiddle1", "RightMiddle2", "RightMiddle3",
|
||||
"RightRing1", "RightRing2", "RightRing3",
|
||||
"RightPinky1", "RightPinky2", "RightPinky3",
|
||||
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
||||
"RightToes",
|
||||
};
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
return s < kNames.size() ? kNames[s] : "?";
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// dialects:命名方言数据表(按"命名规定 / convention"分区)
|
||||
//
|
||||
// 识别 = 归一化 body 精确查表(Unity 的 token 规则表同思想)。
|
||||
// 覆盖的命名规定:
|
||||
// 通用英文 Unity / Blender / 多数 DCC 导出的英文名
|
||||
// Mixamo Adobe Mixamo(导入前已剥 "mixamorig:" 前缀)
|
||||
// MMD / PMX MikuMikuDance 標準ボーン(日文名 + 全角数字)
|
||||
// Rigify Blender Rigify 变形骨(DEF- 前缀)
|
||||
// UE Unreal Mannequin / IK Rig(段词 Proximal…)
|
||||
// 3ds Max 3ds Max Biped(bip01)
|
||||
// 主干(trunk)与指/趾(finger)各一张表:两表键空间不重叠,
|
||||
// 查表主干优先、指骨兜底。各分区键名互不重叠,查表取首个命中,
|
||||
// 分区顺序仅为归类、无功能含义。
|
||||
// ==================================================================
|
||||
namespace dialects {
|
||||
|
||||
// ---- 主干表(body = side 抽取 + 分隔符剥离后的左槽名)----
|
||||
struct TrunkEntry {
|
||||
std::string_view body;
|
||||
HumanoidSlot slot;
|
||||
};
|
||||
|
||||
constexpr std::array<TrunkEntry, 61> kTrunk{{
|
||||
// ==== 通用英文(Unity / Blender / DCC)====
|
||||
// 躯干:spine…/chest/spine1 与 upperchest/spine2 同义梯队
|
||||
{"hips", HumanoidSlot::Hips},
|
||||
{"pelvis", HumanoidSlot::Hips},
|
||||
{"spine", HumanoidSlot::Spine},
|
||||
{"chest", HumanoidSlot::Chest},
|
||||
{"spine1", HumanoidSlot::Chest},
|
||||
{"upperchest", HumanoidSlot::UpperChest},
|
||||
{"spine2", HumanoidSlot::UpperChest},
|
||||
{"neck", HumanoidSlot::Neck},
|
||||
{"head", HumanoidSlot::Head},
|
||||
// 臂:arm/upperarm/bicep 同义;lowerarm/forearm/elbow 同义
|
||||
{"shoulder", HumanoidSlot::LeftShoulder},
|
||||
{"clavicle", HumanoidSlot::LeftShoulder},
|
||||
{"arm", HumanoidSlot::LeftUpperArm},
|
||||
{"upperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"bicep", HumanoidSlot::LeftUpperArm},
|
||||
{"lowerarm", HumanoidSlot::LeftLowerArm},
|
||||
{"forearm", HumanoidSlot::LeftLowerArm},
|
||||
{"elbow", HumanoidSlot::LeftLowerArm},
|
||||
{"hand", HumanoidSlot::LeftHand},
|
||||
// 腿:upperleg/upleg/thigh 同义;lowerleg/leg/knee/calf 同义
|
||||
{"upperleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"upleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"thigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"lowerleg", HumanoidSlot::LeftLowerLeg},
|
||||
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo LeftLeg = 小腿
|
||||
{"knee", HumanoidSlot::LeftLowerLeg},
|
||||
{"calf", HumanoidSlot::LeftLowerLeg},
|
||||
{"foot", HumanoidSlot::LeftFoot},
|
||||
{"ankle", HumanoidSlot::LeftFoot},
|
||||
{"toebase", HumanoidSlot::LeftToes},
|
||||
{"toes", HumanoidSlot::LeftToes},
|
||||
{"toe", HumanoidSlot::LeftToes},
|
||||
|
||||
// ==== MMD / PMX 標準ボーン ====
|
||||
// 下半身=骨盆、上半身链=spine→chest→upperchest、腕=上臂、
|
||||
// ひじ/肘=肘、手首=腕关、足=大腿、ひざ=膝、足首=踝、つま先=趾。
|
||||
// センター(根运动控制骨)与下半身同为 Hips 候选,实际取
|
||||
// DFS 首个命中者(下半身),センター 落为承载节点走根运动
|
||||
{"下半身", HumanoidSlot::Hips},
|
||||
{"センター", HumanoidSlot::Hips},
|
||||
{"上半身", HumanoidSlot::Spine},
|
||||
{"上半身1", HumanoidSlot::Chest},
|
||||
{"上半身2", HumanoidSlot::UpperChest},
|
||||
{"首", HumanoidSlot::Neck},
|
||||
{"頭", HumanoidSlot::Head},
|
||||
{"肩", HumanoidSlot::LeftShoulder},
|
||||
{"腕", HumanoidSlot::LeftUpperArm},
|
||||
{"ひじ", HumanoidSlot::LeftLowerArm},
|
||||
{"肘", HumanoidSlot::LeftLowerArm},
|
||||
{"手首", HumanoidSlot::LeftHand},
|
||||
{"足", HumanoidSlot::LeftUpperLeg},
|
||||
{"ひざ", HumanoidSlot::LeftLowerLeg},
|
||||
{"足首", HumanoidSlot::LeftFoot},
|
||||
{"つま先", HumanoidSlot::LeftToes},
|
||||
|
||||
// ==== Rigify(Blender)DEF- 变形骨 ====
|
||||
// 前缀保留参与精确匹配——裸 "spine" 在 Mixamo(脊柱段)与
|
||||
// Rigify(DEF-spine=骨盆变形骨)间语义冲突,独立键名避免
|
||||
// 误伤既有约定。段号实测自 Lumine(.003 挂肩=胸上段、.005
|
||||
// 颈、.006 头——眼/牙/发均挂其下)
|
||||
{"defspine", HumanoidSlot::Hips},
|
||||
{"defspine001", HumanoidSlot::Spine},
|
||||
{"defspine002", HumanoidSlot::Chest},
|
||||
{"defspine003", HumanoidSlot::UpperChest},
|
||||
{"defspine005", HumanoidSlot::Neck},
|
||||
{"defspine006", HumanoidSlot::Head},
|
||||
{"defshoulder", HumanoidSlot::LeftShoulder},
|
||||
{"defupperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"defforearm", HumanoidSlot::LeftLowerArm},
|
||||
{"defhand", HumanoidSlot::LeftHand},
|
||||
{"defthigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"defshin", HumanoidSlot::LeftLowerLeg},
|
||||
{"deffoot", HumanoidSlot::LeftFoot},
|
||||
{"deftoe", HumanoidSlot::LeftToes},
|
||||
|
||||
// ==== 3ds Max Biped ====
|
||||
{"bip01", HumanoidSlot::Hips},
|
||||
}};
|
||||
|
||||
// ---- 指骨展开表(按命名规定分区)----
|
||||
// 四种段号约定(实测来源):
|
||||
// Mixamo LeftHandThumb1..3 → hand + 指名 + 裸数字
|
||||
// Rigify DEF-thumb.01..03 → (def)? + 指名 + 零填充两位;
|
||||
// 四指带 f_ 前缀(findex...),
|
||||
// 拇指没有;master 裸名同构
|
||||
// UE/kwy Thumb Proximal_L 等 → 指名 + 段词
|
||||
// (Proximal/Intermediate/Distal = 近/中/末节,
|
||||
// 小指叫 little 非 pinky);趾为复数 Toes
|
||||
// MMD/PMX 親指0..2 / 人指1..3 / 中指1..3 / 薬指1..3 / 小指1..3
|
||||
// → 指名 + 数字(拇指 0 起、其余 1 起;全角数字已由
|
||||
// mapper::FoldFullWidthDigits 折半)
|
||||
// 表键为左侧 body(side 由归一化抽取),右槽经 MirrorSlot。
|
||||
// 各约定的「…先 / 段 4」指尖均不展开——标准拓扑无对应槽位,
|
||||
// 源轨道识别失败由 attach 侧 skip(与旧行为一致)。
|
||||
struct BodySlot {
|
||||
std::string body;
|
||||
HumanoidSlot slot;
|
||||
};
|
||||
|
||||
const std::vector<BodySlot>& FingerPatterns()
|
||||
{
|
||||
Side s = Side::None;
|
||||
auto cut = [&](std::string_view token, Side which)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = body.find(token, pos)) != std::string::npos) {
|
||||
body.erase(pos, token.size());
|
||||
if (s == Side::None) s = which;
|
||||
static const std::vector<BodySlot> kTable = [] {
|
||||
std::vector<BodySlot> t;
|
||||
t.reserve(80);
|
||||
|
||||
// Mixamo:handthumb1 ... handpinky3
|
||||
constexpr std::array<std::string_view, 5> kMixamo{
|
||||
"thumb", "index", "middle", "ring", "pinky"};
|
||||
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
1));
|
||||
for (u32 seg = 1; seg <= 3; ++seg) {
|
||||
t.push_back({std::string("hand") +
|
||||
std::string(kMixamo[kindU]) +
|
||||
std::to_string(seg),
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
}
|
||||
}
|
||||
};
|
||||
cut("left", Side::Left);
|
||||
cut("right", Side::Right);
|
||||
cut("左", Side::Left); // MMD 前缀
|
||||
cut("右", Side::Right);
|
||||
cut(".l", Side::Left); // blender 惯例后缀
|
||||
cut(".r", Side::Right);
|
||||
cut("_l", Side::Left);
|
||||
cut("_r", Side::Right);
|
||||
return s;
|
||||
|
||||
// Rigify:defthumb01 ... deffpinky03(+ 无前缀 master 变体)
|
||||
constexpr std::array<std::string_view, 5> kRigify{
|
||||
"thumb", "findex", "fmiddle", "fring", "fpinky"};
|
||||
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
1));
|
||||
for (u32 seg = 1; seg <= 3; ++seg) {
|
||||
const std::string z =
|
||||
std::to_string(100 + seg).substr(1); // 01..03
|
||||
const std::string core =
|
||||
std::string(kRigify[kindU]) + z;
|
||||
t.push_back({"def" + core,
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
t.push_back({core,
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
}
|
||||
}
|
||||
|
||||
// UE:thumbproximal ... littledistal
|
||||
constexpr std::array<std::string_view, 5> kUe{
|
||||
"thumb", "index", "middle", "ring", "little"};
|
||||
constexpr std::array<std::string_view, 3> kUeSeg{
|
||||
"proximal", "intermediate", "distal"};
|
||||
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
1));
|
||||
for (u32 seg = 1; seg <= 3; ++seg) {
|
||||
t.push_back({std::string(kUe[kindU]) +
|
||||
std::string(kUeSeg[seg - 1]),
|
||||
static_cast<HumanoidSlot>(
|
||||
slot + seg - 1)});
|
||||
}
|
||||
}
|
||||
// MMD/PMX:親指0..2 / 人指1..3 / 中指1..3 / 薬指1..3 /
|
||||
// 小指1..3(拇指 0 起、其余 1 起;数字已折半角)
|
||||
{
|
||||
constexpr std::array<std::string_view, 5> kMmd{
|
||||
"親指", "人指", "中指", "薬指", "小指"};
|
||||
for (u32 kindU = 0; kindU < 5; ++kindU) {
|
||||
const u32 numBase = kindU == 0 ? 0u : 1u;
|
||||
for (u32 j = 0; j < 3; ++j) {
|
||||
const auto slot = static_cast<u32>(
|
||||
FingerSlot(Side::Left,
|
||||
static_cast<FingerKind>(kindU),
|
||||
j + 1));
|
||||
t.push_back({std::string(kMmd[kindU]) +
|
||||
std::to_string(numBase + j),
|
||||
static_cast<HumanoidSlot>(slot)});
|
||||
}
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
return kTable;
|
||||
}
|
||||
|
||||
// ① 前缀剥离 + 小写(分隔符**保留**——后缀式 side 标记
|
||||
// 查表:主干优先,指骨兜底(两表键空间无交集)
|
||||
bool Lookup(const std::string& body, HumanoidSlot& out)
|
||||
{
|
||||
for (const auto& e : kTrunk) {
|
||||
if (body == e.body) {
|
||||
out = e.slot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const auto& e : FingerPatterns()) {
|
||||
if (body == e.body) {
|
||||
out = e.slot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dialects
|
||||
|
||||
// ==================================================================
|
||||
// mapper:归一化 + 识别 + 配对
|
||||
// ==================================================================
|
||||
namespace mapper {
|
||||
|
||||
// ⓪ 全角数字折半角:U+FF10..U+FF19 → '0'..'9'(UTF-8 EF BC 90..99)。
|
||||
// MMD 規範骨常写全角数字(親指0/中指1…),折半后各命名规定可在
|
||||
// 表里统一用半角书写。仅折数字,全角字母(如「つま先IK」)不动
|
||||
// ——避免 IK 骨误命中「つま先」
|
||||
void FoldFullWidthDigits(std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
if (i + 3 <= s.size() &&
|
||||
static_cast<unsigned char>(s[i]) == 0xEF &&
|
||||
static_cast<unsigned char>(s[i + 1]) == 0xBC) {
|
||||
const auto b2 = static_cast<unsigned char>(s[i + 2]);
|
||||
if (b2 >= 0x90 && b2 <= 0x99) {
|
||||
out.push_back(
|
||||
static_cast<char>('0' + (b2 - 0x90)));
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push_back(s[i++]);
|
||||
}
|
||||
s.swap(out);
|
||||
}
|
||||
|
||||
// ① 前缀剥离 + 小写(分隔符**保留**——末尾式 side 标记
|
||||
// ".l"/"_l" 依赖分隔符存在,必须先于分隔符删除识别)
|
||||
std::string NormalizeLower(std::string_view name)
|
||||
{
|
||||
@@ -63,10 +370,11 @@ namespace Engine::Animation {
|
||||
c = static_cast<char>(std::tolower(
|
||||
static_cast<unsigned char>(c)));
|
||||
}
|
||||
FoldFullWidthDigits(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ② 分隔符删除(在 ExtractSide 之后调用;CJK 多字节原样过)
|
||||
// ② 分隔符删除(side 抽取之后调用;CJK 多字节原样过)
|
||||
std::string StripSeparators(const std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
@@ -80,105 +388,99 @@ namespace Engine::Animation {
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- body → 左侧槽位 关键词表(右侧 = 左槽 + 段偏移)----
|
||||
// 精确相等匹配;顺序即优先级(先命中先用)
|
||||
struct BodyRule {
|
||||
std::string_view body;
|
||||
HumanoidSlot left;
|
||||
};
|
||||
|
||||
constexpr std::array<BodyRule, 43> kBodyRules{{
|
||||
// 主干(MMD 语义:下半身=hips、上半身链=spine→chest)
|
||||
{"hips", HumanoidSlot::Hips},
|
||||
{"pelvis", HumanoidSlot::Hips},
|
||||
{"bip01", HumanoidSlot::Hips},
|
||||
{"下半身", HumanoidSlot::Hips},
|
||||
{"センター", HumanoidSlot::Hips},
|
||||
{"spine", HumanoidSlot::Spine},
|
||||
{"上半身", HumanoidSlot::Spine},
|
||||
{"chest", HumanoidSlot::Chest},
|
||||
{"spine1", HumanoidSlot::Chest},
|
||||
{"上半身1", HumanoidSlot::Chest},
|
||||
{"upperchest", HumanoidSlot::UpperChest},
|
||||
{"spine2", HumanoidSlot::UpperChest},
|
||||
{"上半身2", HumanoidSlot::UpperChest},
|
||||
{"neck", HumanoidSlot::Neck},
|
||||
{"首", HumanoidSlot::Neck},
|
||||
{"head", HumanoidSlot::Head},
|
||||
{"頭", HumanoidSlot::Head},
|
||||
// 臂段(MMD:腕=upper arm、ひじ/肘=elbow、手首=wrist)
|
||||
{"shoulder", HumanoidSlot::LeftShoulder},
|
||||
{"clavicle", HumanoidSlot::LeftShoulder},
|
||||
{"肩", HumanoidSlot::LeftShoulder},
|
||||
{"arm", HumanoidSlot::LeftUpperArm},
|
||||
{"upperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"bicep", HumanoidSlot::LeftUpperArm},
|
||||
{"腕", HumanoidSlot::LeftUpperArm},
|
||||
{"lowerarm", HumanoidSlot::LeftLowerArm},
|
||||
{"forearm", HumanoidSlot::LeftLowerArm},
|
||||
{"elbow", HumanoidSlot::LeftLowerArm},
|
||||
{"ひじ", HumanoidSlot::LeftLowerArm},
|
||||
{"肘", HumanoidSlot::LeftLowerArm},
|
||||
{"hand", HumanoidSlot::LeftHand},
|
||||
{"手首", HumanoidSlot::LeftHand},
|
||||
// 腿段(MMD:足=thigh、ひざ=knee、足首=ankle)
|
||||
{"upperleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"upleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"thigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"足", HumanoidSlot::LeftUpperLeg},
|
||||
{"lowerleg", HumanoidSlot::LeftLowerLeg},
|
||||
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo:LeftLeg=小腿(大腿是 UpLeg)
|
||||
{"knee", HumanoidSlot::LeftLowerLeg},
|
||||
{"calf", HumanoidSlot::LeftLowerLeg},
|
||||
{"ひざ", HumanoidSlot::LeftLowerLeg},
|
||||
{"foot", HumanoidSlot::LeftFoot},
|
||||
{"ankle", HumanoidSlot::LeftFoot},
|
||||
{"足首", HumanoidSlot::LeftFoot},
|
||||
}};
|
||||
|
||||
// 左→右槽位偏移:臂/肩段(Shoulder..Hand)+4,腿段(UpperLeg..Foot)+3
|
||||
HumanoidSlot ToSide(HumanoidSlot leftSlot, Side side)
|
||||
// 窄 side 抽取:side 只出现在 Mixamo 前缀(LeftHand...)、
|
||||
// MMD 前缀(左手首)与 Blender 末尾后缀(..._L/.L)。
|
||||
// 不用任意位置 cut——Rigify "f_ring" 中间的 "_r" 会被误当
|
||||
// 右侧后缀删掉(body 变 "fing" → Ring 指骨全军覆没的历史
|
||||
// 陷阱),末尾匹配天然规避
|
||||
Side ExtractSideNarrow(std::string& body)
|
||||
{
|
||||
if (side != Side::Right) return leftSlot;
|
||||
const u32 s = static_cast<u32>(leftSlot);
|
||||
const u32 legStart = static_cast<u32>(HumanoidSlot::LeftUpperLeg);
|
||||
return static_cast<HumanoidSlot>(s + (s >= legStart ? 3 : 4));
|
||||
Side s = Side::None;
|
||||
if (body.rfind("left", 0) == 0) {
|
||||
body.erase(0, 4);
|
||||
s = Side::Left;
|
||||
} else if (body.rfind("right", 0) == 0) {
|
||||
body.erase(0, 5);
|
||||
s = Side::Right;
|
||||
} else if (body.rfind("左", 0) == 0) {
|
||||
body.erase(0, strlen("左")); // UTF-8 3 字节
|
||||
s = Side::Left;
|
||||
} else if (body.rfind("右", 0) == 0) {
|
||||
body.erase(0, strlen("右"));
|
||||
s = Side::Right;
|
||||
}
|
||||
const auto cutSuffix = [&](std::string_view token,
|
||||
Side which) {
|
||||
if (body.size() >= token.size() &&
|
||||
body.compare(body.size() - token.size(),
|
||||
token.size(), token) == 0) {
|
||||
body.erase(body.size() - token.size(), token.size());
|
||||
if (s == Side::None) s = which;
|
||||
}
|
||||
};
|
||||
cutSuffix(".l", Side::Left);
|
||||
cutSuffix(".r", Side::Right);
|
||||
cutSuffix("_l", Side::Left);
|
||||
cutSuffix("_r", Side::Right);
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
// 宽 side 抽取(fallback):任意位置删 token,兜底命名习惯
|
||||
// 不在首尾的资产。窄规则先行,本函数只在查表失败后启用
|
||||
Side ExtractSideWide(std::string& body)
|
||||
{
|
||||
Side s = Side::None;
|
||||
auto cut = [&](std::string_view token, Side which)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = body.find(token, pos)) != std::string::npos) {
|
||||
body.erase(pos, token.size());
|
||||
if (s == Side::None) s = which;
|
||||
}
|
||||
};
|
||||
cut("left", Side::Left);
|
||||
cut("right", Side::Right);
|
||||
cut("左", Side::Left);
|
||||
cut("右", Side::Right);
|
||||
cut(".l", Side::Left);
|
||||
cut(".r", Side::Right);
|
||||
cut("_l", Side::Left);
|
||||
cut("_r", Side::Right);
|
||||
return s;
|
||||
}
|
||||
|
||||
const char* HumanoidSlotName(HumanoidSlot slot)
|
||||
{
|
||||
static constexpr std::array<const char*, 20> kNames{
|
||||
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
|
||||
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
||||
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
|
||||
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
|
||||
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
||||
};
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
return s < kNames.size() ? kNames[s] : "?";
|
||||
}
|
||||
} // namespace mapper
|
||||
|
||||
// 名字归一化 + 方言查表识别。两轮:窄 side 抽取优先(避开
|
||||
// f_ring 类中间子串陷阱),查表失败再用宽抽取兜底
|
||||
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||
bool& ok)
|
||||
{
|
||||
side = Side::None;
|
||||
ok = false;
|
||||
std::string body = NormalizeLower(name);
|
||||
side = ExtractSide(body);
|
||||
body = StripSeparators(body);
|
||||
if (body.empty()) return HumanoidSlot::Hips;
|
||||
|
||||
for (const BodyRule& r : kBodyRules) {
|
||||
if (body == r.body) {
|
||||
const std::string lower = mapper::NormalizeLower(name);
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
std::string body = lower;
|
||||
const Side s = pass == 0
|
||||
? mapper::ExtractSideNarrow(body)
|
||||
: mapper::ExtractSideWide(body);
|
||||
body = mapper::StripSeparators(body);
|
||||
if (body.empty()) {
|
||||
return HumanoidSlot::Hips;
|
||||
}
|
||||
HumanoidSlot slot;
|
||||
if (dialects::Lookup(body, slot)) {
|
||||
side = s;
|
||||
ok = true;
|
||||
return ToSide(r.left, side);
|
||||
// 表存左槽名:右 side 经镜像换槽(主干槽镜像原样)
|
||||
return s == Side::Right ? MirrorSlot(slot) : slot;
|
||||
}
|
||||
}
|
||||
return HumanoidSlot::Hips;
|
||||
}
|
||||
|
||||
// 跨骨架自动映射:同名直映 + 52 槽一对一配对(指/趾骨同为主干
|
||||
// 槽位走统一通道,无独立特判)
|
||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
@@ -202,7 +504,7 @@ namespace Engine::Animation {
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 槽位匹配:两侧各自识别 (slot → nodeIdx),槽位一对一配对
|
||||
// 2) 槽位配对:两侧各自识别 (slot → nodeIdx),槽位一对一
|
||||
constexpr u32 kSlotCount = static_cast<u32>(HumanoidSlot::Count);
|
||||
auto slotOf = [](const std::vector<ModelNode>& nodes,
|
||||
std::array<i32, kSlotCount>& out)
|
||||
@@ -218,7 +520,7 @@ namespace Engine::Animation {
|
||||
}
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
if (out[s] < 0) {
|
||||
out[s] = static_cast<i32>(i); // 关键词先命中优先
|
||||
out[s] = static_cast<i32>(i); // 方言表先命中优先
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -243,4 +545,204 @@ namespace Engine::Animation {
|
||||
return pairs;
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// 行走基 / T-pose 估计(rest 姿态几何推导,与命名方言无关)
|
||||
// ==================================================================
|
||||
namespace {
|
||||
|
||||
// 槽位 → 节点下标(第一个命中;无则 -1)
|
||||
i32 FindSlotNode(const std::vector<ModelNode>& nodes,
|
||||
HumanoidSlot slot)
|
||||
{
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
Side side;
|
||||
bool ok = false;
|
||||
if (MatchHumanoidSlot(nodes[i].name, side, ok) == slot &&
|
||||
ok) {
|
||||
return static_cast<i32>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 脚趾节点(按 Toes 槽位 + 侧别匹配;无则 -1)。走槽位而非
|
||||
// 名字含 "toe"——否则 MMD「つま先」等 CJK 趾骨检测不到
|
||||
i32 FindToeNode(const std::vector<ModelNode>& nodes, Side want)
|
||||
{
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
Side side;
|
||||
bool ok = false;
|
||||
const HumanoidSlot slot =
|
||||
MatchHumanoidSlot(nodes[i].name, side, ok);
|
||||
if (!ok || SideOfSlot(slot) != want) {
|
||||
continue;
|
||||
}
|
||||
if (slot == HumanoidSlot::LeftToes ||
|
||||
slot == HumanoidSlot::RightToes) {
|
||||
return static_cast<i32>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// rest 姿态的行走基(right/up/fwd,右手系)。up = 头 - 双脚
|
||||
// 中点;right = 臂差(armR - armL)去 up 分量;fwd = up × right。
|
||||
// 脚趾朝向消歧:fwd 与脚尖反向时 right/fwd 绕 up 同翻 180°
|
||||
// (镜像/腿型特异防呆,同翻保持右手系)。槽位不足 → false
|
||||
bool HumanoidBasis(const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& globals,
|
||||
glm::vec3& right, glm::vec3& up, glm::vec3& fwd)
|
||||
{
|
||||
const i32 head = FindSlotNode(nodes, HumanoidSlot::Head);
|
||||
const i32 footL = FindSlotNode(nodes, HumanoidSlot::LeftFoot);
|
||||
const i32 footR = FindSlotNode(nodes, HumanoidSlot::RightFoot);
|
||||
const i32 armL = FindSlotNode(nodes, HumanoidSlot::LeftUpperArm);
|
||||
const i32 armR = FindSlotNode(nodes, HumanoidSlot::RightUpperArm);
|
||||
if (head < 0 || footL < 0 || footR < 0 || armL < 0 || armR < 0) {
|
||||
return false;
|
||||
}
|
||||
const auto pos = [&](i32 i) {
|
||||
return glm::vec3(globals[static_cast<size_t>(i)][3]);
|
||||
};
|
||||
const glm::vec3 midFoot = 0.5f * (pos(footL) + pos(footR));
|
||||
const glm::vec3 lat = pos(armR) - pos(armL);
|
||||
up = pos(head) - midFoot;
|
||||
if (glm::length(up) < 1e-6f || glm::length(lat) < 1e-6f) {
|
||||
return false;
|
||||
}
|
||||
up = glm::normalize(up);
|
||||
right = lat - up * glm::dot(lat, up); // 去除 up 分量
|
||||
if (glm::length(right) < 1e-6f) {
|
||||
return false;
|
||||
}
|
||||
right = glm::normalize(right);
|
||||
// fwd = up × right(右手系:面朝 fwd 时 right 恒在体右侧)。
|
||||
// 原 right × up 恒指向背后,正常资产(X Bot/Mixamo:脚尖
|
||||
// +Z、右臂 −X)必触发下方脚趾消歧的 180° 翻转——翻转虽把
|
||||
// fwd 纠正,却把 right 连带翻成解剖学左,行走基被镜像污染;
|
||||
// 与未翻转的目标骨架(Lumine 等)对齐后,臂部增量被解释成
|
||||
// 朝身体内侧——两臂交缠的根因。腿/躯干增量前后对称,镜像
|
||||
// 下不显形,故此前未暴露
|
||||
fwd = glm::cross(up, right);
|
||||
const i32 toeL = FindToeNode(nodes, Side::Left);
|
||||
const i32 toeR = FindToeNode(nodes, Side::Right);
|
||||
if (toeL >= 0 && toeR >= 0) {
|
||||
const glm::vec3 toeDir =
|
||||
0.5f * (pos(toeL) + pos(toeR)) - midFoot;
|
||||
if (glm::dot(fwd, toeDir) < 0.0f) {
|
||||
right = -right;
|
||||
fwd = -fwd;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
glm::quat EstimateHumanoidRootAlignment(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
{
|
||||
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
// rest 世界变换(node 表父先子后,直接链乘)
|
||||
auto restGlobals = [](const std::vector<ModelNode>& nodes) {
|
||||
std::vector<glm::mat4> g(nodes.size());
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
const i32 p = nodes[i].parent;
|
||||
g[i] = (p >= 0 ? g[static_cast<size_t>(p)]
|
||||
: glm::mat4(1.0f)) *
|
||||
nodes[i].localTransform;
|
||||
}
|
||||
return g;
|
||||
};
|
||||
|
||||
const auto srcG = restGlobals(srcNodes);
|
||||
const auto dstG = restGlobals(dstNodes);
|
||||
|
||||
glm::vec3 sr, su, sf, dr, du, df;
|
||||
if (!HumanoidBasis(srcNodes, srcG, sr, su, sf) ||
|
||||
!HumanoidBasis(dstNodes, dstG, dr, du, df)) {
|
||||
return kIdentity;
|
||||
}
|
||||
// 列 = [right | up | fwd];A = Bd × Bsᵀ(src 世界 → dst 世界)
|
||||
const glm::mat3 Bs(sr, su, sf);
|
||||
const glm::mat3 Bd(dr, du, df);
|
||||
return glm::quat_cast(Bd * glm::transpose(Bs));
|
||||
}
|
||||
|
||||
std::vector<glm::quat> EstimateHumanoidTPose(
|
||||
const std::vector<ModelNode>& nodes,
|
||||
const std::vector<glm::mat4>& restGlobals)
|
||||
{
|
||||
// T-pose 参考姿势(每节点世界旋转):
|
||||
// 人形动画资产(Mixamo 等)的摆动量以 T-pose 为零位,目标骨架
|
||||
// rest 是 A-pose 时直接搬运"旋转变化"会过摆(臂从已下垂 45°
|
||||
// 再摆 T-pose 基准的全幅)——手越过中线交叉。此处为目标估计
|
||||
// T-pose 参考:臂链(upper_arm 子树)绕肩刚体摆到水平侧向,
|
||||
// 指骨按 seg1 方向伸直(kwy 等 rest 半握骨架与 Mixamo 等
|
||||
// rest 伸直骨架的手指零位不同源,见下)。其余骨取 rest。
|
||||
// rest 已是 T-pose 的资产(臂≈水平、指伸直)摆动量≈0,
|
||||
// 自动退化。
|
||||
auto rotOf = [](const glm::mat4& m) {
|
||||
return glm::quat_cast(glm::mat3(
|
||||
glm::normalize(glm::vec3(m[0])),
|
||||
glm::normalize(glm::vec3(m[1])),
|
||||
glm::normalize(glm::vec3(m[2]))));
|
||||
};
|
||||
std::vector<glm::quat> g(nodes.size(), glm::quat(1.0f, 0.0f, 0.0f,
|
||||
0.0f));
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
g[i] = rotOf(restGlobals[i]);
|
||||
}
|
||||
glm::vec3 right, up, fwd;
|
||||
if (!HumanoidBasis(nodes, restGlobals, right, up, fwd)) {
|
||||
return g; // 行走基不可用 → 全 rest(不补偿)
|
||||
}
|
||||
const auto posOf = [&](i32 i) {
|
||||
return glm::vec3(restGlobals[static_cast<size_t>(i)][3]);
|
||||
};
|
||||
for (const Side side : {Side::Left, Side::Right}) {
|
||||
const HumanoidSlot uaSlot = side == Side::Left
|
||||
? HumanoidSlot::LeftUpperArm : HumanoidSlot::RightUpperArm;
|
||||
const HumanoidSlot faSlot = side == Side::Left
|
||||
? HumanoidSlot::LeftLowerArm : HumanoidSlot::RightLowerArm;
|
||||
const i32 ua = FindSlotNode(nodes, uaSlot);
|
||||
const i32 fa = FindSlotNode(nodes, faSlot);
|
||||
if (ua < 0 || fa < 0) {
|
||||
continue;
|
||||
}
|
||||
const glm::vec3 v =
|
||||
glm::normalize(posOf(fa) - posOf(ua)); // 上臂 rest 指向
|
||||
glm::vec3 vlat = v - up * glm::dot(v, up) -
|
||||
fwd * glm::dot(v, fwd);
|
||||
if (glm::length(vlat) < 1e-6f) {
|
||||
continue; // 臂纯沿 up(退化)→ 不摆
|
||||
}
|
||||
vlat = glm::normalize(vlat);
|
||||
const float d = glm::clamp(glm::dot(v, vlat), -1.0f, 1.0f);
|
||||
glm::vec3 axis = glm::cross(v, vlat);
|
||||
const glm::quat swing =
|
||||
glm::length(axis) < 1e-6f
|
||||
? glm::quat(1.0f, 0.0f, 0.0f, 0.0f)
|
||||
: glm::angleAxis(glm::acos(d),
|
||||
glm::normalize(axis));
|
||||
// upper_arm 子树刚体摆动(保持链内相对姿态;含自身)
|
||||
for (size_t j = 0; j < nodes.size(); ++j) {
|
||||
for (i32 p = static_cast<i32>(j); p >= 0;
|
||||
p = nodes[static_cast<size_t>(p)].parent) {
|
||||
if (p == ua) {
|
||||
g[j] = swing * g[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 手指不参与 T-pose 零位:指根扇形编码在 rest 平移里(旋转
|
||||
// 搬运无法复刻扇形),绝对复刻语义会让 dst 指根方向偏转 +
|
||||
// 跨手型穿插。手指走 rest 零位局部增量通道(Unity muscle
|
||||
// 语义,见 AnimationAttach),各资产保留自然手型,此处取 rest。
|
||||
return g;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <Engine/Core/Window.h>
|
||||
#include <Engine/Core/Input.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/LogRing.h>
|
||||
#include <Engine/Core/Stats.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Physics/PhysicsWorld.h>
|
||||
#include <Engine/Scripting/ScriptRuntime.h>
|
||||
@@ -112,6 +114,12 @@ namespace Engine {
|
||||
|
||||
void Application::Run()
|
||||
{
|
||||
// Log Panel 开闸:到此为止之前的启动日志(窗口/Vulkan/UBO/
|
||||
// IBL/网格构建)只落终端与文件——Unity Console 语义,面板只
|
||||
// 收运行期日志(场景/物理/脚本),启动噪音不进面板
|
||||
Engine::Debug::LogRing().SetEnabled(true);
|
||||
ENGINE_DEBUG("[APP] 主循环启动(Log Panel 开启;此行为 DEBUG 级,"
|
||||
"勾选 Trace 过滤可见青色 DEBUG 徽标)");
|
||||
ENGINE_INFO("Application starting");
|
||||
|
||||
m_Running = true;
|
||||
@@ -133,6 +141,8 @@ namespace Engine {
|
||||
while (m_Running) {
|
||||
const auto frameStart = std::chrono::steady_clock::now();
|
||||
|
||||
STATS.BeginFrame(); // 帧内资源分配清零(面板帧流失真修正)
|
||||
|
||||
m_Window->PollEvents();
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Created by atdunbg on 2026/9/20.
|
||||
//
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/LogRing.h>
|
||||
|
||||
#include "spdlog/sinks/basic_file_sink.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
@@ -26,10 +27,16 @@ namespace Engine::Logger
|
||||
auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("HelloVulkan.log", true);
|
||||
file->set_pattern("[%Y-%m-%d %T.%e] [%l] [%t] %v");
|
||||
|
||||
// 环形缓冲(编辑器 Log Panel 数据源):时间 + 消息——级别词
|
||||
// 不进文本,由面板按 msg.level 画彩色徽标列(文本里重复一遍
|
||||
// 级别既冗余又抢徽标的视觉作用)
|
||||
auto ring = std::make_shared<Engine::Debug::RingSink>();
|
||||
ring->set_pattern("[%H:%M:%S.%e] %v");
|
||||
|
||||
// 多sink 组合默认 logger
|
||||
|
||||
// auto logger = std::make_shared<spdlog::logger>("hello", spdlog::sinks_init_list{console, file});
|
||||
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file});
|
||||
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file, ring});
|
||||
logger->set_level(spdlog::level::trace);
|
||||
logger->flush_on(spdlog::level::warn); // warn 及以上立即 flush,避免崩溃丢日志
|
||||
|
||||
|
||||
+16
-196
@@ -4,10 +4,6 @@
|
||||
#include <Engine/Core/Stats.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
Stats& Stats::Get()
|
||||
@@ -125,200 +121,24 @@ namespace Engine
|
||||
m_TotalDeallocation += bytes;
|
||||
}
|
||||
|
||||
// ============ 进程内存采样 ============
|
||||
// ============ 只读快照 ============
|
||||
|
||||
Stats::ProcessMemory Stats::ReadProcessMemory() const
|
||||
{
|
||||
ProcessMemory result;
|
||||
|
||||
// ---- 1. 从 /proc/self/status 读 VmRSS 和 VmSize ----
|
||||
// 这是内核报告的权威 RSS。smaps_rollup 会漏掉某些 NVIDIA 显存映射,
|
||||
// 但 status 的 VmRSS 包含所有 VMA。
|
||||
{
|
||||
std::ifstream status("/proc/self/status");
|
||||
std::string line;
|
||||
while (std::getline(status, line))
|
||||
{
|
||||
if (line.rfind("VmRSS:", 0) == 0)
|
||||
{
|
||||
std::istringstream iss(line.substr(6));
|
||||
u64 kb = 0;
|
||||
iss >> kb;
|
||||
result.Rss = kb * 1024;
|
||||
}
|
||||
else if (line.rfind("VmSize:", 0) == 0)
|
||||
{
|
||||
std::istringstream iss(line.substr(7));
|
||||
u64 kb = 0;
|
||||
iss >> kb;
|
||||
result.VmSize = kb * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 2. 从 /proc/self/smaps_rollup 读 PSS 和分项 ----
|
||||
// 注意:Rss 不从这里读,用上面的。
|
||||
{
|
||||
std::ifstream smaps("/proc/self/smaps_rollup");
|
||||
std::string line;
|
||||
while (std::getline(smaps, line))
|
||||
{
|
||||
std::istringstream iss(line);
|
||||
std::string key;
|
||||
u64 value = 0;
|
||||
std::string unit;
|
||||
|
||||
iss >> key;
|
||||
if (key.empty()) continue;
|
||||
if (key.back() == ':') key.pop_back();
|
||||
|
||||
if (key == "Pss")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.Pss = value * 1024;
|
||||
}
|
||||
else if (key == "Shared_Clean")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.SharedClean = value * 1024;
|
||||
}
|
||||
else if (key == "Private_Dirty")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.PrivateDirty = value * 1024;
|
||||
}
|
||||
else if (key == "Private_Clean")
|
||||
{
|
||||
iss >> value >> unit;
|
||||
result.PrivateClean = value * 1024;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Stats::SampleProcessMemory()
|
||||
Stats::Snapshot Stats::TakeSnapshot() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
m_ProcessMem = ReadProcessMemory();
|
||||
}
|
||||
|
||||
// ============ 打印 ============
|
||||
|
||||
static double MB(const u64 bytes) { return bytes / (1024.0 * 1024.0); }
|
||||
|
||||
void Stats::Print() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
|
||||
const auto proc = ReadProcessMemory();
|
||||
|
||||
ENGINE_INFO("========== Resource Stats ==========");
|
||||
ENGINE_INFO("Textures: {:>4} ({:.2f} MB)", m_TextureCount, MB(m_TextureBytes));
|
||||
ENGINE_INFO("Meshes: {:>4} ({:.2f} MB)", m_MeshCount, MB(m_MeshBytes));
|
||||
ENGINE_INFO("Buffers: {:>4} ({:.2f} MB)", m_BufferCount, MB(m_BufferBytes));
|
||||
ENGINE_INFO("--- GPU MemoryAllocator ---");
|
||||
ENGINE_INFO("Allocated: {:.2f} MB", MB(m_MemoryBlockBytes));
|
||||
ENGINE_INFO("Used: {:.2f} MB", MB(m_MemoryUsedBytes));
|
||||
ENGINE_INFO("Wasted: {:.2f} MB", MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
ENGINE_INFO("--- Process Memory ---");
|
||||
ENGINE_INFO("VmSize: {:.2f} MB", MB(proc.VmSize));
|
||||
ENGINE_INFO("RSS (resident): {:.2f} MB", MB(proc.Rss));
|
||||
ENGINE_INFO("PSS: {:.2f} MB", MB(proc.Pss));
|
||||
ENGINE_INFO(" Shared Clean: {:.2f} MB", MB(proc.SharedClean));
|
||||
ENGINE_INFO(" Private Dirty: {:.2f} MB", MB(proc.PrivateDirty));
|
||||
ENGINE_INFO(" Private Clean: {:.2f} MB", MB(proc.PrivateClean));
|
||||
ENGINE_INFO("====================================");
|
||||
}
|
||||
|
||||
void Stats::PrintDetailed() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
|
||||
auto proc = ReadProcessMemory();
|
||||
|
||||
ENGINE_INFO("==================== Detailed Memory Stats ====================");
|
||||
|
||||
ENGINE_INFO(" Resources");
|
||||
ENGINE_INFO(" Textures: {:>6} {:>10.2f} MB", m_TextureCount, MB(m_TextureBytes));
|
||||
ENGINE_INFO(" Meshes: {:>6} {:>10.2f} MB", m_MeshCount, MB(m_MeshBytes));
|
||||
ENGINE_INFO(" Buffers: {:>6} {:>10.2f} MB", m_BufferCount, MB(m_BufferBytes));
|
||||
ENGINE_INFO(" Total: {:>10.2f} MB",
|
||||
MB(m_TextureBytes + m_MeshBytes + m_BufferBytes));
|
||||
|
||||
ENGINE_INFO(" GPU MemoryAllocator");
|
||||
ENGINE_INFO(" Allocated: {:>10.2f} MB", MB(m_MemoryBlockBytes));
|
||||
ENGINE_INFO(" Used: {:>10.2f} MB", MB(m_MemoryUsedBytes));
|
||||
ENGINE_INFO(" Wasted: {:>10.2f} MB",
|
||||
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
|
||||
ENGINE_INFO(" Allocation Churn");
|
||||
ENGINE_INFO(" This frame: +{:.2f} MB / -{:.2f} MB",
|
||||
MB(m_FrameAllocation), MB(m_FrameDeallocation));
|
||||
ENGINE_INFO(" Total: +{:.2f} MB / -{:.2f} MB",
|
||||
MB(m_TotalAllocation), MB(m_TotalDeallocation));
|
||||
|
||||
ENGINE_INFO(" Process Memory (/proc/self/status + smaps_rollup)");
|
||||
ENGINE_INFO(" VmSize: {:>10.2f} MB", MB(proc.VmSize));
|
||||
ENGINE_INFO(" RSS: {:>10.2f} MB", MB(proc.Rss));
|
||||
ENGINE_INFO(" PSS: {:>10.2f} MB", MB(proc.Pss));
|
||||
ENGINE_INFO(" Shared Clean: {:>10.2f} MB", MB(proc.SharedClean));
|
||||
ENGINE_INFO(" Private Dirty: {:>10.2f} MB", MB(proc.PrivateDirty));
|
||||
ENGINE_INFO(" Private Clean: {:>10.2f} MB", MB(proc.PrivateClean));
|
||||
|
||||
ENGINE_INFO(" Unaccounted (RSS - GPU Blocks)");
|
||||
u64 engineTotal = m_MemoryBlockBytes;
|
||||
if (proc.Rss > engineTotal)
|
||||
{
|
||||
ENGINE_INFO(" {:.2f} MB (Vulkan driver, libs, stack, heap, ...)",
|
||||
MB(proc.Rss - engineTotal));
|
||||
}
|
||||
else
|
||||
{
|
||||
ENGINE_INFO(" (GPU blocks are the bulk of RSS)");
|
||||
}
|
||||
|
||||
ENGINE_INFO("===============================================================");
|
||||
}
|
||||
|
||||
void Stats::PrintComparison() const
|
||||
{
|
||||
std::lock_guard lock(m_Mutex);
|
||||
|
||||
auto proc = ReadProcessMemory();
|
||||
|
||||
u64 resourceTotal = m_TextureBytes + m_MeshBytes + m_BufferBytes;
|
||||
|
||||
ENGINE_INFO("=============== Memory Comparison ===============");
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO("[Engine 自己统计]");
|
||||
ENGINE_INFO(" Textures {:>10.2f} MB", MB(m_TextureBytes));
|
||||
ENGINE_INFO(" Meshes {:>10.2f} MB", MB(m_MeshBytes));
|
||||
ENGINE_INFO(" Buffers {:>10.2f} MB", MB(m_BufferBytes));
|
||||
ENGINE_INFO(" ─────────────────────────────────────");
|
||||
ENGINE_INFO(" 资源合计 {:>10.2f} MB", MB(resourceTotal));
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO(" Allocator Used {:>10.2f} MB", MB(m_MemoryUsedBytes));
|
||||
ENGINE_INFO(" Allocator Allocated {:>10.2f} MB", MB(m_MemoryBlockBytes));
|
||||
ENGINE_INFO(" Allocator Wasted {:>10.2f} MB",
|
||||
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO("[系统 /proc 读出的]");
|
||||
ENGINE_INFO(" VmRSS {:>10.2f} MB", MB(proc.Rss));
|
||||
ENGINE_INFO(" VmSize {:>10.2f} MB", MB(proc.VmSize));
|
||||
ENGINE_INFO(" PSS {:>10.2f} MB", MB(proc.Pss));
|
||||
ENGINE_INFO(" Shared Clean {:>10.2f} MB", MB(proc.SharedClean));
|
||||
ENGINE_INFO(" Private Dirty {:>10.2f} MB", MB(proc.PrivateDirty));
|
||||
ENGINE_INFO(" Private Clean {:>10.2f} MB", MB(proc.PrivateClean));
|
||||
ENGINE_INFO("");
|
||||
ENGINE_INFO("[差值]");
|
||||
ENGINE_INFO(" RSS - Allocator {:>10.2f} MB (Vulkan 驱动 + libs + 栈堆)",
|
||||
MB(proc.Rss > m_MemoryBlockBytes ? proc.Rss - m_MemoryBlockBytes : 0));
|
||||
ENGINE_INFO(" RSS - 资源合计 {:>10.2f} MB (含分配器碎片)",
|
||||
MB(proc.Rss > resourceTotal ? proc.Rss - resourceTotal : 0));
|
||||
ENGINE_INFO(" Allocated - Used {:>10.2f} MB (分配器空闲块)",
|
||||
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
|
||||
ENGINE_INFO("================================================");
|
||||
Snapshot s;
|
||||
s.textureCount = m_TextureCount;
|
||||
s.textureBytes = m_TextureBytes;
|
||||
s.meshCount = m_MeshCount;
|
||||
s.meshBytes = m_MeshBytes;
|
||||
s.bufferCount = m_BufferCount;
|
||||
s.bufferBytes = m_BufferBytes;
|
||||
s.memoryBlockBytes = m_MemoryBlockBytes;
|
||||
s.memoryUsedBytes = m_MemoryUsedBytes;
|
||||
s.frameAllocation = m_FrameAllocation;
|
||||
s.frameDeallocation = m_FrameDeallocation;
|
||||
s.totalAllocation = m_TotalAllocation;
|
||||
s.totalDeallocation = m_TotalDeallocation;
|
||||
return s;
|
||||
}
|
||||
} // namespace Engine
|
||||
|
||||
@@ -23,10 +23,14 @@ namespace Engine {
|
||||
constexpr i32 kKeyUp = 265, kKeyDown = 264,
|
||||
kKeyLeft = 263, kKeyRight = 262;
|
||||
constexpr i32 kKeySpace = 32;
|
||||
constexpr i32 kKeyLShift = 340; // GLFW_KEY_LEFT_SHIFT
|
||||
// 手柄标准布局
|
||||
constexpr i32 kPadBtnA = 0; // GLFW_GAMEPAD_BUTTON_A
|
||||
constexpr i32 kPadBtnL3 = 9; // GLFW_GAMEPAD_BUTTON_LEFT_THUMB
|
||||
constexpr i32 kPadAxisLeftX = 0; // GLFW_GAMEPAD_AXIS_LEFT_X
|
||||
constexpr i32 kPadAxisLeftY = 1; // GLFW_GAMEPAD_AXIS_LEFT_Y
|
||||
constexpr i32 kPadAxisRightX = 2; // GLFW_GAMEPAD_AXIS_RIGHT_X
|
||||
constexpr i32 kPadAxisRightY = 3; // GLFW_GAMEPAD_AXIS_RIGHT_Y
|
||||
|
||||
constexpr f32 kRadialDeadZone = 0.15f;
|
||||
|
||||
@@ -139,6 +143,19 @@ namespace Engine {
|
||||
const size_t jump = DefineAction("Jump", ActionKind::Button);
|
||||
Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX});
|
||||
Bind(jump, {SourceKind::GamepadButton, kPadBtnA, 1.0f, kX});
|
||||
|
||||
// Run:Shift + 手柄 L3(按下左摇杆 = 冲刺,Xbox 惯例)。
|
||||
// 注意不能用"摇杆推满"判跑——键盘 WASD 经死区/钳制后幅度
|
||||
// 恒为 1.0,会把走路误判成跑(真实踩坑:WASD 一动就 run)
|
||||
const size_t run = DefineAction("Run", ActionKind::Button);
|
||||
Bind(run, {SourceKind::Key, kKeyLShift, 1.0f, kX});
|
||||
Bind(run, {SourceKind::GamepadButton, kPadBtnL3, 1.0f, kX});
|
||||
|
||||
// Look:右摇杆(相机环绕)。鼠标是相对增量不走动作层,
|
||||
// 相机脚本里 GetMouseDelta 叠加
|
||||
const size_t look = DefineAction("Look", ActionKind::Axis2D);
|
||||
Bind(look, {SourceKind::GamepadAxis, kPadAxisRightX, +1.0f, kX});
|
||||
Bind(look, {SourceKind::GamepadAxis, kPadAxisRightY, +1.0f, kY});
|
||||
}
|
||||
|
||||
// ---- 运行期 ----
|
||||
|
||||
@@ -25,8 +25,10 @@ namespace Engine {
|
||||
s.mShape = new JPH::CapsuleShape(
|
||||
std::max(params.radius, 0.01f),
|
||||
std::max(params.halfHeight, 0.0f));
|
||||
s.mShapeOffset = JPH::Vec3(0.0f,
|
||||
params.halfHeight + params.radius, 0.0f);
|
||||
s.mShapeOffset = JPH::Vec3(
|
||||
params.shapeOffset.x,
|
||||
params.shapeOffset.y + params.halfHeight + params.radius,
|
||||
params.shapeOffset.z);
|
||||
|
||||
m_Character = new JPH::CharacterVirtual(
|
||||
&s, JPH::RVec3(position.x, position.y, position.z),
|
||||
@@ -37,6 +39,20 @@ namespace Engine {
|
||||
m_Ext.mWalkStairsStepUp = JPH::Vec3(0.0f, std::max(params.stepHeight, 0.0f), 0.0f);
|
||||
}
|
||||
|
||||
CharacterController::~CharacterController() {
|
||||
// 先摘除再释放:列表持裸 CharacterVirtual*,否则悬垂
|
||||
if (m_VsChar != nullptr) {
|
||||
m_VsChar->Remove(m_Character.GetPtr());
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterController::SetCharacterCollision(
|
||||
JPH::CharacterVsCharacterCollisionSimple &list) {
|
||||
m_VsChar = &list;
|
||||
m_Character->SetCharacterVsCharacterCollision(&list);
|
||||
list.Add(m_Character.GetPtr());
|
||||
}
|
||||
|
||||
void CharacterController::Update(const f32 dt, const JPH::Vec3 &gravity,
|
||||
JPH::TempAllocator &allocator) {
|
||||
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
|
||||
@@ -50,6 +66,10 @@ namespace Engine {
|
||||
m_Character->SetLinearVelocity(v + gravity * dt);
|
||||
}
|
||||
|
||||
// 扫掠前速度快照:撞墙判定用。ExtendedUpdate 会抵消朝向陡面/
|
||||
// 墙面的速度分量,扫掠后再读就归零了(见头文件说明)
|
||||
m_SweepVelocity = m_Character->GetLinearVelocity();
|
||||
|
||||
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
|
||||
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
|
||||
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
|
||||
@@ -78,6 +98,16 @@ namespace Engine {
|
||||
return m_Character->IsSupported();
|
||||
}
|
||||
|
||||
const JPH::CharacterVirtual::ContactList&
|
||||
CharacterController::GetActiveContacts() const {
|
||||
return m_Character->GetActiveContacts();
|
||||
}
|
||||
|
||||
glm::vec3 CharacterController::GetSweepVelocity() const {
|
||||
return {m_SweepVelocity.GetX(), m_SweepVelocity.GetY(),
|
||||
m_SweepVelocity.GetZ()};
|
||||
}
|
||||
|
||||
glm::vec3 CharacterController::GetPosition() const {
|
||||
const JPH::RVec3 p = m_Character->GetPosition();
|
||||
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
|
||||
|
||||
@@ -176,6 +176,10 @@ namespace Engine {
|
||||
// 直接成员会先于 call_once 构造 → 函数指针未注册 → SIGSEGV at 0x0
|
||||
m_DebugDraw = CreateScope<PhysicsDebugDraw>();
|
||||
|
||||
// 角色间碰撞互查表(内部 Array 走 JPH 分配器,须在 call_once 之后)
|
||||
m_VsCharacterCollision =
|
||||
CreateScope<JPH::CharacterVsCharacterCollisionSimple>();
|
||||
|
||||
m_TempAllocator = CreateScope<JPH::TempAllocatorImpl>(16 * 1024 * 1024);
|
||||
const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1);
|
||||
m_JobSystem = CreateScope<JPH::JobSystemThreadPool>(
|
||||
@@ -208,6 +212,7 @@ namespace Engine {
|
||||
// 释放烘焙几何——必须先于 UnregisterTypes
|
||||
m_DebugDraw.reset();
|
||||
m_Bodies.clear();
|
||||
m_Characters.clear(); // 角色析构回 m_VsCharacterCollision 摘除,先于 Jolt 反注册
|
||||
m_ContactListener.reset(); // 监听器先于 system 析构(持 m_Owner 引用)
|
||||
m_System.reset();
|
||||
// 单世界假设:进程退出时统一反注册(多世界时改为引用计数)
|
||||
@@ -695,20 +700,24 @@ namespace Engine {
|
||||
p.mass = cc.mass;
|
||||
p.maxSlopeAngle = cc.maxSlopeAngle;
|
||||
p.stepHeight = cc.stepHeight;
|
||||
p.shapeOffset = cc.shapeOffset;
|
||||
|
||||
auto found = m_Characters.find(bits);
|
||||
if (found == m_Characters.end()) {
|
||||
CharacterEntry entry;
|
||||
entry.cc = std::make_unique<CharacterController>(
|
||||
*m_System, p, tr.GetWorldPosition());
|
||||
entry.cc->SetCharacterCollision(*m_VsCharacterCollision);
|
||||
entry.lastPos = tr.GetWorldPosition();
|
||||
m_Characters.emplace(bits, std::move(entry));
|
||||
continue;
|
||||
}
|
||||
if (found->second.cc->GetParams() != p) {
|
||||
// 参数变更 → 重建(位置沿用当前 Transform)
|
||||
// 参数变更 → 重建(位置沿用当前 Transform);旧实例析构
|
||||
// 自动摘除旧条目,新实例重新登记
|
||||
found->second.cc = std::make_unique<CharacterController>(
|
||||
*m_System, p, tr.GetWorldPosition());
|
||||
found->second.cc->SetCharacterCollision(*m_VsCharacterCollision);
|
||||
found->second.lastPos = tr.GetWorldPosition();
|
||||
continue;
|
||||
}
|
||||
@@ -742,6 +751,42 @@ namespace Engine {
|
||||
static_cast<entt::entity>(bits), p,
|
||||
tr->GetWorldRotationQuat(), tr->GetWorldScale());
|
||||
entry.lastPos = p;
|
||||
|
||||
// ---- 角色碰撞事件合成(Enter/Exit)----
|
||||
// CharacterVirtual 非刚体,Jolt 的 ContactListener 不会为角色
|
||||
// 回调——脚本因此收不到"玩家撞墙"。这里读角色本步活跃接触,
|
||||
// 与上一步集合对比出边沿,合成与刚体同款的 CollisionEvent
|
||||
// (a = 角色实体,b = 被撞实体)。只处理刚体接触(mBodyB 有效):
|
||||
// 角色↔角色由 CharacterVsCharacterCollision 的物理互查处理,
|
||||
// 不发脚本事件(对方是角色时 mBodyB 无效,天然跳过)
|
||||
std::unordered_set<u32> now;
|
||||
now.reserve(entry.contacts.size() + 4);
|
||||
for (const JPH::CharacterContact& c :
|
||||
entry.cc->GetActiveContacts()) {
|
||||
if (c.mBodyB.IsInvalid()) continue; // 对方非刚体(角色)
|
||||
if (c.mWasDiscarded) continue; // 被丢弃/传感器
|
||||
if (c.mDistance > 0.0f) continue; // >0 = 预测接触,未真相交
|
||||
u32 other = 0;
|
||||
if (!TryBitsByBodyId(c.mBodyB, other)) continue;
|
||||
now.insert(other);
|
||||
if (entry.contacts.count(other) == 0) {
|
||||
const glm::vec3 pt(
|
||||
static_cast<f32>(c.mPosition.GetX()),
|
||||
static_cast<f32>(c.mPosition.GetY()),
|
||||
static_cast<f32>(c.mPosition.GetZ()));
|
||||
const glm::vec3 n(c.mContactNormal.GetX(),
|
||||
c.mContactNormal.GetY(),
|
||||
c.mContactNormal.GetZ());
|
||||
QueueEvent({bits, other, CollisionEvent::Kind::Added, pt, n});
|
||||
}
|
||||
}
|
||||
for (const u32 prev : entry.contacts) {
|
||||
if (now.count(prev) == 0) {
|
||||
QueueEvent(
|
||||
{bits, prev, CollisionEvent::Kind::Removed, {}, {}});
|
||||
}
|
||||
}
|
||||
entry.contacts = std::move(now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1124,6 +1169,14 @@ namespace Engine {
|
||||
: glm::vec3{0.0f};
|
||||
}
|
||||
|
||||
glm::vec3
|
||||
PhysicsWorld::GetCharacterImpactVelocity(const u32 entityBits) const {
|
||||
const auto found = m_Characters.find(entityBits);
|
||||
return found != m_Characters.end()
|
||||
? found->second.cc->GetSweepVelocity()
|
||||
: glm::vec3{0.0f};
|
||||
}
|
||||
|
||||
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
|
||||
|
||||
void PhysicsWorld::Writeback() {
|
||||
|
||||
@@ -88,10 +88,17 @@ namespace Engine
|
||||
",e:" + std::to_string(params.emissiveFactor.x) +
|
||||
"," + std::to_string(params.emissiveFactor.y) +
|
||||
"," + std::to_string(params.emissiveFactor.z);
|
||||
// per-slot UV 变换 + unlit 入键:同一张贴图配不同变换是不同材质
|
||||
//(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质以不同
|
||||
// 变换共享,键漏了会串成同一份)
|
||||
key += ",m:" + std::to_string(params.misc.x);
|
||||
// per-slot UV 变换 + unlit + UV 镜像入键:同一张贴图配不同变换是
|
||||
// 不同材质(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质
|
||||
// 以不同变换共享,键漏了会串成同一份)
|
||||
key += ",m:" + std::to_string(params.misc.x) +
|
||||
"," + std::to_string(params.misc.y) +
|
||||
"," + std::to_string(params.misc.z);
|
||||
// alpha 叠加遮罩入键:同贴图不同叠加色是不同材质
|
||||
key += ",o:" + std::to_string(params.alphaOverlay.x) +
|
||||
"," + std::to_string(params.alphaOverlay.y) +
|
||||
"," + std::to_string(params.alphaOverlay.z) +
|
||||
"," + std::to_string(params.alphaOverlay.w);
|
||||
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||
key += ",u" + std::to_string(slot) + ":" +
|
||||
std::to_string(params.uvA[slot].x) + "," +
|
||||
|
||||
@@ -93,6 +93,8 @@ namespace Engine
|
||||
EmitVec4(out, data.params.emissiveFactor);
|
||||
out << YAML::Key << "Misc" << YAML::Value;
|
||||
EmitVec4(out, data.params.misc);
|
||||
out << YAML::Key << "AlphaOverlay" << YAML::Value;
|
||||
EmitVec4(out, data.params.alphaOverlay);
|
||||
// per-slot UV 变换(KHR_texture_transform):6 槽各 A(4 分量) + T
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out << YAML::Key << ("UvA" + std::to_string(slot)) << YAML::Value;
|
||||
@@ -156,6 +158,8 @@ namespace Engine
|
||||
m["EmissiveFactor"], MaterialParams{}.emissiveFactor);
|
||||
out.params.misc = ReadVec4(
|
||||
m["Misc"], MaterialParams{}.misc);
|
||||
out.params.alphaOverlay = ReadVec4(
|
||||
m["AlphaOverlay"], MaterialParams{}.alphaOverlay);
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out.params.uvA[slot] = ReadVec4(
|
||||
m["UvA" + std::to_string(slot)],
|
||||
|
||||
+912
-70
File diff suppressed because it is too large
Load Diff
@@ -529,6 +529,10 @@ namespace Engine::Serialization {
|
||||
<< cc->stepHeight;
|
||||
out << YAML::Key << "GravityScale" << YAML::Value
|
||||
<< cc->gravityScale;
|
||||
out << YAML::Key << "ShapeOffset" << YAML::Value
|
||||
<< YAML::Flow << YAML::BeginSeq << cc->shapeOffset.x
|
||||
<< cc->shapeOffset.y << cc->shapeOffset.z
|
||||
<< YAML::EndSeq;
|
||||
out << YAML::Key << "Active" << YAML::Value << cc->active;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
@@ -681,6 +685,32 @@ namespace Engine::Serialization {
|
||||
it.as<std::string>(""));
|
||||
}
|
||||
}
|
||||
// 外部动画并入(幂等重放):动画 clip 本体不落盘,只在
|
||||
// 此按 Attached 列表把动画容器并入共享 Model 资产。过去
|
||||
// 仅由 Inspector 选中实体时重放 → 不点击实体/开局直跑
|
||||
// 就永远不加载。放在共享的 ApplyComponents 里,场景加载
|
||||
// 与 Prefab 实例化两条路径同时覆盖。
|
||||
// 时序保证:MeshRenderer 分支(上方)先于本分支解析,
|
||||
// 故此处 mr.model 已 resolve 就绪
|
||||
if (!a.attachedAnimPaths.empty() &&
|
||||
handle.HasComponent<ECS::MeshRendererComponent>()) {
|
||||
const auto& mr =
|
||||
handle.GetComponent<ECS::MeshRendererComponent>();
|
||||
if (Model* model =
|
||||
mr.model.Valid() ? assets.GetModel(mr.model)
|
||||
: nullptr) {
|
||||
for (const auto& p : a.attachedAnimPaths) {
|
||||
const bool done = std::any_of(
|
||||
model->GetClips().begin(),
|
||||
model->GetClips().end(),
|
||||
[&](const AnimationClip& c)
|
||||
{ return c.sourcePath == p; });
|
||||
if (!done) {
|
||||
assets.AttachAnimationClips(mr.model, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const YAML::Node n = comps["Camera"]) {
|
||||
@@ -798,6 +828,11 @@ namespace Engine::Serialization {
|
||||
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
|
||||
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
|
||||
cc.gravityScale = n["GravityScale"].as<f32>(cc.gravityScale);
|
||||
if (const YAML::Node so = n["ShapeOffset"]) {
|
||||
cc.shapeOffset = glm::vec3(so[0].as<f32>(0.0f),
|
||||
so[1].as<f32>(0.0f),
|
||||
so[2].as<f32>(0.0f));
|
||||
}
|
||||
cc.active = n["Active"].as<bool>(true);
|
||||
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||||
}
|
||||
|
||||
@@ -8,11 +8,55 @@
|
||||
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
|
||||
#include "Engine/Core/Stats.h"
|
||||
|
||||
namespace Engine {
|
||||
|
||||
u64 EstimateImageBytes(const vk::Format fmt, const u32 w, const u32 h,
|
||||
const u32 depth, const u32 mips)
|
||||
{
|
||||
// 线性格式 = 字节/像素;BC 压缩 = 字节/4x4 块(按块向上取整)。
|
||||
// 未识别格式按 RGBA8 保守估算。逐 mip 累加(mip 链约占 1/3 额外)
|
||||
f32 bpp = 0;
|
||||
u32 blockBytes = 0;
|
||||
switch (fmt) {
|
||||
case vk::Format::eR8Unorm:
|
||||
case vk::Format::eR8Srgb: bpp = 1; break;
|
||||
case vk::Format::eR8G8Unorm:
|
||||
case vk::Format::eR8G8Srgb: bpp = 2; break;
|
||||
case vk::Format::eR16G16B16A16Sfloat: bpp = 8; break;
|
||||
case vk::Format::eR32G32B32A32Sfloat: bpp = 16; break;
|
||||
case vk::Format::eB10G11R11UfloatPack32: bpp = 4; break;
|
||||
case vk::Format::eBc1RgbaUnormBlock:
|
||||
case vk::Format::eBc1RgbaSrgbBlock:
|
||||
case vk::Format::eBc4UnormBlock: blockBytes = 8; break;
|
||||
case vk::Format::eBc2SrgbBlock:
|
||||
case vk::Format::eBc2UnormBlock:
|
||||
case vk::Format::eBc3SrgbBlock:
|
||||
case vk::Format::eBc3UnormBlock:
|
||||
case vk::Format::eBc5UnormBlock:
|
||||
case vk::Format::eBc6HUfloatBlock:
|
||||
case vk::Format::eBc7SrgbBlock:
|
||||
case vk::Format::eBc7UnormBlock: blockBytes = 16; break;
|
||||
default: bpp = 4; break;
|
||||
}
|
||||
u64 total = 0;
|
||||
const u32 levels = std::max(1u, mips);
|
||||
for (u32 mip = 0; mip < levels; ++mip) {
|
||||
const u64 mw = std::max(1u, w >> mip);
|
||||
const u64 mh = std::max(1u, h >> mip);
|
||||
if (blockBytes != 0) {
|
||||
total += static_cast<u64>((mw + 3) / 4) *
|
||||
static_cast<u64>((mh + 3) / 4) * blockBytes * depth;
|
||||
} else {
|
||||
total += mw * mh * static_cast<u64>(bpp) * depth;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// props → VMA 创建参数:
|
||||
@@ -88,6 +132,7 @@ namespace Engine {
|
||||
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
|
||||
|
||||
STATS.AddMemoryUsed(size);
|
||||
STATS.AddMemoryBlock(size); // 1:1 VMA 分配:块 == 占用(无子分配)
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -108,11 +153,15 @@ namespace Engine {
|
||||
|
||||
Allocation out;
|
||||
out.Image = image;
|
||||
out.Size = ii.extent.depth * ii.extent.height * ii.extent.width; // 近似值(统计用)
|
||||
// 按真实格式 + mip 链估算(原来 w*h*d 近似值对 HDR/压缩全错)
|
||||
out.Size = EstimateImageBytes(imageInfo.format,
|
||||
ii.extent.width, ii.extent.height,
|
||||
ii.extent.depth, imageInfo.mipLevels);
|
||||
out.Handle = handle;
|
||||
out.Valid = true;
|
||||
|
||||
STATS.AddMemoryUsed(out.Size);
|
||||
STATS.AddMemoryBlock(out.Size);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -128,6 +177,7 @@ namespace Engine {
|
||||
}
|
||||
|
||||
STATS.RemoveMemoryUsed(alloc.Size);
|
||||
STATS.RemoveMemoryBlock(alloc.Size);
|
||||
alloc = {};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,19 @@ namespace Engine
|
||||
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
||||
{
|
||||
// 重入安全:CreateFromEncodedMemory 先经 ProceduralTag ctor 落 1x1
|
||||
// 占位(只为确定 device/allocator 成员),再调本函数传真实像素。
|
||||
// 必须先成对释放占位资源,否则 1x1 的 image/view/sampler 泄漏 →
|
||||
// allocator 析构时 VMA 块非空断言崩溃(GLB 内嵌贴图模型关闭必崩)
|
||||
if (m_Image) {
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
vkDevice.destroySampler(m_Sampler);
|
||||
vkDevice.destroyImageView(m_View);
|
||||
m_Allocator.Destroy(m_ImageAllocation);
|
||||
m_Sampler = nullptr;
|
||||
m_View = nullptr;
|
||||
m_Image = nullptr;
|
||||
}
|
||||
m_Width = width;
|
||||
m_Height = height;
|
||||
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
|
||||
@@ -299,8 +312,12 @@ namespace Engine
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
m_Allocator.Destroy(stagingAlloc);
|
||||
|
||||
// 显存占用 = 宽 * 高 * 4 字节(RGBA8)
|
||||
m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4;
|
||||
// 显存占用按真实格式 + mip 链估算(与分配器台账同一算法)
|
||||
if (m_TextureBytes != 0) {
|
||||
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||
}
|
||||
m_TextureBytes = EstimateImageBytes(m_Format, m_Width, m_Height, 1,
|
||||
m_MipLevels);
|
||||
STATS.AddTexture(m_TextureBytes);
|
||||
}
|
||||
|
||||
@@ -492,6 +509,9 @@ namespace Engine
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
m_Allocator.Destroy(stagingAlloc);
|
||||
|
||||
if (m_TextureBytes != 0) {
|
||||
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||
}
|
||||
m_TextureBytes = total;
|
||||
STATS.AddTexture(m_TextureBytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user