- .animctrl state machine asset (params/transitions/blend trees, 1D+2D directional blending) with Unity Animator-style editor panel: state list, transitions, params, directional pad preview driving live CrossFade on the selected entity - Split animation evaluation out of SceneRenderer into AnimationSystem/AnimationEvaluator; skin palettes with rest-pose fallback and blend-weight epsilon filtering - MaterialAsset (.material) with reload-safe preview tile pool (DropMaterial clears dirty refs before instance destruction) - EditorGizmoPass skeleton wires + viewport joint overlay; camera projection rework (baked Vulkan Y-flip, ortho support) - Editor/game build split via HELLOVULKAN_EDITOR option
298 lines
15 KiB
C++
298 lines
15 KiB
C++
//
|
||
// Created by atdunbg on 2026/9/21.
|
||
//
|
||
|
||
#ifndef HELLOVULKAN_MODEL_H
|
||
#define HELLOVULKAN_MODEL_H
|
||
|
||
#pragma once
|
||
|
||
#include <Engine/Core/Types.h>
|
||
#include <Engine/Renderer/Material.h>
|
||
#include <Engine/Renderer/Mesh.h>
|
||
#include <glm/gtc/quaternion.hpp>
|
||
#include <vulkan/vulkan.hpp>
|
||
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <filesystem>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <vector>
|
||
|
||
struct aiNode;
|
||
struct aiMesh;
|
||
struct aiScene;
|
||
|
||
namespace Engine
|
||
{
|
||
class Device;
|
||
class MemoryAllocator;
|
||
class DescriptorManager;
|
||
class Texture;
|
||
class Material;
|
||
|
||
// ---- CPU 解析产物(异步加载的跨线程交棒数据)----
|
||
// 纯数据:几何 + 材质槽位引用(路径 + 因子),无任何 GPU 资源。
|
||
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
|
||
struct ParsedMesh {
|
||
std::vector<Vertex> vertices;
|
||
// LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify
|
||
// 生成(worker 线程),消费端共享顶点只换索引
|
||
std::vector<std::vector<u32>> lods;
|
||
std::array<std::string, Material::kSlotCount> texPaths;
|
||
MaterialParams params;
|
||
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
|
||
bool skinned = false; // 有骨骼(渲染走 palette 蒙皮)
|
||
std::string name; // UI 名字:材质名(AI_MATKEY_NAME)优先,
|
||
// 回退 aiMesh mName(可空 → UI 回退 Slot N)
|
||
|
||
// 蒙皮 mesh 专属:绑定姿态渲染空间的位置副本(palette(bind) 按
|
||
// 顶点权重混合,与 shader 同式)。非空 = Mesh 的物理数据源与
|
||
// 包围球改用它——FBX 单位换算折在骨骼链上(bind palette ≠ I),
|
||
// 直接用原始顶点会让物理凸包大一个数量级(M1911 实测 10×)
|
||
std::vector<glm::vec3> cpuPositions;
|
||
};
|
||
|
||
// 内建材质源信息快照(per-mesh,与 GetMeshes() 下标对齐)。
|
||
// 只读语义:这是"模型固有材质"的权威描述——编辑器用它展示内建
|
||
// 材质并生成覆盖材质快照(Create Override),**永不回写**:
|
||
// 内建材质属于模型资产,原地修改会波及所有引用该模型的实体。
|
||
// 可编辑材质走 MaterialAsset(.material 文件)+ 实体级覆盖表
|
||
struct MeshMaterialInfo {
|
||
std::string name; // UI 名字:材质名优先,回退 mesh 名(空 → Slot N)
|
||
std::array<std::string, Material::kSlotCount> texPaths;
|
||
MaterialParams params;
|
||
bool transparent = false;
|
||
};
|
||
|
||
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
|
||
// 关键帧(time 单位 = clip 原生 tick,运行时按 tps 换算秒)
|
||
struct PosKey { f32 time; glm::vec3 value; };
|
||
struct RotKey { f32 time; glm::quat value; };
|
||
struct SclKey { f32 time; glm::vec3 value; };
|
||
|
||
// 一条节点动画轨道:作用于 node 表中下标为 nodeIdx 的节点;
|
||
// 无轨道的分量运行时用节点静止姿态(localTransform)
|
||
struct AnimChannel {
|
||
u32 nodeIdx = 0;
|
||
std::vector<PosKey> positions;
|
||
std::vector<RotKey> rotations;
|
||
std::vector<SclKey> scales;
|
||
};
|
||
|
||
struct AnimationClip {
|
||
std::string name;
|
||
f32 duration = 0.0f; // 秒(已按 tps 换算)
|
||
f32 tps = 1.0f; // ticks per second(0 按 1,assimp 惯例)
|
||
std::vector<AnimChannel> channels;
|
||
// 外部动画文件来源路径(运行时并入的 clip 标记;空 = 模型内嵌)。
|
||
// 用于幂等去重:同一源文件不重复并入
|
||
std::string sourcePath;
|
||
};
|
||
|
||
// aiNode 树拍平表:骨骼 global 变换的累乘骨架(解析期 ProcessNode
|
||
// 原本丢弃 mTransformation,骨骼层级必须保留)
|
||
struct ModelNode {
|
||
std::string name;
|
||
i32 parent = -1; // 根节点 = -1
|
||
glm::mat4 localTransform{1.0f}; // 静止姿态 local TRS(assimp 行主→glm 列主已转置)
|
||
};
|
||
|
||
// 骨骼 = palette 槽位 ↔ 节点 ↔ 逆绑定矩阵
|
||
// palette[i] = global(bone.nodeIdx) × invBind
|
||
struct ModelBone {
|
||
std::string name;
|
||
u32 nodeIdx = 0;
|
||
glm::mat4 invBind{1.0f}; // aiBone mOffsetMatrix(绑定姿势全局逆)
|
||
};
|
||
|
||
struct ParsedModel {
|
||
std::string path;
|
||
bool isGltf = false;
|
||
std::vector<ParsedMesh> meshes;
|
||
std::vector<ModelNode> nodes;
|
||
std::vector<ModelBone> bones;
|
||
std::vector<AnimationClip> clips;
|
||
// GLB 内嵌贴图(assimp "*<index>" 约定):解析期从 aiScene 拷出
|
||
// 原始编码字节(PNG/JPEG),实例期 GetOrLoadTexture 解码上传。
|
||
// 解析线程无 GPU 上下文,只带数据不建纹理
|
||
std::unordered_map<std::string, std::vector<u8>> embeddedImages;
|
||
// 解析有效性:mesh 与 clip 至少其一非空。纯动画 FBX(无网格只有
|
||
// 骨骼动画,UE 的 Animation Sequence 资产同理)是合法资产——
|
||
// 过去只认 meshes 导致动画源文件在缓存层被整体拒载
|
||
bool Valid() const { return !meshes.empty() || !clips.empty(); }
|
||
};
|
||
|
||
class Model
|
||
{
|
||
public:
|
||
// 同步便捷路径(等价 ParseFromDisk + FromParsed,启动期小模型可用)
|
||
Model(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
const std::string& path);
|
||
|
||
// ---- 异步加载两段式(assimp 解析占大模型耗时大头,可搬后台线程)----
|
||
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||
static ParsedModel ParseFromDisk(const std::string& path);
|
||
|
||
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||
// 解析失败返回 nullptr。
|
||
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
ParsedModel&& parsed);
|
||
|
||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||
// size 为半边长(平面总边长 = 2*size)。
|
||
struct ProceduralTag {};
|
||
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
f32 halfSize, u8 r, u8 g, u8 b);
|
||
// 半透明纯色平面(alpha 0-255,预乘 alpha 链):透明管线验证用
|
||
static Scope<Model> CreateTransparentPlane(
|
||
Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
|
||
|
||
~Model();
|
||
|
||
Model(const Model&) = delete;
|
||
Model& operator=(const Model&) = delete;
|
||
|
||
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
|
||
// 内建材质源信息(与 GetMeshes() 下标对齐;只读,见结构注释)
|
||
const std::vector<MeshMaterialInfo>& GetMeshMaterialInfos() const
|
||
{
|
||
return m_MeshMaterialInfos;
|
||
}
|
||
// 全模型联合包围球(各 mesh bounds 的并集,局部空间):
|
||
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
|
||
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
|
||
|
||
// ---- 骨骼动画运行时数据(无动画模型全部为空,零开销)----
|
||
bool HasAnimations() const { return !m_Clips.empty(); }
|
||
u32 GetBoneCount() const { return static_cast<u32>(m_Bones.size()); }
|
||
const std::vector<AnimationClip>& GetClips() const { return m_Clips; }
|
||
// 外部动画并入(编辑器 attach 操作):追加到 clip 表尾部,
|
||
// 既有 clipIndex 不受影响。仅运行时内存状态(重启后由场景
|
||
// 里 Animation.attachedAnimPaths 驱动重放)
|
||
void AppendClips(std::vector<AnimationClip>&& extra)
|
||
{
|
||
m_Clips.insert(m_Clips.end(),
|
||
std::make_move_iterator(extra.begin()),
|
||
std::make_move_iterator(extra.end()));
|
||
}
|
||
// 外部动画剥离(编辑器 detach 操作):删除 sourcePath 匹配的
|
||
// clip,返回删除数。clipIndex/fadeFrom 下标悬空由消费端自愈
|
||
//(AnimationSystem 每帧 clamp)
|
||
u32 RemoveClipsBySource(const std::string& sourcePath)
|
||
{
|
||
const u32 before = static_cast<u32>(m_Clips.size());
|
||
m_Clips.erase(
|
||
std::remove_if(m_Clips.begin(), m_Clips.end(),
|
||
[&](const AnimationClip& c)
|
||
{ return c.sourcePath == sourcePath; }),
|
||
m_Clips.end());
|
||
return before - static_cast<u32>(m_Clips.size());
|
||
}
|
||
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
|
||
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
|
||
|
||
private:
|
||
Model(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
|
||
bool transparent);
|
||
|
||
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||
Model(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout);
|
||
|
||
void LoadModel(const std::string& path);
|
||
// aiNode 树拍平成 node 表(DFS 序,parent 下标回填)
|
||
static u32 FlattenNodes(const aiNode* node, i32 parent,
|
||
std::vector<ModelNode>& out);
|
||
// 每帧动画的 channel 轨道提取(nodeName → node 表下标)
|
||
static void ParseAnimations(const aiScene* scene,
|
||
const std::vector<ModelNode>& nodes,
|
||
std::vector<AnimationClip>& out);
|
||
static void ProcessNode(const aiNode* node, const aiScene* scene,
|
||
std::vector<ParsedMesh>& out,
|
||
const std::filesystem::path& modelDir,
|
||
bool isGltf,
|
||
std::vector<ModelBone>& bones,
|
||
std::unordered_map<std::string, u32>& boneSlots,
|
||
std::unordered_map<std::string,
|
||
std::vector<u8>>& embeddedImages,
|
||
const aiMatrix4x4& parentGlobal
|
||
= aiMatrix4x4());
|
||
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||
const std::filesystem::path& modelDir,
|
||
bool isGltf,
|
||
std::vector<ModelBone>& bones,
|
||
std::unordered_map<std::string, u32>& boneSlots,
|
||
std::unordered_map<std::string,
|
||
std::vector<u8>>& embeddedImages,
|
||
const aiMatrix4x4& globalTransform
|
||
= aiMatrix4x4());
|
||
// mesh 的 aiBone 权重灌进顶点 + 模型级骨骼表注册去重
|
||
static void CollectMeshBones(const aiMesh* mesh,
|
||
std::vector<Vertex>& vertices,
|
||
std::vector<ModelBone>& bones,
|
||
std::unordered_map<std::string, u32>& boneSlots);
|
||
void InitDefaultTextures();
|
||
|
||
// GLB 内嵌贴图字节("*N" → 编码数据):解析期拷出,纹理解析期
|
||
// GetOrLoadTexture 消费(随实例生命周期,键同 texPaths 引用)
|
||
std::unordered_map<std::string, std::vector<u8>> m_EmbeddedImages;
|
||
|
||
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
|
||
void RebuildUnionBounds();
|
||
|
||
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||
|
||
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
|
||
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
|
||
// paths 下标 = Material::Slot;params = metallic/roughness 因子;
|
||
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
|
||
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
|
||
const MaterialParams& params,
|
||
bool transparent = false,
|
||
const std::array<Texture*, Material::kSlotCount>&
|
||
slotOverrides = {});
|
||
|
||
Device& m_Device;
|
||
MemoryAllocator& m_Allocator;
|
||
DescriptorManager& m_DescriptorManager;
|
||
vk::DescriptorSetLayout m_TextureLayout;
|
||
|
||
// 缓存声明在 m_Meshes 之前:mesh 持 Material* 裸指针,
|
||
// 析构序 = meshes 先于材质/纹理缓存
|
||
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
|
||
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
||
// 各槽位默认纹理(材质缺图时兜底):白 / 平法线 / 黑高光 / 黑自发光
|
||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
||
|
||
std::vector<Scope<Mesh>> m_Meshes;
|
||
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
|
||
// 与 m_Meshes 平行的内建材质快照(每条建 mesh 路径同步填充:
|
||
// FromParsed / 同步 LoadModel / 程序化平面)
|
||
std::vector<MeshMaterialInfo> m_MeshMaterialInfos;
|
||
std::string m_ModelPath;
|
||
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
|
||
|
||
// ---- 骨骼动画运行时数据(FromParsed/LoadModel 从解析产物 move)----
|
||
std::vector<ModelNode> m_Nodes;
|
||
std::vector<ModelBone> m_Bones;
|
||
std::vector<AnimationClip> m_Clips;
|
||
};
|
||
} // namespace Engine
|
||
|
||
#endif //HELLOVULKAN_MODEL_H
|