Files
HelloVulkan/Engine/src/Renderer/Model.cpp
T
atdunbg 9cc36af3f0 fix: 金属渲染塑料感——glTF MR 贴图未加载 + UV 翻转错误
- assimp 5.x glTF2 importer 把 MR 合图落在 METALNESS(15)/
  DIFFUSE_ROUGHNESS(16),不是 UNKNOWN(18)——此前 metallic 恒 0
  全走电介质路径。槽位查找改为优先级数组(先到先得)
- glTF 的 UV 本就是左上原点,aiProcess_FlipUVs 会把全部贴图上下
  颠倒(法线/MR 分布全反)——按扩展名区分 flip
- 材质加载 INFO 日志(贴图槽缺失会静默退化默认纹理,留诊断锚点)
2026-09-23 19:35:06 +08:00

304 lines
11 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.
//
#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 <filesystem>
#include <algorithm>
namespace Engine {
Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
const std::string& path)
: m_Device(device)
, m_Allocator(allocator)
, m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout)
{
InitDefaultTextures();
LoadModel(path);
}
Model::~Model() = default;
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);
}
Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color)
: m_Device(device)
, m_Allocator(allocator)
, m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout)
{
// 纯色 1x1 贴图(albedo 兜底)
InitDefaultTextures();
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
// 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};
// 材质/纹理生命周期随模型(缓存持有)
std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat));
}
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, 3>{r, g, b}));
}
void Model::LoadModel(const std::string& path)
{
m_ModelPath = 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;
}();
const bool 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 |
(isGltf ? 0 : aiProcess_FlipUVs) |
aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices);
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
return;
}
ProcessNode(scene->mRootNode, scene);
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
path, m_Meshes.size(), m_TextureCache.size());
}
void Model::ProcessNode(aiNode* node, const aiScene* scene)
{
for (u32 i = 0; i < node->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
m_Meshes.push_back(ProcessMesh(mesh, scene));
}
for (u32 i = 0; i < node->mNumChildren; ++i) {
ProcessNode(node->mChildren[i], scene);
}
}
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)
{
const std::string key = Material::MakeCacheKey(paths, params);
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 = nullptr;
if (!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);
Material* ptr = mat.get();
m_MaterialCache.emplace(key, std::move(mat));
return ptr;
}
Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
{
std::vector<Vertex> vertices;
std::vector<u32> indices;
vertices.reserve(mesh->mNumVertices);
for (u32 i = 0; i < mesh->mNumVertices; ++i) {
Vertex vertex{};
vertex.position = {
mesh->mVertices[i].x,
mesh->mVertices[i].y,
mesh->mVertices[i].z
};
if (mesh->HasNormals()) {
vertex.normal = {
mesh->mNormals[i].x,
mesh->mNormals[i].y,
mesh->mNormals[i].z
};
}
if (mesh->mTextureCoords[0]) {
vertex.uv = {
mesh->mTextureCoords[0][i].x,
mesh->mTextureCoords[0][i].y
};
}
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
if (mesh->mTangents) {
vertex.tangent = {
mesh->mTangents[i].x,
mesh->mTangents[i].y,
mesh->mTangents[i].z
};
}
vertices.push_back(vertex);
}
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) {
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
const std::filesystem::path modelDir =
std::filesystem::path(m_ModelPath).parent_path();
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
// 先到先得,数组顺序即优先级
const std::pair<aiTextureType, u32> slots[] = {
{aiTextureType_DIFFUSE, Material::Albedo},
{aiTextureType_BASE_COLOR, Material::Albedo},
{aiTextureType_NORMALS, Material::Normal},
{aiTextureType_METALNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::MetallicRoughness},
{aiTextureType_UNKNOWN, Material::MetallicRoughness},
{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={} emissive={}",
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::Emissive].empty() ? "<none>"
: texPaths[Material::Emissive]);
// PBR 因子(glTF2 材质属性;旧资产无此键 → 默认电介质 0/0.5)
ai_real metallic = 0.0f, roughness = 0.5f;
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);
}
// 材质缓存:同贴图组合+因子 → 同一 Material(一个描述符集)
Material* mat = GetOrCreateMaterial(texPaths, params);
return CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat);
}
} // namespace Engine