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:
@@ -1,12 +1,22 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/21.
|
// Created by atdunbg on 2026/9/21.
|
||||||
//
|
//
|
||||||
|
// Material:着色材质 = 一组纹理槽 + 一个描述符集。
|
||||||
|
//
|
||||||
|
// 槽位(binding = 槽位序号,与 simple_shader.frag 契约一致):
|
||||||
|
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
|
||||||
|
// 2 specular(高光强度/颜色,线性) 3 emissive(自发光,sRGB)
|
||||||
|
//
|
||||||
|
// 纹理非拥有(Model 的纹理缓存持有);空槽由调用方传默认纹理
|
||||||
|
//(白/平法线/黑/黑,Model 持有),shader 永远可以安全采样。
|
||||||
|
//
|
||||||
|
|
||||||
#ifndef HELLOVULKAN_MATERIAL_H
|
#ifndef HELLOVULKAN_MATERIAL_H
|
||||||
#define HELLOVULKAN_MATERIAL_H
|
#define HELLOVULKAN_MATERIAL_H
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace Engine
|
namespace Engine
|
||||||
@@ -18,20 +28,32 @@ namespace Engine
|
|||||||
class Material
|
class Material
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// 槽位序号(与 binding 对齐)
|
||||||
|
enum Slot : u32 {
|
||||||
|
Albedo = 0,
|
||||||
|
Normal = 1,
|
||||||
|
Specular = 2,
|
||||||
|
Emissive = 3,
|
||||||
|
kSlotCount = 4,
|
||||||
|
};
|
||||||
|
|
||||||
Material(Device& device,
|
Material(Device& device,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout layout,
|
vk::DescriptorSetLayout layout,
|
||||||
Texture* texture);
|
const std::array<Texture*, kSlotCount>& textures);
|
||||||
~Material();
|
~Material();
|
||||||
|
|
||||||
Material(const Material&) = delete;
|
Material(const Material&) = delete;
|
||||||
Material& operator=(const Material&) = delete;
|
Material& operator=(const Material&) = delete;
|
||||||
|
|
||||||
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
|
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
|
||||||
Texture* GetTexture() const { return m_Texture; }
|
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
|
||||||
|
|
||||||
|
// 材质缓存键:同贴图组合共享一份(Model 层使用)
|
||||||
|
static std::string MakeCacheKey(const std::string paths[kSlotCount]);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Texture* m_Texture = nullptr;
|
std::array<Texture*, kSlotCount> m_Textures{};
|
||||||
vk::DescriptorSet m_DescriptorSet;
|
vk::DescriptorSet m_DescriptorSet;
|
||||||
};
|
};
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Renderer/Material.h>
|
||||||
#include <Engine/Renderer/Mesh.h>
|
#include <Engine/Renderer/Mesh.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -59,12 +60,14 @@ namespace Engine
|
|||||||
void LoadModel(const std::string& path);
|
void LoadModel(const std::string& path);
|
||||||
void ProcessNode(aiNode* node, const aiScene* scene);
|
void ProcessNode(aiNode* node, const aiScene* scene);
|
||||||
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
|
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
|
||||||
|
void InitDefaultTextures();
|
||||||
|
|
||||||
Texture* GetOrLoadTexture(const std::string& path);
|
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||||||
|
|
||||||
// 材质缓存入口:同贴图路径共享同一 Material(一个描述符集),
|
// 材质缓存入口:同贴图组合共享同一 Material(一个描述符集),
|
||||||
// 空路径/缺文件折到兜底材质。路径已做反斜杠归一化。
|
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
|
||||||
Material* GetOrCreateMaterial(const std::string& path);
|
// paths 下标 = Material::Slot(albedo/normal/specular/emissive)
|
||||||
|
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount]);
|
||||||
|
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
MemoryAllocator& m_Allocator;
|
MemoryAllocator& m_Allocator;
|
||||||
@@ -75,7 +78,8 @@ namespace Engine
|
|||||||
// 析构序 = meshes 先于材质/纹理缓存
|
// 析构序 = meshes 先于材质/纹理缓存
|
||||||
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
|
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
|
||||||
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
||||||
Scope<Texture> m_DefaultTexture;
|
// 各槽位默认纹理(材质缺图时兜底):白 / 平法线 / 黑高光 / 黑自发光
|
||||||
|
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
||||||
|
|
||||||
std::vector<Scope<Mesh>> m_Meshes;
|
std::vector<Scope<Mesh>> m_Meshes;
|
||||||
std::string m_ModelPath;
|
std::string m_ModelPath;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ namespace Engine {
|
|||||||
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
|
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
|
||||||
glm::mat4 prevView; // 上一帧视图(velocity)
|
glm::mat4 prevView; // 上一帧视图(velocity)
|
||||||
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
|
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
|
||||||
|
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
|
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Engine {
|
|||||||
glm::vec3 position;
|
glm::vec3 position;
|
||||||
glm::vec3 normal;
|
glm::vec3 normal;
|
||||||
glm::vec2 uv;
|
glm::vec2 uv;
|
||||||
|
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
|
||||||
|
|
||||||
static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
|
static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
|
||||||
vk::VertexInputBindingDescription2EXT binding;
|
vk::VertexInputBindingDescription2EXT binding;
|
||||||
@@ -24,8 +25,8 @@ namespace Engine {
|
|||||||
return binding;
|
return binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::array<vk::VertexInputAttributeDescription2EXT, 3> GetAttributeDescriptions() {
|
static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() {
|
||||||
std::array<vk::VertexInputAttributeDescription2EXT, 3> attrs;
|
std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs;
|
||||||
|
|
||||||
attrs[0].setLocation(0)
|
attrs[0].setLocation(0)
|
||||||
.setBinding(0)
|
.setBinding(0)
|
||||||
@@ -42,6 +43,11 @@ namespace Engine {
|
|||||||
.setFormat(vk::Format::eR32G32Sfloat)
|
.setFormat(vk::Format::eR32G32Sfloat)
|
||||||
.setOffset(offsetof(Vertex, uv));
|
.setOffset(offsetof(Vertex, uv));
|
||||||
|
|
||||||
|
attrs[3].setLocation(3)
|
||||||
|
.setBinding(0)
|
||||||
|
.setFormat(vk::Format::eR32G32B32Sfloat)
|
||||||
|
.setOffset(offsetof(Vertex, tangent));
|
||||||
|
|
||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ namespace Engine {
|
|||||||
|
|
||||||
class Texture {
|
class Texture {
|
||||||
public:
|
public:
|
||||||
// 从磁盘图片加载(按扩展名分发:.dds → DdsLoader,其它 → stbi)
|
// 从磁盘图片加载(按扩展名分发:.dds → DdsLoader,其它 → stbi)。
|
||||||
Texture(Device& device, MemoryAllocator& allocator, const std::string& path);
|
// srgb:颜色贴图语义(BaseColor/Emissive)为 true;数据图
|
||||||
|
//(Normal/Specular 等)为 false —— 决定 vk 格式的 _SRGB 变体
|
||||||
|
Texture(Device& device, MemoryAllocator& allocator, const std::string& path,
|
||||||
|
bool srgb = true);
|
||||||
|
|
||||||
// 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景
|
// 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景
|
||||||
struct ProceduralTag {};
|
struct ProceduralTag {};
|
||||||
static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator,
|
static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator,
|
||||||
u8 r, u8 g, u8 b);
|
u8 r, u8 g, u8 b, bool srgb = true);
|
||||||
|
|
||||||
~Texture();
|
~Texture();
|
||||||
|
|
||||||
@@ -46,13 +49,13 @@ namespace Engine {
|
|||||||
private:
|
private:
|
||||||
// 两段式构造:先确定像素来源(磁盘 / 程序化),再走公共上传路径
|
// 两段式构造:先确定像素来源(磁盘 / 程序化),再走公共上传路径
|
||||||
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
||||||
const std::array<u8, 4>& rgba);
|
const std::array<u8, 4>& rgba, bool srgb);
|
||||||
|
|
||||||
// 公共路径:staging → device local image → view/sampler(RGBA8 单 mip)
|
// 公共路径:staging → device local image → view/sampler(RGBA8 单 mip)
|
||||||
void InitFromPixels(const void* pixels, u32 width, u32 height);
|
void InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb);
|
||||||
|
|
||||||
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
|
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
|
||||||
void InitFromDds(const std::string& path);
|
void InitFromDds(const std::string& path, bool srgb);
|
||||||
|
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
vk::Image m_Image;
|
vk::Image m_Image;
|
||||||
|
|||||||
@@ -14,17 +14,29 @@ namespace Engine
|
|||||||
Material::Material(Device& device,
|
Material::Material(Device& device,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout layout,
|
vk::DescriptorSetLayout layout,
|
||||||
Texture* texture)
|
const std::array<Texture*, kSlotCount>& textures)
|
||||||
: m_Texture(texture)
|
: m_Textures(textures)
|
||||||
{
|
{
|
||||||
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
|
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
|
||||||
|
|
||||||
|
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||||
descriptorManager.UpdateTexture(
|
descriptorManager.UpdateTexture(
|
||||||
m_DescriptorSet, 0,
|
m_DescriptorSet, slot,
|
||||||
m_Texture->GetSampler(),
|
m_Textures[slot]->GetSampler(),
|
||||||
m_Texture->GetView());
|
m_Textures[slot]->GetView());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Material::~Material() = default;
|
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
|
} // namespace Engine
|
||||||
|
|||||||
@@ -28,15 +28,27 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
|||||||
, m_DescriptorManager(descriptorManager)
|
, m_DescriptorManager(descriptorManager)
|
||||||
, m_TextureLayout(textureLayout)
|
, m_TextureLayout(textureLayout)
|
||||||
{
|
{
|
||||||
// 兜底纹理
|
InitDefaultTextures();
|
||||||
m_DefaultTexture = CreateScope<Texture>(
|
|
||||||
m_Device, m_Allocator, "assets/textures/default.tga");
|
|
||||||
|
|
||||||
LoadModel(path);
|
LoadModel(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Model::~Model() = default;
|
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,
|
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout textureLayout,
|
vk::DescriptorSetLayout textureLayout,
|
||||||
@@ -46,7 +58,8 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
|||||||
, m_DescriptorManager(descriptorManager)
|
, m_DescriptorManager(descriptorManager)
|
||||||
, m_TextureLayout(textureLayout)
|
, m_TextureLayout(textureLayout)
|
||||||
{
|
{
|
||||||
// 纯色 1x1 贴图
|
// 纯色 1x1 贴图(albedo 兜底)
|
||||||
|
InitDefaultTextures();
|
||||||
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
|
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
|
||||||
|
|
||||||
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
|
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
|
||||||
@@ -59,9 +72,9 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
|||||||
};
|
};
|
||||||
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
|
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
|
||||||
|
|
||||||
// 贴图/材质生命周期随模型(缓存持有)
|
// 材质/纹理生命周期随模型(缓存持有)
|
||||||
m_DefaultTexture = std::move(tex);
|
std::string paths[Material::kSlotCount] = {"", "", "", ""};
|
||||||
Material* mat = GetOrCreateMaterial(""); // 兜底材质
|
Material* mat = GetOrCreateMaterial(paths); // 全默认材质
|
||||||
|
|
||||||
m_Meshes.push_back(CreateScope<Mesh>(
|
m_Meshes.push_back(CreateScope<Mesh>(
|
||||||
m_Device, m_Allocator, vertices, indices, mat));
|
m_Device, m_Allocator, vertices, indices, mat));
|
||||||
@@ -86,6 +99,7 @@ void Model::LoadModel(const std::string& path)
|
|||||||
aiProcess_Triangulate |
|
aiProcess_Triangulate |
|
||||||
aiProcess_FlipUVs |
|
aiProcess_FlipUVs |
|
||||||
aiProcess_GenSmoothNormals |
|
aiProcess_GenSmoothNormals |
|
||||||
|
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||||||
aiProcess_JoinIdenticalVertices);
|
aiProcess_JoinIdenticalVertices);
|
||||||
|
|
||||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
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 语义),统一成正斜杠
|
// FBX/DCC 导出的路径常带反斜杠(Windows 语义),统一成正斜杠
|
||||||
std::string path = rawPath;
|
std::string path = rawPath;
|
||||||
std::replace(path.begin(), path.end(), '\\', '/');
|
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()) {
|
if (it != m_TextureCache.end()) {
|
||||||
return it->second.get();
|
return it->second.get();
|
||||||
}
|
}
|
||||||
@@ -123,40 +139,39 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath)
|
|||||||
// 文件不存在则用兜底纹理
|
// 文件不存在则用兜底纹理
|
||||||
if (!std::filesystem::exists(path)) {
|
if (!std::filesystem::exists(path)) {
|
||||||
ENGINE_WARN("Texture not found, using default: {}", 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();
|
Texture* ptr = tex.get();
|
||||||
m_TextureCache.emplace(path, std::move(tex));
|
m_TextureCache.emplace(key, std::move(tex));
|
||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Material* Model::GetOrCreateMaterial(const std::string& rawPath)
|
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount])
|
||||||
{
|
{
|
||||||
// 路径归一化 + 空路径/缺文件统一折到兜底材质(缓存键 "")
|
const std::string key = Material::MakeCacheKey(paths);
|
||||||
std::string path = rawPath;
|
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
|
||||||
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()) {
|
|
||||||
return it->second.get();
|
return it->second.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture* tex = path.empty()
|
// 每槽位解析纹理:空/缺失折到该槽位默认纹理
|
||||||
? m_DefaultTexture.get()
|
std::array<Texture*, Material::kSlotCount> textures{};
|
||||||
: GetOrLoadTexture(path);
|
// 槽位 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>(
|
auto mat = CreateScope<Material>(
|
||||||
m_Device, m_DescriptorManager, m_TextureLayout, tex);
|
m_Device, m_DescriptorManager, m_TextureLayout, textures);
|
||||||
Material* ptr = mat.get();
|
Material* ptr = mat.get();
|
||||||
m_MaterialCache.emplace(path, std::move(mat));
|
m_MaterialCache.emplace(key, std::move(mat));
|
||||||
return ptr;
|
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);
|
vertices.push_back(vertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,22 +225,32 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取材质纹理路径
|
// 读取材质纹理路径(4 槽:albedo/normal/specular/emissive)
|
||||||
std::string texturePath;
|
std::string texPaths[Material::kSlotCount];
|
||||||
|
|
||||||
if (mesh->mMaterialIndex >= 0) {
|
if (mesh->mMaterialIndex >= 0) {
|
||||||
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
||||||
|
|
||||||
aiString path;
|
|
||||||
if (material->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) {
|
|
||||||
const std::filesystem::path modelDir =
|
const std::filesystem::path modelDir =
|
||||||
std::filesystem::path(m_ModelPath).parent_path();
|
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(一个描述符集),mesh 数 ≠ 集数
|
||||||
Material* mat = GetOrCreateMaterial(texturePath);
|
Material* mat = GetOrCreateMaterial(texPaths);
|
||||||
|
|
||||||
return CreateScope<Mesh>(
|
return CreateScope<Mesh>(
|
||||||
m_Device, m_Allocator, vertices, indices, mat);
|
m_Device, m_Allocator, vertices, indices, mat);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||||
|
#include <Engine/Renderer/Material.h>
|
||||||
#include <Engine/Vulkan/Device.h>
|
#include <Engine/Vulkan/Device.h>
|
||||||
#include <Engine/Vulkan/DescriptorManager.h>
|
#include <Engine/Vulkan/DescriptorManager.h>
|
||||||
#include <Engine/Vulkan/ShaderManager.h>
|
#include <Engine/Vulkan/ShaderManager.h>
|
||||||
@@ -31,10 +32,14 @@ namespace Engine::Passes
|
|||||||
};
|
};
|
||||||
uboSetLayout = descriptors.CreateLayout(uboBindings);
|
uboSetLayout = descriptors.CreateLayout(uboBindings);
|
||||||
|
|
||||||
// Set 1: texture
|
// Set 1: 材质纹理槽(binding = Material::Slot:0 albedo / 1 normal /
|
||||||
std::vector<DescriptorBinding> texBindings = {
|
// 2 specular / 3 emissive)
|
||||||
{0, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment},
|
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);
|
textureSetLayout = descriptors.CreateLayout(texBindings);
|
||||||
|
|
||||||
// Set 2: CSM 级联阴影图(fragment 采样,binding = 级联序号)
|
// Set 2: CSM 级联阴影图(fragment 采样,binding = 级联序号)
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ namespace Engine
|
|||||||
ubo.lights[i].color = glm::vec4(0.0f);
|
ubo.lights[i].color = glm::vec4(0.0f);
|
||||||
}
|
}
|
||||||
ComputeCascades(shadowDir);
|
ComputeCascades(shadowDir);
|
||||||
|
ubo.cameraPos = glm::vec4(camera.GetPosition(), 1.0f);
|
||||||
|
|
||||||
// ---- 3. GPUDriven:单趟收集 + 双视图剔除 ----
|
// ---- 3. GPUDriven:单趟收集 + 双视图剔除 ----
|
||||||
// 每个实例对相机视锥、光源视锥分别做包围球测试:
|
// 每个实例对相机视锥、光源视锥分别做包围球测试:
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
|
|
||||||
namespace Engine
|
namespace Engine
|
||||||
{
|
{
|
||||||
Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path)
|
Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path,
|
||||||
|
bool srgb)
|
||||||
: m_Device(device) , m_Allocator(allocator)
|
: m_Device(device) , m_Allocator(allocator)
|
||||||
{
|
{
|
||||||
// 按扩展名分发:DDS(BC 压缩直传 + mip 链)vs stbi(无压缩单 mip)
|
// 按扩展名分发:DDS(BC 压缩直传 + mip 链)vs stbi(无压缩单 mip)
|
||||||
@@ -28,7 +29,7 @@ namespace Engine
|
|||||||
for (char c : ext) lower += static_cast<char>(std::tolower(c));
|
for (char c : ext) lower += static_cast<char>(std::tolower(c));
|
||||||
|
|
||||||
if (lower == ".dds") {
|
if (lower == ".dds") {
|
||||||
InitFromDds(path);
|
InitFromDds(path, srgb);
|
||||||
} else {
|
} else {
|
||||||
// 1. CPU 加载图片
|
// 1. CPU 加载图片
|
||||||
int width = 0, height = 0, channels = 0;
|
int width = 0, height = 0, channels = 0;
|
||||||
@@ -40,7 +41,7 @@ namespace Engine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InitFromPixels(pixels, static_cast<u32>(width), static_cast<u32>(height));
|
InitFromPixels(pixels, static_cast<u32>(width), static_cast<u32>(height), srgb);
|
||||||
|
|
||||||
stbi_image_free(pixels);
|
stbi_image_free(pixels);
|
||||||
|
|
||||||
@@ -49,24 +50,25 @@ namespace Engine
|
|||||||
}
|
}
|
||||||
|
|
||||||
Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
|
||||||
const std::array<u8, 4>& rgba)
|
const std::array<u8, 4>& rgba, bool srgb)
|
||||||
: m_Device(device) , m_Allocator(allocator)
|
: m_Device(device) , m_Allocator(allocator)
|
||||||
{
|
{
|
||||||
const u8 pixels[4] = { rgba[0], rgba[1], rgba[2], rgba[3] };
|
const u8 pixels[4] = { rgba[0], rgba[1], rgba[2], rgba[3] };
|
||||||
InitFromPixels(pixels, 1, 1);
|
InitFromPixels(pixels, 1, 1, srgb);
|
||||||
}
|
}
|
||||||
|
|
||||||
Scope<Texture> Texture::CreateSolidColor(Device& device, MemoryAllocator& allocator,
|
Scope<Texture> Texture::CreateSolidColor(Device& device, MemoryAllocator& allocator,
|
||||||
u8 r, u8 g, u8 b)
|
u8 r, u8 g, u8 b, bool srgb)
|
||||||
{
|
{
|
||||||
return Scope<Texture>(new Texture(device, allocator, ProceduralTag{}, {r, g, b, 255}));
|
return Scope<Texture>(new Texture(device, allocator, ProceduralTag{},
|
||||||
|
{r, g, b, 255}, srgb));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height)
|
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
||||||
{
|
{
|
||||||
m_Width = width;
|
m_Width = width;
|
||||||
m_Height = height;
|
m_Height = height;
|
||||||
m_Format = vk::Format::eR8G8B8A8Srgb;
|
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
|
||||||
m_MipLevels = 1;
|
m_MipLevels = 1;
|
||||||
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
|
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
|
||||||
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
|
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
|
||||||
@@ -84,7 +86,7 @@ namespace Engine
|
|||||||
// 4. 创建 Device Local Image
|
// 4. 创建 Device Local Image
|
||||||
vk::ImageCreateInfo imageInfo;
|
vk::ImageCreateInfo imageInfo;
|
||||||
imageInfo.setImageType(vk::ImageType::e2D)
|
imageInfo.setImageType(vk::ImageType::e2D)
|
||||||
.setFormat(vk::Format::eR8G8B8A8Srgb)
|
.setFormat(m_Format)
|
||||||
.setExtent({m_Width, m_Height, 1})
|
.setExtent({m_Width, m_Height, 1})
|
||||||
.setMipLevels(1)
|
.setMipLevels(1)
|
||||||
.setArrayLayers(1)
|
.setArrayLayers(1)
|
||||||
@@ -214,21 +216,28 @@ namespace Engine
|
|||||||
STATS.AddTexture(m_TextureBytes);
|
STATS.AddTexture(m_TextureBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DDS → Vulkan 格式映射(渲染侧职责:sRGB 语义按颜色贴图默认开启;
|
// DDS → Vulkan 格式映射。sRGB 语义由贴图槽位决定(渲染侧职责):
|
||||||
// BC4/BC5/BC6H 是数据图,保持线性)
|
// 颜色图(BaseColor/Emissive)→ SRGB 变体
|
||||||
vk::Format ToVkFormat(Assets::Dds::DdsFormat f)
|
// 数据图(Normal/Specular 等线性槽位)→ UNORM
|
||||||
|
vk::Format ToVkFormat(Assets::Dds::DdsFormat f, bool srgb)
|
||||||
{
|
{
|
||||||
using DF = Assets::Dds::DdsFormat;
|
using DF = Assets::Dds::DdsFormat;
|
||||||
switch (f) {
|
switch (f) {
|
||||||
case DF::BC1: return vk::Format::eBc1RgbaSrgbBlock;
|
case DF::BC1: return srgb ? vk::Format::eBc1RgbaSrgbBlock
|
||||||
case DF::BC2: return vk::Format::eBc2SrgbBlock;
|
: vk::Format::eBc1RgbaUnormBlock;
|
||||||
case DF::BC3: return vk::Format::eBc3SrgbBlock;
|
case DF::BC2: return srgb ? vk::Format::eBc2SrgbBlock
|
||||||
|
: vk::Format::eBc2UnormBlock;
|
||||||
|
case DF::BC3: return srgb ? vk::Format::eBc3SrgbBlock
|
||||||
|
: vk::Format::eBc3UnormBlock;
|
||||||
case DF::BC4: return vk::Format::eBc4UnormBlock;
|
case DF::BC4: return vk::Format::eBc4UnormBlock;
|
||||||
case DF::BC5: return vk::Format::eBc5UnormBlock;
|
case DF::BC5: return vk::Format::eBc5UnormBlock;
|
||||||
case DF::BC6H: return vk::Format::eBc6HUfloatBlock;
|
case DF::BC6H: return vk::Format::eBc6HUfloatBlock;
|
||||||
case DF::BC7: return vk::Format::eBc7SrgbBlock;
|
case DF::BC7: return srgb ? vk::Format::eBc7SrgbBlock
|
||||||
case DF::RGBA8: return vk::Format::eR8G8B8A8Srgb;
|
: vk::Format::eBc7UnormBlock;
|
||||||
case DF::BGRA8: return vk::Format::eB8G8R8A8Srgb;
|
case DF::RGBA8: return srgb ? vk::Format::eR8G8B8A8Srgb
|
||||||
|
: vk::Format::eR8G8B8A8Unorm;
|
||||||
|
case DF::BGRA8: return srgb ? vk::Format::eB8G8R8A8Srgb
|
||||||
|
: vk::Format::eB8G8R8A8Unorm;
|
||||||
}
|
}
|
||||||
return vk::Format::eUndefined;
|
return vk::Format::eUndefined;
|
||||||
}
|
}
|
||||||
@@ -238,7 +247,7 @@ namespace Engine
|
|||||||
return std::max(1u, v >> level);
|
return std::max(1u, v >> level);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Texture::InitFromDds(const std::string& path)
|
void Texture::InitFromDds(const std::string& path, bool srgb)
|
||||||
{
|
{
|
||||||
Assets::Dds::DdsData data;
|
Assets::Dds::DdsData data;
|
||||||
std::string err;
|
std::string err;
|
||||||
@@ -250,7 +259,7 @@ namespace Engine
|
|||||||
m_Width = data.Width;
|
m_Width = data.Width;
|
||||||
m_Height = data.Height;
|
m_Height = data.Height;
|
||||||
m_MipLevels = data.MipLevels;
|
m_MipLevels = data.MipLevels;
|
||||||
m_Format = ToVkFormat(data.Format);
|
m_Format = ToVkFormat(data.Format, srgb);
|
||||||
|
|
||||||
ENGINE_INFO("DDS loaded: {} ({}x{}, {} mips, {})", path,
|
ENGINE_INFO("DDS loaded: {} ({}x{}, {} mips, {})", path,
|
||||||
m_Width, m_Height, m_MipLevels, vk::to_string(m_Format));
|
m_Width, m_Height, m_MipLevels, vk::to_string(m_Format));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ layout(location = 1) in vec3 fragNormal;
|
|||||||
layout(location = 2) in vec3 fragWorldPos;
|
layout(location = 2) in vec3 fragWorldPos;
|
||||||
layout(location = 3) in float fragViewDepth;
|
layout(location = 3) in float fragViewDepth;
|
||||||
layout(location = 4) in vec2 fragVelocity;
|
layout(location = 4) in vec2 fragVelocity;
|
||||||
|
layout(location = 5) in vec3 fragTangent;
|
||||||
|
|
||||||
layout(location = 0) out vec4 outColor;
|
layout(location = 0) out vec4 outColor;
|
||||||
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
|
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
|
||||||
@@ -26,9 +27,15 @@ layout(set = 0, binding = 0) uniform UniformBufferObject {
|
|||||||
mat4 projNoJitter;
|
mat4 projNoJitter;
|
||||||
mat4 prevView;
|
mat4 prevView;
|
||||||
mat4 prevProj;
|
mat4 prevProj;
|
||||||
|
vec4 cameraPos;
|
||||||
} ubo;
|
} ubo;
|
||||||
|
|
||||||
layout(set = 1, binding = 0) uniform sampler2D texSampler;
|
// 材质纹理槽(binding = Material::Slot)
|
||||||
|
layout(set = 1, binding = 0) uniform sampler2D albedoMap;
|
||||||
|
layout(set = 1, binding = 1) uniform sampler2D normalMap;
|
||||||
|
layout(set = 1, binding = 2) uniform sampler2D specularMap;
|
||||||
|
layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
|
||||||
|
|
||||||
layout(set = 2, binding = 0) uniform sampler2D shadowMap0; // CSM 级联 0(最近)
|
layout(set = 2, binding = 0) uniform sampler2D shadowMap0; // CSM 级联 0(最近)
|
||||||
layout(set = 2, binding = 1) uniform sampler2D shadowMap1; // 级联 1
|
layout(set = 2, binding = 1) uniform sampler2D shadowMap1; // 级联 1
|
||||||
layout(set = 2, binding = 2) uniform sampler2D shadowMap2; // 级联 2(最远)
|
layout(set = 2, binding = 2) uniform sampler2D shadowMap2; // 级联 2(最远)
|
||||||
@@ -60,19 +67,45 @@ float ShadowFactor(vec3 N, float viewDepth) {
|
|||||||
return (proj.z - bias > closest) ? 0.3 : 1.0;
|
return (proj.z - bias > closest) ? 0.3 : 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 法线贴图 → 世界空间法线(TBN 构建)。
|
||||||
|
// 无切线数据(零向量)或平法线贴图时退回几何法线。
|
||||||
|
vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
|
||||||
|
if (dot(T, T) < 1e-4) return geomN;
|
||||||
|
|
||||||
|
vec3 N = geomN;
|
||||||
|
vec3 nT = normalize(T - N * dot(N, T)); // 正交化
|
||||||
|
vec3 B = cross(N, nT);
|
||||||
|
vec3 sampled = texture(normalMap, uv).xyz * 2.0 - 1.0;
|
||||||
|
if (dot(sampled, sampled) < 1e-4) return geomN;
|
||||||
|
|
||||||
|
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 albedo = texture(texSampler, fragUV).rgb;
|
vec3 albedo = texture(albedoMap, fragUV).rgb;
|
||||||
vec3 N = normalize(fragNormal);
|
float specIntensity = texture(specularMap, fragUV).r;
|
||||||
|
vec3 emissive = texture(emissiveMap, fragUV).rgb;
|
||||||
|
|
||||||
|
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
|
||||||
|
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
|
||||||
|
|
||||||
vec3 result = ubo.ambientColor.rgb * albedo;
|
vec3 result = ubo.ambientColor.rgb * albedo;
|
||||||
|
|
||||||
for (int i = 0; i < MAX_LIGHTS; ++i) {
|
for (int i = 0; i < MAX_LIGHTS; ++i) {
|
||||||
vec3 L = normalize(-ubo.lights[i].direction.xyz);
|
vec3 L = normalize(-ubo.lights[i].direction.xyz);
|
||||||
float diff = max(dot(N, L), 0.0);
|
float diff = max(dot(N, L), 0.0);
|
||||||
|
// Blinn-Phong 高光(半程向量;specular 图做强度系数)
|
||||||
|
vec3 H = normalize(L + V);
|
||||||
|
float spec = pow(max(dot(N, H), 0.0), 32.0) * specIntensity;
|
||||||
|
|
||||||
float shade = (i == 0) ? ShadowFactor(N, fragViewDepth) : 1.0;
|
float shade = (i == 0) ? ShadowFactor(N, fragViewDepth) : 1.0;
|
||||||
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a * diff * albedo * shade;
|
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a *
|
||||||
|
(albedo * diff + vec3(spec)) * shade;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自发光不经光照/阴影直接叠加
|
||||||
|
result += emissive;
|
||||||
|
|
||||||
outColor = vec4(result, 1.0);
|
outColor = vec4(result, 1.0);
|
||||||
outVelocity = fragVelocity;
|
outVelocity = fragVelocity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ struct Light {
|
|||||||
layout(location = 0) in vec3 inPosition;
|
layout(location = 0) in vec3 inPosition;
|
||||||
layout(location = 1) in vec3 inNormal;
|
layout(location = 1) in vec3 inNormal;
|
||||||
layout(location = 2) in vec2 inUV;
|
layout(location = 2) in vec2 inUV;
|
||||||
|
layout(location = 3) in vec3 inTangent;
|
||||||
|
|
||||||
layout(location = 0) out vec2 fragUV;
|
layout(location = 0) out vec2 fragUV;
|
||||||
layout(location = 1) out vec3 fragNormal;
|
layout(location = 1) out vec3 fragNormal;
|
||||||
layout(location = 2) out vec3 fragWorldPos;
|
layout(location = 2) out vec3 fragWorldPos;
|
||||||
layout(location = 3) out float fragViewDepth; // CSM:视图深度(级联选择)
|
layout(location = 3) out float fragViewDepth; // CSM:视图深度(级联选择)
|
||||||
layout(location = 4) out vec2 fragVelocity; // TAA:clip 空间运动向量
|
layout(location = 4) out vec2 fragVelocity; // TAA:clip 空间运动向量
|
||||||
|
layout(location = 5) out vec3 fragTangent; // 世界空间切线(法线贴图)
|
||||||
|
|
||||||
// GPUDriven:模型矩阵从实例缓冲按实例索引取。
|
// GPUDriven:模型矩阵从实例缓冲按实例索引取。
|
||||||
// 同一 mesh 的 N 个实体 = 1 次 drawIndexed(instanceCount=N),
|
// 同一 mesh 的 N 个实体 = 1 次 drawIndexed(instanceCount=N),
|
||||||
@@ -39,6 +41,7 @@ layout(set = 0, binding = 0) uniform UniformBufferObject {
|
|||||||
mat4 projNoJitter;
|
mat4 projNoJitter;
|
||||||
mat4 prevView;
|
mat4 prevView;
|
||||||
mat4 prevProj;
|
mat4 prevProj;
|
||||||
|
vec4 cameraPos;
|
||||||
} ubo;
|
} ubo;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -60,6 +63,10 @@ void main() {
|
|||||||
|
|
||||||
mat3 normalMatrix = mat3(transpose(inverse(model)));
|
mat3 normalMatrix = mat3(transpose(inverse(model)));
|
||||||
fragNormal = normalize(normalMatrix * inNormal);
|
fragNormal = normalize(normalMatrix * inNormal);
|
||||||
|
// 切线随模型变换(法线贴图 TBN;无切线数据保持零,frag 侧退回几何法线)
|
||||||
|
fragTangent = normalize(mat3(model) * inTangent);
|
||||||
|
// 零向量 normalize 产生 NaN,这里直接透传零,frag 判长度
|
||||||
|
fragTangent = dot(inTangent, inTangent) > 0.0 ? fragTangent : vec3(0.0);
|
||||||
|
|
||||||
fragUV = inUV;
|
fragUV = inUV;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user