feat: 材质系统 v1——4 纹理槽(albedo/normal/specular/emissive)+ 法线贴图
- Material:单贴图 → 4 槽(binding=Slot 枚举),MakeCacheKey 组合键 保持材质去重(同贴图组合共享描述符集) - Model:读取 aiTextureType DIFFUSE/NORMALS/SPECULAR/EMISSIVE 四类路径; aiProcess_CalcTangentSpace 生成切线(Vertex +tangent 属性,location 3); 每槽默认纹理兜底(白/平法线(128,128,255)/黑/黑,1x1) - 纹理 sRGB 语义按槽位:颜色图 SRGB 变体,数据图(Normal/Specular)线性 UNORM——Texture 路径加载加 srgb 参数;修复 InitFromPixels 图文 format 不一致的 validation 警告 - UBO 尾部追加 cameraPos(Blinn-Phong V 向量) - simple_shader.frag:TBN 法线贴图(无切线/平法线退化回几何法线)+ Blinn-Phong 高光(specular 图强度系数)+ 自发光直加 - SunTemple 实测:148 张 DDS 全槽位加载(96 张线性变体)、1056 mesh、 validation 零警告
This commit is contained in:
@@ -12,19 +12,31 @@
|
||||
namespace Engine
|
||||
{
|
||||
Material::Material(Device& device,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout layout,
|
||||
Texture* texture)
|
||||
: m_Texture(texture)
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout layout,
|
||||
const std::array<Texture*, kSlotCount>& textures)
|
||||
: m_Textures(textures)
|
||||
{
|
||||
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
|
||||
|
||||
descriptorManager.UpdateTexture(
|
||||
m_DescriptorSet, 0,
|
||||
m_Texture->GetSampler(),
|
||||
m_Texture->GetView());
|
||||
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||
descriptorManager.UpdateTexture(
|
||||
m_DescriptorSet, slot,
|
||||
m_Textures[slot]->GetSampler(),
|
||||
m_Textures[slot]->GetView());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Material::~Material() = default;
|
||||
|
||||
std::string Material::MakeCacheKey(const std::string paths[kSlotCount])
|
||||
{
|
||||
std::string key;
|
||||
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||
if (slot) key += '\n';
|
||||
key += paths[slot];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
} // namespace Engine
|
||||
|
||||
@@ -28,15 +28,27 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
, m_DescriptorManager(descriptorManager)
|
||||
, m_TextureLayout(textureLayout)
|
||||
{
|
||||
// 兜底纹理
|
||||
m_DefaultTexture = CreateScope<Texture>(
|
||||
m_Device, m_Allocator, "assets/textures/default.tga");
|
||||
InitDefaultTextures();
|
||||
|
||||
LoadModel(path);
|
||||
}
|
||||
|
||||
Model::~Model() = default;
|
||||
|
||||
void Model::InitDefaultTextures()
|
||||
{
|
||||
// 各材质槽位的兜底纹理(1x1,开销可忽略):
|
||||
// albedo 白(受光) / normal 平法线 (0.5,0.5,1) / specular 黑 / emissive 黑
|
||||
m_DefaultTextures[Material::Albedo] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255);
|
||||
m_DefaultTextures[Material::Normal] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 255, false);
|
||||
m_DefaultTextures[Material::Specular] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0, 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,
|
||||
@@ -46,7 +58,8 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
, m_DescriptorManager(descriptorManager)
|
||||
, m_TextureLayout(textureLayout)
|
||||
{
|
||||
// 纯色 1x1 贴图
|
||||
// 纯色 1x1 贴图(albedo 兜底)
|
||||
InitDefaultTextures();
|
||||
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
|
||||
|
||||
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
|
||||
@@ -59,9 +72,9 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
};
|
||||
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
|
||||
|
||||
// 贴图/材质生命周期随模型(缓存持有)
|
||||
m_DefaultTexture = std::move(tex);
|
||||
Material* mat = GetOrCreateMaterial(""); // 兜底材质
|
||||
// 材质/纹理生命周期随模型(缓存持有)
|
||||
std::string paths[Material::kSlotCount] = {"", "", "", ""};
|
||||
Material* mat = GetOrCreateMaterial(paths); // 全默认材质
|
||||
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, indices, mat));
|
||||
@@ -86,6 +99,7 @@ void Model::LoadModel(const std::string& path)
|
||||
aiProcess_Triangulate |
|
||||
aiProcess_FlipUVs |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||||
aiProcess_JoinIdenticalVertices);
|
||||
|
||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
||||
@@ -109,13 +123,15 @@ void Model::ProcessNode(aiNode* node, const aiScene* scene)
|
||||
}
|
||||
}
|
||||
|
||||
Texture* Model::GetOrLoadTexture(const std::string& rawPath)
|
||||
Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
|
||||
{
|
||||
// FBX/DCC 导出的路径常带反斜杠(Windows 语义),统一成正斜杠
|
||||
std::string path = rawPath;
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
|
||||
auto it = m_TextureCache.find(path);
|
||||
// 缓存键带 sRGB 语义:同一路径可能同时被颜色槽与数据槽引用
|
||||
std::string key = (srgb ? "s:" : "l:") + path;
|
||||
auto it = m_TextureCache.find(key);
|
||||
if (it != m_TextureCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
@@ -123,40 +139,39 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath)
|
||||
// 文件不存在则用兜底纹理
|
||||
if (!std::filesystem::exists(path)) {
|
||||
ENGINE_WARN("Texture not found, using default: {}", path);
|
||||
return m_DefaultTexture.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto tex = CreateScope<Texture>(m_Device, m_Allocator, path);
|
||||
auto tex = CreateScope<Texture>(m_Device, m_Allocator, path, srgb);
|
||||
Texture* ptr = tex.get();
|
||||
m_TextureCache.emplace(path, std::move(tex));
|
||||
m_TextureCache.emplace(key, std::move(tex));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
Material* Model::GetOrCreateMaterial(const std::string& rawPath)
|
||||
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount])
|
||||
{
|
||||
// 路径归一化 + 空路径/缺文件统一折到兜底材质(缓存键 "")
|
||||
std::string path = rawPath;
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
|
||||
if (path.empty() || !std::filesystem::exists(path)) {
|
||||
if (!rawPath.empty()) {
|
||||
ENGINE_WARN("Texture not found, using default: {}", path);
|
||||
}
|
||||
path.clear();
|
||||
}
|
||||
|
||||
if (auto it = m_MaterialCache.find(path); it != m_MaterialCache.end()) {
|
||||
const std::string key = Material::MakeCacheKey(paths);
|
||||
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
Texture* tex = path.empty()
|
||||
? m_DefaultTexture.get()
|
||||
: GetOrLoadTexture(path);
|
||||
// 每槽位解析纹理:空/缺失折到该槽位默认纹理
|
||||
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_DescriptorManager, m_TextureLayout, tex);
|
||||
m_Device, m_DescriptorManager, m_TextureLayout, textures);
|
||||
Material* ptr = mat.get();
|
||||
m_MaterialCache.emplace(path, std::move(mat));
|
||||
m_MaterialCache.emplace(key, std::move(mat));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
@@ -191,6 +206,15 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
||||
};
|
||||
}
|
||||
|
||||
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
|
||||
if (mesh->mTangents) {
|
||||
vertex.tangent = {
|
||||
mesh->mTangents[i].x,
|
||||
mesh->mTangents[i].y,
|
||||
mesh->mTangents[i].z
|
||||
};
|
||||
}
|
||||
|
||||
vertices.push_back(vertex);
|
||||
}
|
||||
|
||||
@@ -201,22 +225,32 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
||||
}
|
||||
}
|
||||
|
||||
// 读取材质纹理路径
|
||||
std::string texturePath;
|
||||
// 读取材质纹理路径(4 槽:albedo/normal/specular/emissive)
|
||||
std::string texPaths[Material::kSlotCount];
|
||||
|
||||
if (mesh->mMaterialIndex >= 0) {
|
||||
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
||||
const std::filesystem::path modelDir =
|
||||
std::filesystem::path(m_ModelPath).parent_path();
|
||||
|
||||
aiString path;
|
||||
if (material->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) {
|
||||
const std::filesystem::path modelDir =
|
||||
std::filesystem::path(m_ModelPath).parent_path();
|
||||
texturePath = (modelDir / path.C_Str()).string();
|
||||
const std::pair<aiTextureType, u32> slots[] = {
|
||||
{aiTextureType_DIFFUSE, Material::Albedo},
|
||||
{aiTextureType_NORMALS, Material::Normal},
|
||||
{aiTextureType_SPECULAR, Material::Specular},
|
||||
{aiTextureType_EMISSIVE, Material::Emissive},
|
||||
};
|
||||
|
||||
for (const auto& [type, slot] : slots) {
|
||||
aiString path;
|
||||
if (material->GetTexture(type, 0, &path) == AI_SUCCESS &&
|
||||
path.length > 0) {
|
||||
texPaths[slot] = (modelDir / path.C_Str()).string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 材质缓存:同贴图 → 同一 Material(一个描述符集),mesh 数 ≠ 集数
|
||||
Material* mat = GetOrCreateMaterial(texturePath);
|
||||
// 材质缓存:同贴图组合 → 同一 Material(一个描述符集),mesh 数 ≠ 集数
|
||||
Material* mat = GetOrCreateMaterial(texPaths);
|
||||
|
||||
return CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, indices, mat);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
@@ -31,10 +32,14 @@ namespace Engine::Passes
|
||||
};
|
||||
uboSetLayout = descriptors.CreateLayout(uboBindings);
|
||||
|
||||
// Set 1: texture
|
||||
std::vector<DescriptorBinding> texBindings = {
|
||||
{0, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment},
|
||||
};
|
||||
// Set 1: 材质纹理槽(binding = Material::Slot:0 albedo / 1 normal /
|
||||
// 2 specular / 3 emissive)
|
||||
std::vector<DescriptorBinding> texBindings;
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
texBindings.push_back(
|
||||
{slot, vk::DescriptorType::eCombinedImageSampler,
|
||||
vk::ShaderStageFlagBits::eFragment});
|
||||
}
|
||||
textureSetLayout = descriptors.CreateLayout(texBindings);
|
||||
|
||||
// Set 2: CSM 级联阴影图(fragment 采样,binding = 级联序号)
|
||||
|
||||
@@ -177,6 +177,7 @@ namespace Engine
|
||||
ubo.lights[i].color = glm::vec4(0.0f);
|
||||
}
|
||||
ComputeCascades(shadowDir);
|
||||
ubo.cameraPos = glm::vec4(camera.GetPosition(), 1.0f);
|
||||
|
||||
// ---- 3. GPUDriven:单趟收集 + 双视图剔除 ----
|
||||
// 每个实例对相机视锥、光源视锥分别做包围球测试:
|
||||
|
||||
Reference in New Issue
Block a user