Skinned meshes keep vertices in raw mesh space (bakeTransform=false); rendering transforms them via bind palette (global(bone) x invBind, folds FBX unit scale). Physics MeshShape/ConvexHull and the mesh bounding sphere consumed the raw positions directly -> M1911 hull measured ~10x the visual size. ParseFromDisk now bakes bind-palette-skinned positions per vertex (same formula as the skinning shader) into ParsedMesh::cpuPositions; Mesh uses them for m_CpuPositions and bounds. GPU vertex data and rendering are untouched.
763 lines
33 KiB
C++
763 lines
33 KiB
C++
//
|
||
// Created by atdunbg on 2026/9/21.
|
||
//
|
||
|
||
#include <Engine/Renderer/Model.h>
|
||
#include <Engine/Renderer/Material.h>
|
||
#include <Engine/Vulkan/Device.h>
|
||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||
#include <Engine/Vulkan/DescriptorManager.h>
|
||
#include <Engine/Vulkan/Texture.h>
|
||
#include <Engine/Core/Log.h>
|
||
|
||
#include <assimp/Importer.hpp>
|
||
#include <assimp/scene.h>
|
||
#include <assimp/postprocess.h>
|
||
#include <assimp/GltfMaterial.h> // AI_MATKEY_GLTF_ALPHAMODE
|
||
|
||
#include <meshoptimizer.h>
|
||
|
||
#include <filesystem>
|
||
#include <algorithm>
|
||
|
||
namespace Engine {
|
||
|
||
namespace {
|
||
// assimp 行主矩阵 → glm 列主(转置搬运;骨骼 node TRS / invBind 共用)
|
||
glm::mat4 ToGlm(const aiMatrix4x4& m)
|
||
{
|
||
glm::mat4 out;
|
||
out[0][0] = m.a1; out[1][0] = m.a2; out[2][0] = m.a3; out[3][0] = m.a4;
|
||
out[0][1] = m.b1; out[1][1] = m.b2; out[2][1] = m.b3; out[3][1] = m.b4;
|
||
out[0][2] = m.c1; out[1][2] = m.c2; out[2][2] = m.c3; out[3][2] = m.c4;
|
||
out[0][3] = m.d1; out[1][3] = m.d2; out[2][3] = m.d3; out[3][3] = m.d4;
|
||
return out;
|
||
}
|
||
}
|
||
|
||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
const std::string& path)
|
||
: Model(device, allocator, descriptorManager, textureLayout)
|
||
{
|
||
// 同步便捷路径 = 两段式串行(等价异步收割后的主线程消费)
|
||
LoadModel(path);
|
||
}
|
||
|
||
// 空模型骨架:只建默认纹理;mesh/材质由 FromParsed 填充
|
||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout)
|
||
: m_Device(device)
|
||
, m_Allocator(allocator)
|
||
, m_DescriptorManager(descriptorManager)
|
||
, m_TextureLayout(textureLayout)
|
||
{
|
||
InitDefaultTextures();
|
||
}
|
||
|
||
Model::~Model() = default;
|
||
|
||
void Model::RebuildUnionBounds()
|
||
{
|
||
if (m_Meshes.empty()) {
|
||
m_Bounds = {};
|
||
return;
|
||
}
|
||
// 各 mesh 包围球 → AABB 并集 → 外接球(保守,宁多画不漏画)
|
||
glm::vec3 mn(1e9f), mx(-1e9f);
|
||
for (const auto& mesh : m_Meshes) {
|
||
const auto& b = mesh->GetBounds();
|
||
mn = glm::min(mn, b.center - glm::vec3(b.radius));
|
||
mx = glm::max(mx, b.center + glm::vec3(b.radius));
|
||
}
|
||
m_Bounds.center = (mn + mx) * 0.5f;
|
||
m_Bounds.radius = 0.5f * glm::length(mx - mn);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
|
||
bool transparent)
|
||
: m_Device(device)
|
||
, m_Allocator(allocator)
|
||
, m_DescriptorManager(descriptorManager)
|
||
, m_TextureLayout(textureLayout)
|
||
{
|
||
// 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质);
|
||
// 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突)
|
||
InitDefaultTextures();
|
||
Scope<Texture> tex = (rgba[3] == 255)
|
||
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
|
||
: Texture::CreateSolidColorRGBA(device, allocator,
|
||
rgba[0], rgba[1], rgba[2], rgba[3]);
|
||
Texture* albedoOverride = tex.get();
|
||
const std::string texKey = "proc:" + std::to_string(rgba[0]) + ',' +
|
||
std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' +
|
||
std::to_string(rgba[3]);
|
||
m_TextureCache.emplace(texKey, std::move(tex));
|
||
|
||
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
|
||
const f32 h = halfSize;
|
||
std::vector<Vertex> vertices = {
|
||
{{-h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||
{{-h, 0.0f, h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||
{{ h, 0.0f, h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||
{{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||
};
|
||
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
|
||
const std::vector<std::vector<u32>> lodIndices = {indices};
|
||
|
||
// 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
|
||
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
|
||
std::string paths[Material::kSlotCount] = {"", "", "", ""};
|
||
std::array<Texture*, Material::kSlotCount> overrides{};
|
||
overrides[Material::Albedo] = albedoOverride;
|
||
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent,
|
||
overrides);
|
||
|
||
m_Meshes.push_back(CreateScope<Mesh>(
|
||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||
RebuildUnionBounds();
|
||
}
|
||
|
||
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
f32 halfSize, u8 r, u8 g, u8 b)
|
||
{
|
||
return Scope<Model>(new Model(device, allocator, descriptorManager,
|
||
textureLayout, ProceduralTag{},
|
||
halfSize, std::array<u8, 4>{r, g, b, 255},
|
||
false));
|
||
}
|
||
|
||
Scope<Model> Model::CreateTransparentPlane(
|
||
Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
|
||
{
|
||
return Scope<Model>(new Model(device, allocator, descriptorManager,
|
||
textureLayout, ProceduralTag{},
|
||
halfSize, std::array<u8, 4>{r, g, b, a},
|
||
true));
|
||
}
|
||
|
||
ParsedModel Model::ParseFromDisk(const std::string& path)
|
||
{
|
||
ParsedModel out;
|
||
out.path = path;
|
||
|
||
// UV 翻转按格式区分:
|
||
// OBJ/FBX —— v 原点在底部,需翻到左上惯例(配合"v=0=图像顶"采样)
|
||
// glTF/GLB —— UV 本就是左上原点(v=0=图像顶),翻转会把所有
|
||
// 贴图上下颠倒(法线/MR 全反 = 塑料感元凶)
|
||
const std::string lower = [&] {
|
||
std::string s = path;
|
||
std::transform(s.begin(), s.end(), s.begin(),
|
||
[](unsigned char c) { return std::tolower(c); });
|
||
return s;
|
||
}();
|
||
out.isGltf = lower.size() >= 5 &&
|
||
(lower.rfind(".gltf") == lower.size() - 5 ||
|
||
lower.rfind(".glb") == lower.size() - 4);
|
||
|
||
Assimp::Importer importer;
|
||
const aiScene* scene = importer.ReadFile(path,
|
||
aiProcess_Triangulate |
|
||
(out.isGltf ? 0 : aiProcess_FlipUVs) |
|
||
aiProcess_GenSmoothNormals |
|
||
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||
aiProcess_JoinIdenticalVertices |
|
||
aiProcess_LimitBoneWeights); // 每顶点限 4 权重(= 顶点格式 boneIds 容量)
|
||
|
||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
||
if (!scene->mNumMeshes) {
|
||
ENGINE_WARN("Assimp warn: model dose not have mesh assets\nmodel has following asset:");
|
||
|
||
ENGINE_WARN("\tMeshes Count: {}", scene->mNumMeshes);
|
||
ENGINE_WARN("\tAnimations Count: {}", scene->mNumAnimations);
|
||
ENGINE_WARN("\tCameras Count: {}", scene->mNumCameras);
|
||
ENGINE_WARN("\tLights Count: {}", scene->mNumLights);
|
||
ENGINE_WARN("\tMaterials Count: {}", scene->mNumMaterials);
|
||
ENGINE_WARN("\tSkeletons Count: {}", scene->mNumSkeletons);
|
||
ENGINE_WARN("\tTextures Count: {}", scene->mNumTextures);
|
||
|
||
}
|
||
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
|
||
return out; // meshes 空 = 无效
|
||
}
|
||
|
||
// 骨骼动画三件套:node 表(global 累乘骨架)→ 动画轨道 → 骨骼注册
|
||
//(DFS 序;ProcessNode 的 mesh 遍历序与其独立,nodeIdx 靠 name 查表)
|
||
FlattenNodes(scene->mRootNode, -1, out.nodes);
|
||
ParseAnimations(scene, out.nodes, out.clips);
|
||
|
||
const std::filesystem::path modelDir =
|
||
std::filesystem::path(path).parent_path();
|
||
// 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
|
||
std::unordered_map<std::string, u32> boneSlots;
|
||
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
|
||
out.bones, boneSlots);
|
||
|
||
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
|
||
// 这里按 node 表查 name 定位累乘路径)
|
||
if (!out.bones.empty()) {
|
||
std::unordered_map<std::string, u32> nodeByName;
|
||
nodeByName.reserve(out.nodes.size());
|
||
for (u32 i = 0; i < out.nodes.size(); ++i) {
|
||
nodeByName.emplace(out.nodes[i].name, i);
|
||
}
|
||
for (auto& bone : out.bones) {
|
||
if (auto it = nodeByName.find(bone.name); it != nodeByName.end()) {
|
||
bone.nodeIdx = it->second;
|
||
} else {
|
||
ENGINE_WARN("Bone '{}' has no matching node, palette entry static",
|
||
bone.name);
|
||
}
|
||
}
|
||
|
||
// 蒙皮 mesh 的 CPU 位置副本换算到渲染空间:渲染 = palette(t)×raw
|
||
//(palette = global(bone)×invBind,与 shader 同式),物理/包围球取
|
||
// 绑定姿态。FBX 的单位换算折在骨骼链上(bind palette ≠ I),直接
|
||
// 用 raw 会让物理凸包大一个数量级(M1911 实测 10×)
|
||
if (!out.bones.empty()) {
|
||
std::vector<glm::mat4> globals(out.nodes.size());
|
||
for (size_t i = 0; i < out.nodes.size(); ++i) {
|
||
globals[i] = out.nodes[i].parent < 0
|
||
? out.nodes[i].localTransform
|
||
: globals[out.nodes[i].parent] * out.nodes[i].localTransform;
|
||
}
|
||
std::vector<glm::mat4> bindPalette(out.bones.size());
|
||
for (size_t b = 0; b < out.bones.size(); ++b) {
|
||
bindPalette[b] = globals[out.bones[b].nodeIdx]
|
||
* out.bones[b].invBind;
|
||
}
|
||
for (auto& pm : out.meshes) {
|
||
if (!pm.skinned) continue;
|
||
pm.cpuPositions.resize(pm.vertices.size());
|
||
for (size_t v = 0; v < pm.vertices.size(); ++v) {
|
||
const Vertex& vert = pm.vertices[v];
|
||
glm::vec3 p(0.0f);
|
||
f32 wsum = 0.0f;
|
||
for (int k = 0; k < 4; ++k) {
|
||
const f32 w = vert.weights[k];
|
||
if (w <= 0.0f) continue;
|
||
p += w * glm::vec3(bindPalette[vert.boneIds[k]]
|
||
* glm::vec4(vert.position, 1.0f));
|
||
wsum += w;
|
||
}
|
||
// 零权重顶点(shader 侧塌到原点)物理侧保留原始位置兜底
|
||
pm.cpuPositions[v] = wsum > 1e-6f ? p : vert.position;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
|
||
path, out.meshes.size(), out.bones.size(), out.clips.size());
|
||
return out;
|
||
}
|
||
|
||
u32 Model::FlattenNodes(const aiNode* node, i32 parent,
|
||
std::vector<ModelNode>& out)
|
||
{
|
||
const u32 idx = static_cast<u32>(out.size());
|
||
ModelNode n;
|
||
n.name = node->mName.C_Str();
|
||
n.parent = parent;
|
||
// 完整 mTransformation:FBX 的 PreRotation/单位换算都折在这里,
|
||
// 骨骼 global 累乘必须用全量矩阵,不能拆 TRS 重建
|
||
n.localTransform = ToGlm(node->mTransformation);
|
||
out.push_back(std::move(n));
|
||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||
FlattenNodes(node->mChildren[i], static_cast<i32>(idx), out);
|
||
}
|
||
return idx;
|
||
}
|
||
|
||
void Model::ParseAnimations(const aiScene* scene,
|
||
const std::vector<ModelNode>& nodes,
|
||
std::vector<AnimationClip>& out)
|
||
{
|
||
if (scene->mNumAnimations == 0) return;
|
||
|
||
// nodeName → node 表下标(channel 的目标必须能在层级里累乘)
|
||
std::unordered_map<std::string, u32> nodeByName;
|
||
nodeByName.reserve(nodes.size());
|
||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||
nodeByName.emplace(nodes[i].name, i);
|
||
}
|
||
|
||
for (u32 a = 0; a < scene->mNumAnimations; ++a) {
|
||
const aiAnimation* anim = scene->mAnimations[a];
|
||
AnimationClip clip;
|
||
clip.name = anim->mName.length > 0 ? anim->mName.C_Str()
|
||
: ("clip_" + std::to_string(a));
|
||
// tps=0 按 1(assimp 对部分导出器的惯例缺省)
|
||
clip.tps = anim->mTicksPerSecond > 0.0
|
||
? static_cast<f32>(anim->mTicksPerSecond) : 1.0f;
|
||
clip.duration = static_cast<f32>(anim->mDuration) / clip.tps;
|
||
|
||
for (u32 c = 0; c < anim->mNumChannels; ++c) {
|
||
const aiNodeAnim* ch = anim->mChannels[c];
|
||
auto it = nodeByName.find(ch->mNodeName.C_Str());
|
||
if (it == nodeByName.end()) {
|
||
ENGINE_WARN("Anim '{}': channel node '{}' not in hierarchy, skipped",
|
||
clip.name, ch->mNodeName.C_Str());
|
||
continue;
|
||
}
|
||
AnimChannel chan;
|
||
chan.nodeIdx = it->second;
|
||
// 关键帧 time 统一换算成秒(运行时采样免除法)
|
||
chan.positions.reserve(ch->mNumPositionKeys);
|
||
for (u32 k = 0; k < ch->mNumPositionKeys; ++k) {
|
||
const auto& key = ch->mPositionKeys[k];
|
||
chan.positions.push_back({
|
||
static_cast<f32>(key.mTime) / clip.tps,
|
||
{key.mValue.x, key.mValue.y, key.mValue.z}});
|
||
}
|
||
chan.rotations.reserve(ch->mNumRotationKeys);
|
||
for (u32 k = 0; k < ch->mNumRotationKeys; ++k) {
|
||
const auto& key = ch->mRotationKeys[k];
|
||
// aiQuaternion 存储序 (w,x,y,z) → glm 构造序一致
|
||
chan.rotations.push_back({
|
||
static_cast<f32>(key.mTime) / clip.tps,
|
||
glm::quat(key.mValue.w, key.mValue.x,
|
||
key.mValue.y, key.mValue.z)});
|
||
}
|
||
chan.scales.reserve(ch->mNumScalingKeys);
|
||
for (u32 k = 0; k < ch->mNumScalingKeys; ++k) {
|
||
const auto& key = ch->mScalingKeys[k];
|
||
chan.scales.push_back({
|
||
static_cast<f32>(key.mTime) / clip.tps,
|
||
{key.mValue.x, key.mValue.y, key.mValue.z}});
|
||
}
|
||
clip.channels.push_back(std::move(chan));
|
||
}
|
||
out.push_back(std::move(clip));
|
||
}
|
||
}
|
||
|
||
void Model::CollectMeshBones(const aiMesh* mesh,
|
||
std::vector<Vertex>& vertices,
|
||
std::vector<ModelBone>& bones,
|
||
std::unordered_map<std::string, u32>& boneSlots)
|
||
{
|
||
for (u32 b = 0; b < mesh->mNumBones; ++b) {
|
||
const aiBone* bone = mesh->mBones[b];
|
||
const char* name = bone->mName.C_Str();
|
||
|
||
u32 slot;
|
||
if (auto it = boneSlots.find(name); it != boneSlots.end()) {
|
||
slot = it->second; // 多 mesh 共享同一骨架(身体/附件分区)
|
||
} else {
|
||
slot = static_cast<u32>(bones.size());
|
||
boneSlots.emplace(name, slot);
|
||
// nodeIdx 由 ParseFromDisk 统一回填(node 表此时已就绪)
|
||
bones.push_back({name, 0, ToGlm(bone->mOffsetMatrix)});
|
||
}
|
||
|
||
// 顶点权重累加:LimitBoneWeights 保证每顶点 ≤4 根骨,逐槽找空位;
|
||
// 4 槽满的理论残留权重直接忽略(量化误差级)
|
||
for (u32 w = 0; w < bone->mNumWeights; ++w) {
|
||
const aiVertexWeight& vw = bone->mWeights[w];
|
||
if (vw.mVertexId >= vertices.size() || vw.mWeight <= 0.0f) continue;
|
||
Vertex& v = vertices[vw.mVertexId];
|
||
for (u32 k = 0; k < 4; ++k) {
|
||
if (v.weights[k] == 0.0f) {
|
||
v.boneIds[k] = slot;
|
||
v.weights[k] = vw.mWeight;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
||
DescriptorManager& descriptorManager,
|
||
vk::DescriptorSetLayout textureLayout,
|
||
ParsedModel&& parsed)
|
||
{
|
||
if (!parsed.Valid()) {
|
||
ENGINE_ERROR("Model::FromParsed: invalid parsed model ({})", parsed.path);
|
||
return nullptr;
|
||
}
|
||
|
||
auto model = Scope<Model>(new Model(device, allocator,
|
||
descriptorManager, textureLayout));
|
||
model->m_ModelPath = parsed.path;
|
||
model->m_IsGltf = parsed.isGltf;
|
||
model->m_Nodes = std::move(parsed.nodes);
|
||
model->m_Bones = std::move(parsed.bones);
|
||
model->m_Clips = std::move(parsed.clips);
|
||
|
||
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
|
||
for (auto& pm : parsed.meshes) {
|
||
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||
model->m_Meshes.push_back(CreateScope<Mesh>(
|
||
device, allocator, pm.vertices, pm.lods, mat,
|
||
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||
}
|
||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
|
||
model->RebuildUnionBounds();
|
||
return model;
|
||
}
|
||
|
||
void Model::LoadModel(const std::string& path)
|
||
{
|
||
// 同步路径:解析 + GPU 构建串行(与 FromParsed 相同的消费逻辑)
|
||
ParsedModel parsed = ParseFromDisk(path);
|
||
if (!parsed.Valid()) {
|
||
return;
|
||
}
|
||
m_ModelPath = parsed.path;
|
||
m_IsGltf = parsed.isGltf;
|
||
m_Nodes = std::move(parsed.nodes);
|
||
m_Bones = std::move(parsed.bones);
|
||
m_Clips = std::move(parsed.clips);
|
||
for (auto& pm : parsed.meshes) {
|
||
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||
m_Meshes.push_back(CreateScope<Mesh>(
|
||
m_Device, m_Allocator, pm.vertices, pm.lods, mat,
|
||
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||
}
|
||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||
path, m_Meshes.size(), m_TextureCache.size());
|
||
RebuildUnionBounds();
|
||
}
|
||
|
||
void Model::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,
|
||
const aiMatrix4x4& parentGlobal)
|
||
{
|
||
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
|
||
// Transform)把物体旋转(含 Z-up→Y-up)与 cm→m 单位换算全折在
|
||
// 节点变换上,忽略会让每个零件按原始局部空间乱画(朝向丢失、
|
||
// 尺度差百倍,车都不成车)
|
||
const aiMatrix4x4 global = parentGlobal * node->mTransformation;
|
||
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));
|
||
}
|
||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
|
||
bones, boneSlots, global);
|
||
}
|
||
}
|
||
|
||
Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
|
||
{
|
||
// FBX/DCC 导出的路径常带反斜杠(Windows 语义),统一成正斜杠
|
||
std::string path = rawPath;
|
||
std::replace(path.begin(), path.end(), '\\', '/');
|
||
|
||
// 缓存键带 sRGB 语义:同一路径可能同时被颜色槽与数据槽引用
|
||
std::string key = (srgb ? "s:" : "l:") + path;
|
||
auto it = m_TextureCache.find(key);
|
||
if (it != m_TextureCache.end()) {
|
||
return it->second.get();
|
||
}
|
||
|
||
// 文件不存在则用兜底纹理
|
||
if (!std::filesystem::exists(path)) {
|
||
ENGINE_WARN("Texture not found, using default: {}", path);
|
||
return nullptr;
|
||
}
|
||
|
||
auto tex = CreateScope<Texture>(m_Device, m_Allocator, path, srgb);
|
||
Texture* ptr = tex.get();
|
||
m_TextureCache.emplace(key, std::move(tex));
|
||
return ptr;
|
||
}
|
||
|
||
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
|
||
const MaterialParams& params,
|
||
bool transparent,
|
||
const std::array<Texture*, Material::kSlotCount>&
|
||
slotOverrides)
|
||
{
|
||
// 程序化覆盖纹理以指针地址入键:同模型内不同颜色的覆盖组合
|
||
//(如两块玻璃板)各得一份材质,不与纯路径键的材质串缓存
|
||
std::string key = Material::MakeCacheKey(paths, params, transparent);
|
||
bool hasOverride = false;
|
||
for (const Texture* t : slotOverrides) {
|
||
if (t) { hasOverride = true; break; }
|
||
}
|
||
if (hasOverride) {
|
||
key += "@ovr";
|
||
for (const Texture* t : slotOverrides) {
|
||
key += t ? ":" + std::to_string(reinterpret_cast<uintptr_t>(t))
|
||
: ":0";
|
||
}
|
||
}
|
||
|
||
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
|
||
return it->second.get();
|
||
}
|
||
|
||
// 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理
|
||
std::array<Texture*, Material::kSlotCount> textures{};
|
||
// 槽位 sRGB 语义:颜色图 true,数据图 false
|
||
const bool slotSrgb[Material::kSlotCount] = {true, false, false, true};
|
||
|
||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||
Texture* tex = slotOverrides[slot];
|
||
if (!tex && !paths[slot].empty()) {
|
||
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
|
||
}
|
||
textures[slot] = tex ? tex : m_DefaultTextures[slot].get();
|
||
}
|
||
|
||
auto mat = CreateScope<Material>(
|
||
m_Device, m_Allocator, m_DescriptorManager, m_TextureLayout,
|
||
textures, params, transparent);
|
||
Material* ptr = mat.get();
|
||
m_MaterialCache.emplace(key, std::move(mat));
|
||
return ptr;
|
||
}
|
||
|
||
ParsedMesh Model::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,
|
||
const aiMatrix4x4& globalTransform)
|
||
{
|
||
ParsedMesh out;
|
||
std::vector<Vertex>& vertices = out.vertices;
|
||
std::vector<u32> indices;
|
||
|
||
vertices.reserve(mesh->mNumVertices);
|
||
// 有骨骼的 mesh:顶点权重先清零再由 CollectMeshBones 灌入——
|
||
// 默认 w=(1,0,0,0) 的槽 0 若不清,palette[0] 的 identity 会以
|
||
// 权重 1 混进蒙皮矩阵(顶点永远钉在原点)
|
||
const bool hasBones = mesh->mNumBones > 0;
|
||
|
||
// 非蒙皮 mesh:烘焙节点链全局变换(旋转/单位换算折进顶点);
|
||
// 蒙皮 mesh 保持 mesh 空间——palette × offsetMatrix 的蒙皮
|
||
// 约定依赖原空间,烘焙会与动画路径打架
|
||
const bool bakeTransform = !hasBones;
|
||
const aiMatrix3x3 normalMat(globalTransform);
|
||
|
||
for (u32 i = 0; i < mesh->mNumVertices; ++i) {
|
||
Vertex vertex{};
|
||
|
||
if (bakeTransform) {
|
||
const aiVector3D p = globalTransform * mesh->mVertices[i];
|
||
vertex.position = {p.x, p.y, p.z};
|
||
} else {
|
||
vertex.position = {
|
||
mesh->mVertices[i].x,
|
||
mesh->mVertices[i].y,
|
||
mesh->mVertices[i].z
|
||
};
|
||
}
|
||
|
||
if (mesh->HasNormals()) {
|
||
const aiVector3D n = bakeTransform
|
||
? normalMat * mesh->mNormals[i]
|
||
: mesh->mNormals[i];
|
||
const glm::vec3 nn{n.x, n.y, n.z};
|
||
vertex.normal = bakeTransform ? glm::normalize(nn) : nn;
|
||
}
|
||
|
||
if (mesh->mTextureCoords[0]) {
|
||
vertex.uv = {
|
||
mesh->mTextureCoords[0][i].x,
|
||
mesh->mTextureCoords[0][i].y
|
||
};
|
||
}
|
||
|
||
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
|
||
if (mesh->mTangents) {
|
||
const aiVector3D t = bakeTransform
|
||
? normalMat * mesh->mTangents[i]
|
||
: mesh->mTangents[i];
|
||
const glm::vec3 tt{t.x, t.y, t.z};
|
||
vertex.tangent = bakeTransform ? glm::normalize(tt) : tt;
|
||
}
|
||
|
||
if (hasBones) {
|
||
vertex.weights = glm::vec4(0.0f);
|
||
}
|
||
|
||
vertices.push_back(vertex);
|
||
}
|
||
|
||
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
|
||
CollectMeshBones(mesh, vertices, bones, boneSlots);
|
||
out.skinned = hasBones;
|
||
|
||
for (u32 i = 0; i < mesh->mNumFaces; ++i) {
|
||
aiFace face = mesh->mFaces[i];
|
||
for (u32 j = 0; j < face.mNumIndices; ++j) {
|
||
indices.push_back(face.mIndices[j]);
|
||
}
|
||
}
|
||
|
||
// 读取材质纹理路径(PBR metallic-roughness 槽位)+ 金属度/粗糙度因子
|
||
std::string texPaths[Material::kSlotCount];
|
||
MaterialParams params;
|
||
|
||
if (mesh->mMaterialIndex >= 0) {
|
||
const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
||
|
||
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
|
||
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
|
||
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
|
||
// 先到先得,数组顺序即优先级。
|
||
// 注意:FBX 独立 metalness/roughness 是两张图——metal 落 MR 槽
|
||
//(灰度 R=G=B,shader 读 .b 当金属度恰好正确),roughness 落
|
||
// 独立 Roughness 槽(factors.z=1 启用),不再挤占 MR 槽的 .g
|
||
const std::pair<aiTextureType, u32> slots[] = {
|
||
{aiTextureType_DIFFUSE, Material::Albedo},
|
||
{aiTextureType_BASE_COLOR, Material::Albedo},
|
||
{aiTextureType_NORMALS, Material::Normal},
|
||
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
|
||
{aiTextureType_METALNESS, Material::MetallicRoughness},
|
||
{aiTextureType_DIFFUSE_ROUGHNESS, Material::Roughness},
|
||
{aiTextureType_SHININESS, Material::Roughness}, // OBJ map_Ns
|
||
{aiTextureType_UNKNOWN, Material::MetallicRoughness},
|
||
{aiTextureType_AMBIENT_OCCLUSION, Material::Ao},
|
||
{aiTextureType_LIGHTMAP, Material::Ao},
|
||
{aiTextureType_AMBIENT, Material::Ao},
|
||
{aiTextureType_EMISSIVE, Material::Emissive},
|
||
{aiTextureType_EMISSION_COLOR, Material::Emissive},
|
||
};
|
||
|
||
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();
|
||
}
|
||
}
|
||
// 注:不做文件名探测/自动补槽——资产缺什么就是什么;
|
||
// 补槽是将来手动材质编辑的职责,加载期保持确定性
|
||
|
||
// 材质加载可见性:贴图槽缺失/路径解析错误会静默退化成默认
|
||
// 纹理(金属全没 → 塑料感),这里留下诊断锚点
|
||
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} roughness={} "
|
||
"emissive={} ao={}",
|
||
material->GetName().C_Str(),
|
||
texPaths[Material::Albedo].empty() ? "<none>"
|
||
: texPaths[Material::Albedo],
|
||
texPaths[Material::Normal].empty() ? "<none>"
|
||
: texPaths[Material::Normal],
|
||
texPaths[Material::MetallicRoughness].empty() ? "<none>"
|
||
: texPaths[Material::MetallicRoughness],
|
||
texPaths[Material::Roughness].empty() ? "<none>"
|
||
: texPaths[Material::Roughness],
|
||
texPaths[Material::Emissive].empty() ? "<none>"
|
||
: texPaths[Material::Emissive],
|
||
texPaths[Material::Ao].empty() ? "<none>"
|
||
: texPaths[Material::Ao]);
|
||
|
||
// 因子按工作流分支:
|
||
// glTF2:MR 合图(B=金属度语义),因子按声明(缺省 1/1)
|
||
// OBJ:specular 工作流无金属概念 → metallic=0;
|
||
// roughness 有独立贴图 → 因子 1(G 通道直达);
|
||
// 无贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
|
||
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
|
||
if (isGltf) {
|
||
ai_real metallic = 1.0f, roughness = 1.0f;
|
||
material->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
|
||
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
|
||
params.factors = glm::vec4(static_cast<f32>(metallic),
|
||
static_cast<f32>(roughness), 0.0f, 0.0f);
|
||
|
||
// glTF alphaMode:BLEND = 半透明(透明绘制队列 + 深度只测不写)。
|
||
// MASK(alpha cutout)暂按不透明——需要 alpha test,后续演进。
|
||
// OBJ/FBX 无此键 → 恒不透明(资产格式定义写死)
|
||
aiString alphaMode;
|
||
if (material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode) == AI_SUCCESS) {
|
||
out.transparent = (alphaMode == aiString("BLEND"));
|
||
}
|
||
} else {
|
||
// FBX/OBJ 独立贴图工作流
|
||
const bool hasSepRough = !texPaths[Material::Roughness].empty();
|
||
f32 roughnessFactor = 1.0f;
|
||
if (!hasSepRough && texPaths[Material::MetallicRoughness].empty()) {
|
||
// 无粗糙度贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
|
||
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
|
||
ai_real shininess = 10.0f;
|
||
material->Get(AI_MATKEY_SHININESS, shininess);
|
||
const f32 rough =
|
||
std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f));
|
||
roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.0f);
|
||
}
|
||
// 金属度图落 MR 槽(灰度 .b 即金属度语义):
|
||
// 有图 metallic 因子 = 1,无图 = 0(OBJ specular 工作流无金属概念)
|
||
const f32 metallicFactor =
|
||
texPaths[Material::MetallicRoughness].empty() ? 0.0f : 1.0f;
|
||
params.factors = glm::vec4(metallicFactor, roughnessFactor,
|
||
hasSepRough ? 1.0f : 0.0f, 0.0f);
|
||
}
|
||
}
|
||
|
||
// 材质创建延迟到 GPU 构建段(FromParsed):解析期只留引用
|
||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||
out.texPaths[slot] = std::move(texPaths[slot]);
|
||
}
|
||
out.params = params;
|
||
// out.transparent 已在 glTF 分支读取
|
||
|
||
// ---- LOD 链(解析期生成,worker 线程免费时间):----
|
||
// LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享,
|
||
// 简化只重排索引(meshopt_simplify 从 LOD0 单次起点逐级向下)
|
||
out.lods.push_back(std::move(indices));
|
||
for (u32 lod = 1; lod < kMeshLodCount; ++lod) {
|
||
const size_t target = out.lods[0].size() >> lod;
|
||
// 太小的网格不简化:简化收益低于误差风险,直接复用上一级
|
||
if (target < 96 || out.lods[0].size() < 192) {
|
||
out.lods.push_back(out.lods.back());
|
||
continue;
|
||
}
|
||
std::vector<u32> dest(out.lods[0].size());
|
||
float error = 0.0f;
|
||
const size_t count = meshopt_simplify(
|
||
dest.data(),
|
||
out.lods[0].data(), out.lods[0].size(),
|
||
&out.vertices[0].position.x, out.vertices.size(), sizeof(Vertex),
|
||
target, 1e-2f, 0, &error);
|
||
if (count == 0) {
|
||
out.lods.push_back(out.lods.back()); // 简化失败兜底
|
||
continue;
|
||
}
|
||
dest.resize(count);
|
||
out.lods.push_back(std::move(dest));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
} // namespace Engine
|