阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 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:
2026-10-11 22:22:19 +08:00
parent 334819bf97
commit beba491bcb
57 changed files with 43200 additions and 1378 deletions
@@ -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 的表现一致(无动画/组件禁用实体的
+112 -15
View File
@@ -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
+113
View File
@@ -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
+17 -22
View File
@@ -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()
+3
View File
@@ -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
+12 -1
View File
@@ -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;
+24 -4
View File
@@ -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;
+6
View File
@@ -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);