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
This commit is contained in:
+177
-19
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h> // CreateDefaultMaterialTexture
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
@@ -78,21 +79,12 @@ void Model::RebuildUnionBounds()
|
||||
|
||||
void Model::InitDefaultTextures()
|
||||
{
|
||||
// 各材质槽位的兜底纹理(1x1,开销可忽略):
|
||||
// albedo 白 / MR 平参数(roughness 0.5, metallic 0)/ emissive 黑
|
||||
m_DefaultTextures[Material::Albedo] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255);
|
||||
m_DefaultTextures[Material::MetallicRoughness] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 128, 0, false);
|
||||
m_DefaultTextures[Material::Normal] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 255, false);
|
||||
m_DefaultTextures[Material::Emissive] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
|
||||
m_DefaultTextures[Material::Ao] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 0, 0, false);
|
||||
// 独立粗糙度槽兜底:0.5 灰(无图资产 = 中性粗糙度)
|
||||
m_DefaultTextures[Material::Roughness] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 128, false);
|
||||
// 各槽位兜底纹理(1x1,开销可忽略):语义单点维护在
|
||||
// CreateDefaultMaterialTexture(MaterialAsset 构建端共用同一组)
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
m_DefaultTextures[slot] = CreateDefaultMaterialTexture(
|
||||
m_Device, m_Allocator, static_cast<Material::Slot>(slot));
|
||||
}
|
||||
}
|
||||
|
||||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
@@ -139,6 +131,10 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||||
// 程序化平面同样登记内建材质快照(与 m_Meshes 下标对齐,
|
||||
// 编辑器材质槽 UI 不越界)
|
||||
m_MeshMaterialInfos.push_back(
|
||||
{"Plane", {}, MaterialParams{}, transparent});
|
||||
RebuildUnionBounds();
|
||||
}
|
||||
|
||||
@@ -188,6 +184,12 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
|
||||
const aiScene* scene = importer.ReadFile(path,
|
||||
aiProcess_Triangulate |
|
||||
(out.isGltf ? 0 : aiProcess_FlipUVs) |
|
||||
// 注意:不用 aiProcess_TransformUVCoords 烘焙 KHR_texture_transform
|
||||
// ——该步骤的复合顺序是"先平移后缩放"(TextureTransform.cpp
|
||||
// matrix = m2·m4·R·m3·m5,m5 最右先作用),与 glTF2Importer 写出
|
||||
// AI_MATKEY_UVTRANSFORM 时假定的"先缩放旋转后平移"不一致,
|
||||
// 顺序差在 scale≠1 时结果偏出整数倍 tile(ChronographWatch
|
||||
// 背板实测)。改由 ProcessMesh 按正确约定自烘焙
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
@@ -218,8 +220,11 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
|
||||
std::filesystem::path(path).parent_path();
|
||||
// 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
|
||||
std::unordered_map<std::string, u32> boneSlots;
|
||||
// GLB 内嵌贴图字节池("*N" → PNG/JPEG 原始数据)
|
||||
std::unordered_map<std::string, std::vector<u8>> embeddedImages;
|
||||
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
|
||||
out.bones, boneSlots);
|
||||
out.bones, boneSlots, embeddedImages);
|
||||
out.embeddedImages = std::move(embeddedImages);
|
||||
|
||||
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
|
||||
// 这里按 node 表查 name 定位累乘路径)
|
||||
@@ -413,10 +418,13 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
model->m_Nodes = std::move(parsed.nodes);
|
||||
model->m_Bones = std::move(parsed.bones);
|
||||
model->m_Clips = std::move(parsed.clips);
|
||||
model->m_EmbeddedImages = std::move(parsed.embeddedImages);
|
||||
|
||||
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
|
||||
for (auto& pm : parsed.meshes) {
|
||||
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||
model->m_MeshMaterialInfos.push_back(
|
||||
{pm.name, pm.texPaths, pm.params, pm.transparent});
|
||||
model->m_Meshes.push_back(CreateScope<Mesh>(
|
||||
device, allocator, pm.vertices, pm.lods, mat,
|
||||
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||||
@@ -439,8 +447,11 @@ void Model::LoadModel(const std::string& path)
|
||||
m_Nodes = std::move(parsed.nodes);
|
||||
m_Bones = std::move(parsed.bones);
|
||||
m_Clips = std::move(parsed.clips);
|
||||
m_EmbeddedImages = std::move(parsed.embeddedImages);
|
||||
for (auto& pm : parsed.meshes) {
|
||||
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||
m_MeshMaterialInfos.push_back(
|
||||
{pm.name, pm.texPaths, pm.params, pm.transparent});
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, pm.vertices, pm.lods, mat,
|
||||
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||||
@@ -455,6 +466,8 @@ void Model::ProcessNode(const aiNode* node, 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& parentGlobal)
|
||||
{
|
||||
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
|
||||
@@ -465,11 +478,11 @@ void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
||||
for (u32 i = 0; i < node->mNumMeshes; ++i) {
|
||||
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
|
||||
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf,
|
||||
bones, boneSlots, global));
|
||||
bones, boneSlots, embeddedImages, global));
|
||||
}
|
||||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||||
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
|
||||
bones, boneSlots, global);
|
||||
bones, boneSlots, embeddedImages, global);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +499,26 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// GLB 内嵌贴图("*N"):字节在解析期随 ParsedModel 带过来,
|
||||
// 此处(主线程、GPU 上下文)解码上传
|
||||
if (!path.empty() && path[0] == '*') {
|
||||
const auto emb = m_EmbeddedImages.find(path);
|
||||
if (emb == m_EmbeddedImages.end()) {
|
||||
ENGINE_WARN("Embedded texture bytes not carried: {}", path);
|
||||
return nullptr;
|
||||
}
|
||||
auto tex = Texture::CreateFromEncodedMemory(
|
||||
m_Device, m_Allocator, emb->second.data(),
|
||||
static_cast<u32>(emb->second.size()), srgb);
|
||||
if (!tex) {
|
||||
ENGINE_WARN("Embedded texture decode failed: {}", path);
|
||||
return nullptr;
|
||||
}
|
||||
Texture* ptr = tex.get();
|
||||
m_TextureCache.emplace(key, std::move(tex));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
// 文件不存在则用兜底纹理
|
||||
if (!std::filesystem::exists(path)) {
|
||||
ENGINE_WARN("Texture not found, using default: {}", path);
|
||||
@@ -549,6 +582,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
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)
|
||||
{
|
||||
ParsedMesh out;
|
||||
@@ -626,10 +661,21 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
// 读取材质纹理路径(PBR metallic-roughness 槽位)+ 金属度/粗糙度因子
|
||||
std::string texPaths[Material::kSlotCount];
|
||||
MaterialParams params;
|
||||
std::string materialName; // 材质资产名(UI 材质槽显示/覆盖文件命名优先用它)
|
||||
|
||||
if (mesh->mMaterialIndex >= 0) {
|
||||
const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
||||
|
||||
// 材质名(glTF material name / FBX 材质名):Blender 导出的 mesh
|
||||
// 名是 "Plane.023" 这类几何拓扑名,对"这是哪个材质"毫无信息量;
|
||||
// 材质名才是 UE Material Slots 列表的语义
|
||||
aiString matName;
|
||||
if (material &&
|
||||
material->Get(AI_MATKEY_NAME, matName) == aiReturn_SUCCESS &&
|
||||
matName.length > 0) {
|
||||
materialName = matName.C_Str();
|
||||
}
|
||||
|
||||
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
|
||||
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
|
||||
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
|
||||
@@ -653,12 +699,46 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
{aiTextureType_EMISSION_COLOR, Material::Emissive},
|
||||
};
|
||||
|
||||
// 每槽命中源类型:KHR_texture_transform 在 assimp 侧按
|
||||
// aiTextureType 键控(AI_MATKEY_UVTRANSFORM(type, 0)),必须按
|
||||
// 命中时的同款类型查询才能取到对应贴图的变换
|
||||
aiTextureType slotSrcType[Material::kSlotCount] = {};
|
||||
|
||||
for (const auto& [type, slot] : slots) {
|
||||
if (!texPaths[slot].empty()) continue; // 高优先级已命中
|
||||
aiString path;
|
||||
if (material->GetTexture(type, 0, &path) == AI_SUCCESS &&
|
||||
path.length > 0) {
|
||||
texPaths[slot] = (modelDir / path.C_Str()).string();
|
||||
slotSrcType[slot] = type;
|
||||
if (path.C_Str()[0] == '*') {
|
||||
// GLB/gltf 内嵌贴图:assimp 记作 "*<index>"(aiTexture
|
||||
// 表下标),不是磁盘路径——解析期拷出原始编码字节,
|
||||
// 实例期 GetOrLoadTexture 解码上传(解析线程无 GPU)
|
||||
texPaths[slot] = path.C_Str();
|
||||
if (!embeddedImages.count(path.C_Str())) {
|
||||
const int texIdx = atoi(path.C_Str() + 1);
|
||||
if (texIdx < 0 ||
|
||||
texIdx >= static_cast<int>(scene->mNumTextures)) {
|
||||
ENGINE_WARN("Embedded texture index out of range: "
|
||||
"{}", path.C_Str());
|
||||
continue;
|
||||
}
|
||||
const aiTexture* t = scene->mTextures[texIdx];
|
||||
if (t->mHeight != 0) {
|
||||
// 未压缩 ARGB8888 内嵌数据(罕见):折默认纹理
|
||||
ENGINE_WARN("Uncompressed embedded texture "
|
||||
"unsupported: {}", path.C_Str());
|
||||
continue;
|
||||
}
|
||||
const u8* src =
|
||||
reinterpret_cast<const u8*>(t->pcData);
|
||||
embeddedImages[path.C_Str()].assign(
|
||||
src, src + t->mWidth);
|
||||
}
|
||||
}
|
||||
else {
|
||||
texPaths[slot] = (modelDir / path.C_Str()).string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 注:不做文件名探测/自动补槽——资产缺什么就是什么;
|
||||
@@ -717,6 +797,81 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
params.factors.w = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// 表面扩展四键(assimp 仅在扩展存在时写出,缺省 = 物理默认):
|
||||
// KHR_materials_ior → REFRACTI(缺省 1.5)
|
||||
// KHR_materials_transmission → TRANSMISSION_FACTOR(缺省 0)
|
||||
// KHR_materials_emissive_strength → EMISSIVE_INTENSITY(缺省 1)
|
||||
// KHR_materials_volume → VOLUME_THICKNESS_FACTOR(缺省 0)
|
||||
ai_real ior = 1.5f;
|
||||
material->Get(AI_MATKEY_REFRACTI, ior);
|
||||
ai_real transmission = 0.0f;
|
||||
material->Get(AI_MATKEY_TRANSMISSION_FACTOR, transmission);
|
||||
ai_real emissiveStrength = 1.0f;
|
||||
material->Get(AI_MATKEY_EMISSIVE_INTENSITY, emissiveStrength);
|
||||
ai_real thickness = 0.0f;
|
||||
material->Get(AI_MATKEY_VOLUME_THICKNESS_FACTOR, thickness);
|
||||
params.surface = glm::vec4(static_cast<f32>(ior),
|
||||
static_cast<f32>(transmission),
|
||||
static_cast<f32>(emissiveStrength),
|
||||
static_cast<f32>(thickness));
|
||||
|
||||
// transmission > 0 的材质必须走透明队列:物理透射表面要采样
|
||||
// 不透明场景副本做折射。Khronos 新式玻璃写法是 alphaMode=OPAQUE
|
||||
// + transmission=1(透明度语义全在 transmission,alpha 恒 1)——
|
||||
// 只认 alphaMode=BLEND 会把玻璃送进不透明队列,画成白 albedo
|
||||
// 镜面实心盖板(表盘/指针全被挡住,正面"糊成一坨")
|
||||
if (static_cast<f32>(transmission) > 0.0f) {
|
||||
out.transparent = true;
|
||||
}
|
||||
|
||||
// KHR_texture_transform:每张贴图各带独立 offset/scale/rotation
|
||||
//(AnimationPointerUVs 一致性模型:82 材质共享一张棋盘格、
|
||||
// 各槽变换各异),烘焙进顶点 UV 不可行(顶点 UV 只有一份),
|
||||
// 改写入材质参数、shader 采样前逐槽应用。
|
||||
//
|
||||
// importer 把 glTF 参数换算进 assimp 约定(缩放 → 绕 (0.5,0.5)
|
||||
// 旋转 -rot → 平移)写入 UVTRANSFORM;该换算在 θ≠0 时与 glTF
|
||||
// 规范语义不符(数值实测误差 0.44+),θ=0 才精确。这里先从
|
||||
// assimp 值精确反演回原始 glTF 参数,再用规范公式写入:
|
||||
// rot = -mRotation
|
||||
// off.x = mTrl.x - 0.5·sx·(-c + s + 1)
|
||||
// off.y = 1 - sy - mTrl.y + 0.5·sy·(s + c - 1)
|
||||
// spec(glTF v-down 空间 uv' = T(off)·R(rot)·S(scale)·uv,
|
||||
// 折算进引擎 v-up 顶点 UV 空间 uv' = A·uv + t):
|
||||
// A = [[c·sx, s·sy], [-s·sx, c·sy]](行主序摊平 vec4)
|
||||
// t = (off.x - s·sy, 1 - off.y - c·sy)
|
||||
// rot=0 时退化为已视觉验证的旧烘焙式 t=(off.x, 1-sy-off.y)
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
if (texPaths[slot].empty()) continue;
|
||||
aiUVTransform at;
|
||||
if (material->Get(AI_MATKEY_UVTRANSFORM(slotSrcType[slot], 0),
|
||||
at) != AI_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
const f32 sx = static_cast<f32>(at.mScaling.x);
|
||||
const f32 sy = static_cast<f32>(at.mScaling.y);
|
||||
const f32 rot = -static_cast<f32>(at.mRotation);
|
||||
const f32 c = std::cos(rot);
|
||||
const f32 s = std::sin(rot);
|
||||
const f32 offx = static_cast<f32>(at.mTranslation.x)
|
||||
- 0.5f * sx * (-c + s + 1.0f);
|
||||
const f32 offy = 1.0f - sy
|
||||
- static_cast<f32>(at.mTranslation.y)
|
||||
+ 0.5f * sy * (s + c - 1.0f);
|
||||
params.uvA[slot] = glm::vec4(c * sx, s * sy, -s * sx, c * sy);
|
||||
params.uvT[slot] = glm::vec4(offx - s * sy,
|
||||
1.0f - offy - c * sy,
|
||||
0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// KHR_materials_unlit:misc.x=1 → shader 早退直出 albedo
|
||||
int shadingModel = 0;
|
||||
if (material->Get(AI_MATKEY_SHADING_MODEL, shadingModel)
|
||||
== AI_SUCCESS &&
|
||||
shadingModel == aiShadingMode_Unlit) {
|
||||
params.misc.x = 1.0f;
|
||||
}
|
||||
} else {
|
||||
// FBX/OBJ 独立贴图工作流
|
||||
const bool hasSepRough = !texPaths[Material::Roughness].empty();
|
||||
@@ -745,6 +900,9 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
}
|
||||
out.params = params;
|
||||
// out.transparent 已在 glTF 分支读取
|
||||
// UI 名字:材质名优先(语义对齐 UE Material Slots),无材质名
|
||||
//(罕见)回退 mesh 名
|
||||
out.name = !materialName.empty() ? materialName : mesh->mName.C_Str();
|
||||
|
||||
// ---- LOD 链(解析期生成,worker 线程免费时间):----
|
||||
// LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享,
|
||||
|
||||
Reference in New Issue
Block a user