Files
HelloVulkan/Engine/include/Engine/Renderer/Model.h
T
atdunbg f107854236 MeshFactory:程序化几何单一源 + CreatePrimitive 链路
新增 MeshFactory(Cube/Plane/Sphere/Cylinder/Cone/Torus + 切线计算),
Model 程序化几何改走 MeshFactory(收敛为单一几何源,消除重复的顶点
生成),并新增 CreatePrimitive;打通 AssetManager/IAssetUploader/
VulkanAssetUploader 的 CreatePrimitive 链路;编辑器 Procedural 弹窗
下拉 + ResolveProcedural 伪路径反解 + 场景序列化(程序化图元可存取)。
2026-10-18 17:09:25 +08:00

330 lines
17 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 <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
#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);
// 资产显示名(编辑器 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,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ParsedModel&& parsed);
// 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
// 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
struct ProceduralTag {};
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
// size 为半边长(平面总边长 = 2*size)。
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);
// 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube):
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
// 缩放——同 halfSize 同色共享一份缓存模型
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b);
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
// cone/torus 等):几何由 MeshFactory::Make 生成,纯色 albedo。
// 非法形状名返回 nullptr(调用方回退)
static Scope<Model> CreatePrimitive(Device& device,
MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
const std::string& shape,
const PrimitiveDesc& desc,
u8 r, u8 g, u8 b);
~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:
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
bool transparent, const std::string& name);
// 空模型骨架(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