Files
HelloVulkan/Engine/include/Engine/Renderer/Model.h
T
atdunbg cc837bc605 Add animation controller system, material assets and skeleton visualization
- .animctrl state machine asset (params/transitions/blend trees, 1D+2D
  directional blending) with Unity Animator-style editor panel: state
  list, transitions, params, directional pad preview driving live
  CrossFade on the selected entity
- Split animation evaluation out of SceneRenderer into
  AnimationSystem/AnimationEvaluator; skin palettes with rest-pose
  fallback and blend-weight epsilon filtering
- MaterialAsset (.material) with reload-safe preview tile pool
  (DropMaterial clears dirty refs before instance destruction)
- EditorGizmoPass skeleton wires + viewport joint overlay; camera
  projection rework (baked Vulkan Y-flip, ortho support)
- Editor/game build split via HELLOVULKAN_EDITOR option
2026-10-07 01:50:04 +08:00

298 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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