// // Created by atdunbg on 2026/9/21. // #include #include #include #include #include #include #include #include #include #include #include // AI_MATKEY_GLTF_ALPHAMODE #include #include #include 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& rgba, bool transparent) : m_Device(device) , m_Allocator(allocator) , m_DescriptorManager(descriptorManager) , m_TextureLayout(textureLayout) { // 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质); // 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突) InitDefaultTextures(); Scope 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 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 indices = {0, 1, 2, 0, 2, 3}; const std::vector> lodIndices = {indices}; // 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo // 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训) std::string paths[Material::kSlotCount] = {"", "", "", ""}; std::array overrides{}; overrides[Material::Albedo] = albedoOverride; Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent, overrides); m_Meshes.push_back(CreateScope( m_Device, m_Allocator, vertices, lodIndices, mat)); RebuildUnionBounds(); } Scope Model::CreatePlane(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, f32 halfSize, u8 r, u8 g, u8 b) { return Scope(new Model(device, allocator, descriptorManager, textureLayout, ProceduralTag{}, halfSize, std::array{r, g, b, 255}, false)); } Scope Model::CreateTransparentPlane( Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, f32 halfSize, u8 r, u8 g, u8 b, u8 a) { return Scope(new Model(device, allocator, descriptorManager, textureLayout, ProceduralTag{}, halfSize, std::array{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 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 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 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 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& out) { const u32 idx = static_cast(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(idx), out); } return idx; } void Model::ParseAnimations(const aiScene* scene, const std::vector& nodes, std::vector& out) { if (scene->mNumAnimations == 0) return; // nodeName → node 表下标(channel 的目标必须能在层级里累乘) std::unordered_map 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(anim->mTicksPerSecond) : 1.0f; clip.duration = static_cast(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(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(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(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& vertices, std::vector& bones, std::unordered_map& 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(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::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(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( 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( 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& out, const std::filesystem::path& modelDir, bool isGltf, std::vector& bones, std::unordered_map& 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(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& 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(t)) : ":0"; } } if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) { return it->second.get(); } // 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理 std::array 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( 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& bones, std::unordered_map& boneSlots, const aiMatrix4x4& globalTransform) { ParsedMesh out; std::vector& vertices = out.vertices; std::vector 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 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() ? "" : texPaths[Material::Albedo], texPaths[Material::Normal].empty() ? "" : texPaths[Material::Normal], texPaths[Material::MetallicRoughness].empty() ? "" : texPaths[Material::MetallicRoughness], texPaths[Material::Roughness].empty() ? "" : texPaths[Material::Roughness], texPaths[Material::Emissive].empty() ? "" : texPaths[Material::Emissive], texPaths[Material::Ao].empty() ? "" : 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(metallic), static_cast(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(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 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