Compare commits

..
11 Commits
Author SHA1 Message Date
atdunbg 960bdf2662 refactor: 移除贴图文件名探测——资产读取保持确定性
按用户决定:OBJ/FBX 按材质定义(MTL/assimp 槽位)写死读取,
不做文件名猜测补槽;贴图缺失走默认纹理 + INFO 日志可见。
补槽职责归将来手动材质编辑接口。
2026-09-23 19:51:03 +08:00
atdunbg 2b875c9284 feat: OBJ 全量贴图接入——Material 加 AO 槽 + OBJ 工作流适配
- Material 槽位 4→5(+AO),参数 UBO binding 4→5;shader ambient
  diffuse 项乘 ao(IBL 遮蔽),AO 默认纹理白色
- OBJ 槽落点补全:map_Bump→HEIGHT(5)、map_Ns→SHININESS(7)、
  map_Ka→AMBIENT(3)(assimp OBJ importer 约定)——backpack 的
  法线贴图此前也从未加载
- MTL 导出遗漏(roughness.jpg/ao.jpg 在目录未被引用):约定文件名
  探测补槽(roughness/ao/occlusion × jpg/png),命中留 INFO 锚点
- 因子按工作流分支:glTF 用声明因子(MR 合图 B=金属度);
  OBJ metallic=0(specular 工作流无金属概念),roughness 有图→1,
  无图→Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2))(补偿默认贴图 G=0.5)
2026-09-23 19:49:36 +08:00
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
atdunbg d2b878c077 refactor: Cubemap 拆掉 17 参超级构造——资源三元组 EnvTexture
- image/view/显存分配收口为 EnvTexture 单元(RHI 惯例:三者同生共死),
  Cubemap = 4 个 EnvTexture + 2 采样器的组合,构造 17 参 → 8 参
- UploadCube/UploadTexture2D 直接返回完整资源单元(view 创建收进
  helper),工厂函数只做"生成数据 → 上传 → 组装"
- 析构循环成对释放,杜绝半套泄漏;删除无调用者的 Get()/GetFaceSize()/
  GetPrefilteredMipCount()(shader mip 上限以常量契约)
2026-09-23 19:25:32 +08:00
atdunbg bd4ac6aa12 fix: IBL 全黑根因——RadicalInverseVdC 误用 bit_cast,Hammersley 序列错乱
- bit_cast<f32>(bits) 把反转后的整数当 IEEE754 位型解读(LearnOpenGL
  原文 float(bits) 是整数值转换)→ xi.y 越出 [0,1] → sqrt(负数) →
  BRDF LUT NaN + pre-filter 数据全垃圾 → ambient NaN 整屏黑
- 分离出的积分程序实测:texel(0,2) 样本 10 xi.y=2 → NaN;修后无 NaN
- 加 CPU 侧 fp16 NaN 自检(上传前最后一道防线),数据异常拒绝加载
  走中性兜底(不再解引用空指针)
- 中性兜底 1×1 图只建 1 mip(修 mipLevels=5 validation 警告)
- prefilter mip 拷贝偏移改累积制(此前用当前 dim×序号错位)
- prefilter/irradiance 绑定换带 mip 的环境采样器(shadowSampler
  maxLod=0 把 textureLod clamp 到最锐 mip)
2026-09-23 19:18:06 +08:00
atdunbg 6e107ac2ba fix: IBL 模型全黑——prefilter mip 拷贝偏移错位 + 采样器 maxLod 错误
- UploadCube 的 mip buffer 偏移误用当前 mip 尺寸×序号(mip 面递减,
  正确应为累积偏移)→ 各 mip 拷到 staging 错位数据,含未初始化 fp16
  NaN → 污染 ambient 输出整屏黑
- prefilter/irradiance 绑定改用环境采样器(maxLod=mipCount-1):
  shadowSampler maxLod=0 会把 textureLod 显式 lod clamp 到 0,
  pre-filter 永远停在最锐 mip
2026-09-23 19:07:53 +08:00
atdunbg 9269afaa18 feat: IBL 阶段③——pre-filtered cubemap + BRDF LUT,split-sum 镜面项接入
- pre-filter:GGX 重要性采样(Hammersley + van der Corput 位反转),
  128²×5 mip 分级 roughness,采样数 32<<mip 上限 256;UploadCube 泛化
  支持 mip 链([mip: 6面] 紧凑排布逐 mip 逐面拷贝)
- BRDF LUT:128² R16G16Sfloat 积分表((NdotV,roughness)→F 缩放/偏置),
  纯数学与环境无关,中性兜底同样生成;专用 clamp-to-edge 采样器
  (共享 border 白采样器在 uv=1.0 混入白色导致高 roughness 偏大)
- shader:specular = prefiltered(R, roughness×4) × (F·A+B),
  ambient = (diffuse + specular) × ambientColor
- Hammersley 分母修正为当次积分采样数(低 mip 偏差 bug)
- Debug 构建预积分 26s→14s(采样预算收敛 + pow 改乘法链;
  env 1024² 重采样为大头,compute 迁移后归零)
2026-09-23 19:04:25 +08:00
atdunbg fa25cb3348 feat: IBL 阶段②——漫反射辐照度卷积,PBR ambient 接入环境光
- Cubemap 构造期同步 CPU 半球 Lambert 积分(64×32 网格 ≈2048 采样/像素)
  生成 32² irradiance cube(漫反射是超低频信号,分辨率无需高)
- shader:kD 用粗糙度感知 FresnelSchlickRoughness + (1-metallic),
  N 采样 irradianceMap(Set2 binding 3)替换常数 ambient;
  ambientColor 保留为强度/色调乘子
- CreateNeutral 兜底:无 HDR 资产时 1×1 常数 cube,描述符恒有效
- FaceDir 改为返回单位方向(equirect 查表要求,修掉未归一化的畸变)
2026-09-23 18:50:34 +08:00
atdunbg 4b5990e6c8 feat: 环境背景管线(IBL 阶段①)——HDR 等距柱状转 cube + Skybox pass
- Cubemap:stbi_loadf 加载 .hdr,CPU 双线性重采样 6 面(GL 规范方向表),
  f32→fp16 转换后整块上传 eCubeCompatible image(R16G16B16A16Sfloat)
- SkyboxPass:HDRColor 的 first writer,承担清屏职责;无环境贴图自动
  退化为纯清屏(Forward 不受影响)
- frag 逐像素 NDC 反投影取世界方向(invVP push,投影变换不可线性插值)
- Forward color 附件 Clear→Load:几何合成在天空之上
- SceneRenderer 产出 skyInvVP = inverse(proj × mat4(mat3(view)))
2026-09-23 18:37:01 +08:00
atdunbg 89bf4256e8 feat: 解析式 PBR(Cook-Torrance GGX,metallic-roughness 工作流)
- 修 void* 指针算术警告(Texture::InitFromDds)
- Material:slot2 语义切为 metallicRoughness(glTF 约定 G=粗糙度
  B=金属度)+ 新增材质因子参数 UBO(binding 4,per-slot ring,
  创建时写死,metallic/roughness 因子乘 MR 贴图)
- simple_shader.frag:Blinn-Phong → Cook-Torrance(GGX 分布 +
  Smith 几何 + Schlick Fresnel,k=(r+1)²/8 解析光源版);金属不参与
  漫反射;环境光暂为常数(IBL 后续)
- Model:读 aiTextureType_UNKNOWN(glTF MR 落点)+ AI_MATKEY_
  METALLIC/ROUGHNESS_FACTOR(旧资产缺键退化为电介质 0/0.5);
  材质缓存键含因子
- main.cpp:演示场景换回 backpack(旧资产走因子路径验证 PBR 全链路)
- 旧 specular 贴图不再使用(语义被 MR 取代)
2026-09-23 18:01:14 +08:00
atdunbg 2c4efc5165 feat: 多光源——点光源类型(距离衰减,位置来自 Transform)
- LightComponent 加 Type(Directional/Point)+ radius;点光位置取
  实体 Transform(固有组件首次实际受益)
- LightData shader 契约扩为 4×vec4(color/direction/position/params,
  每灯 64B,未用字段置零);UBO 688→816B
- 光照循环按类型分支:方向光无衰减;点光 L 指向光源 + 平方距离衰减,
  衰减半径外跳过;CSM 阴影只来自第 0 盏方向光
- main.cpp:SunTemple 场景加暖橙火坑光 + 冷蓝填充光(点光);
  KeyLight/FillLight 改 designated initializer
2026-09-23 17:46:25 +08:00
41 changed files with 1827 additions and 388 deletions
+33 -17
View File
@@ -28,6 +28,8 @@
using namespace Engine;
using namespace Engine::ECS;
namespace
{
class SandboxApp final : public Application
{
public:
@@ -46,10 +48,9 @@ protected:
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
EnableImGui();
// 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄)
// SunTemple:FBX + DDS 压缩贴图(DdsLoader 验证场景)
const auto model = GetAssets().LoadModel("assets/models/viking_room/viking_room.obj");
const auto temple = GetAssets().LoadModel("assets/models/SunTemple/SunTemple.fbx");
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
m_Spinners.push_back(scene.CreateEntity("backpack"));
m_Spinners[0].AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
@@ -69,27 +70,40 @@ protected:
m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
// 神庙实体 × 1( FBX 单位较大,缩放到场景尺度)
{
auto e = scene.CreateEntity("SunTemple");
e.GetComponent<TransformComponent>()
.SetScale(glm::vec3(0.05f));
e.AddComponent<MeshRendererComponent>(MeshRendererComponent{temple});
}
// 灯光实体(第一个灯 = 阴影光源)
// 灯光实体(第一个方向光 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight");
m_KeyLight.AddComponent<LightComponent>(
LightComponent{
glm::vec3(-0.5f, -0.6f, -0.35f),
glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)
.type = LightComponent::Type::Directional,
.direction = glm::vec3(-0.5f, -0.6f, -0.35f),
.color = glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)
});
scene.CreateEntity("FillLight")
.AddComponent<LightComponent>(
LightComponent{
glm::vec3(0.5f, -0.5f, 0.3f),
glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)
.type = LightComponent::Type::Directional,
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
.color = glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)
});
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
auto fire = scene.CreateEntity("FirePitLight");
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
fire.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Point,
.color = glm::vec4(1.0f, 0.6f, 0.3f, 2.0f), // 暖橙
.radius = 8.0f,
});
auto cool = scene.CreateEntity("CoolFillLight");
cool.GetComponent<TransformComponent>().SetPosition(glm::vec3(-2.5f, 2.0f, -2.0f));
cool.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Point,
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
.radius = 10.0f,
});
}
@@ -258,6 +272,7 @@ protected:
void OnWindowClose(const Event& event) override
{
SetImGuiEnabled(true);
m_ShowQuitConfirm = true;
}
@@ -289,6 +304,7 @@ private:
f32 m_Time = 0.0f;
bool m_PauseAnim = false; // 冻结动画时钟(帧历史 diff 验证用)
};
}
int main()
{
+1 -1
View File
@@ -39,7 +39,7 @@ namespace Engine {
class Application {
public:
Application(std::string title, u32 width, u32 height);
Application(const std::string& title, u32 width, u32 height);
virtual ~Application();
Application(const Application&) = delete;
+2 -2
View File
@@ -44,7 +44,7 @@ namespace Engine {
m_MouseCapture = std::move(mouse);
}
vk::SurfaceKHR CreateSurface(vk::Instance instance);
vk::SurfaceKHR CreateSurface(vk::Instance instance) const;
void DestroySurface(vk::Instance instance, vk::SurfaceKHR surface);
u32 GetWidth() const { return m_Width; }
@@ -63,7 +63,7 @@ namespace Engine {
// 输入事件系统
bool IsKeyPressed(int key) const;
void GetMouseDelta(float& dx, float& dy);
void GetMouseDelta(float& dx, float& dy) const;
+9 -3
View File
@@ -89,10 +89,16 @@ namespace Engine::ECS {
bool primary = false;
};
// 方向光
// 灯光。类型:
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
struct LightComponent {
glm::vec3 direction{0.0f, -1.0f, 0.0f};
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f};
enum class Type : u8 { Directional = 0, Point = 1 };
Type type = Type::Directional;
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f}; // rgb = 颜色,a = 强度
f32 radius = 10.0f; // 仅点光:衰减半径
};
} // namespace Engine::ECS
+90
View File
@@ -0,0 +1,90 @@
//
// Created by atdunbg on 2026/9/23.
//
// Cubemap:环境立方体贴图 + IBL 预积分资源(阶段②③,split-sum)。
//
// 从等距柱状 HDR(.hdr,polyhaven 等)生成,CPU 一次性预积分:
// env cube —— 重采样 6 面(Skybox 背景)
// irradiance cube —— 半球 Lambert 积分(PBR 漫反射 ambient)
// prefilter cube —— GGX 重要性采样 × mip 分级 roughness(镜面 IBL)
// BRDF LUT —— 2D 积分表(纯数学,与环境内容无关)
// 全部 fp16 上传,运行时采样零积分开销。
//
// 后续演进:CPU 预积分 → compute pass(动态环境零卡顿);本类的
// 方向表/上传路径届时整体迁移,接口不变。
//
#ifndef HELLOVULKAN_CUBEMAP_H
#define HELLOVULKAN_CUBEMAP_H
#include <Engine/Core/Types.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <glm/glm.hpp>
#include <vulkan/vulkan.hpp>
namespace Engine {
class Device;
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
struct EnvTexture {
vk::Image image{};
vk::ImageView view{};
MemoryAllocator::Allocation allocation;
};
class Cubemap {
public:
// 从等距柱状 HDR 生成(CPU 重采样 + irradiance/prefilter/BRDF 预积分)
static Scope<Cubemap> CreateFromHdrEquirect(Device& device,
MemoryAllocator& allocator,
const std::string& path,
u32 faceSize);
// 中性兜底(无 HDR 资产时):cube 均为常数色;BRDF LUT 仍为
// 真实积分(纯数学)。保证引用侧描述符恒有效(validation)
static Scope<Cubemap> CreateNeutral(Device& device,
MemoryAllocator& allocator,
const glm::vec3& rgb);
~Cubemap();
Cubemap(const Cubemap&) = delete;
Cubemap& operator=(const Cubemap&) = delete;
// ---- 消费端契约(SkyboxPass:背景;SceneGpu:PBR ambient)----
vk::ImageView GetView() const { return m_Env.view; } // 环境 cube
vk::ImageView GetIrradianceView() const { return m_Irr.view; } // 漫反射
vk::ImageView GetPrefilteredView() const { return m_Pref.view; } // 镜面
vk::ImageView GetBrdfLutView() const { return m_Lut.view; } // LUT
// cube 资源共享采样器(maxLod 覆盖 pre-filter mip 链)
vk::Sampler GetSampler() const { return m_EnvSampler; }
// LUT 专用采样器(clamp-to-edge):共享的 border 白采样器会在
// uv=1.0 处混入 50% 白色 border,导致高 roughness 镜面偏大
vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; }
private:
Cubemap(Device& device, MemoryAllocator& allocator,
EnvTexture env, EnvTexture irr, EnvTexture pref,
EnvTexture lut, vk::Sampler envSampler, vk::Sampler lutSampler)
: m_Device(device), m_Allocator(allocator),
m_Env(env), m_Irr(irr), m_Pref(pref), m_Lut(lut),
m_EnvSampler(envSampler), m_LutSampler(lutSampler) {}
Device& m_Device;
MemoryAllocator& m_Allocator;
EnvTexture m_Env; // 环境 cube(Skybox 背景)
EnvTexture m_Irr; // 漫反射辐照度 cube
EnvTexture m_Pref; // pre-filtered cube(mip 分 roughness)
EnvTexture m_Lut; // BRDF 积分 LUT(2D)
vk::Sampler m_EnvSampler;
vk::Sampler m_LutSampler;
};
} // namespace Engine
#endif //HELLOVULKAN_CUBEMAP_H
+31 -14
View File
@@ -1,46 +1,61 @@
//
// Created by atdunbg on 2026/9/21.
//
// Material:着色材质 = 一组纹理槽 + 一个描述符集。
// Material:PBR 材质 = 纹理槽组 + 因子参数 UBO + 一个描述符集。
//
// 槽位(binding = 槽位序号,与 simple_shader.frag 契约一致):
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 specular(高光强度/颜色,线性) 3 emissive(自发光,sRGB)
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
// 5 材质参数 UBO(fragment,kParamsBinding)
//
// 纹理非拥有(Model 的纹理缓存持有);空槽由调用方传默认纹理
//(白/平法线/黑/黑,Model 持有),shader 永远可以安全采样。
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
// 因子(metallicFactor/roughnessFactor)乘在 MR 贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1)
// OBJ(specular 工作流,独立 roughness 图或无图)→ metallic=0,
// roughness 因子按贴图有无/Ns 换算——金属度强制 0
//
#ifndef HELLOVULKAN_MATERIAL_H
#define HELLOVULKAN_MATERIAL_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
#include <string>
namespace Engine
{
namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
class Texture;
class UniformBuffer;
class Material
{
// 材质因子(乘 MR 贴图;无贴图时即最终值)
struct MaterialParams {
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f}; // x=metallic y=roughness
};
class Material {
public:
// 槽位序号(与 binding 对齐)
enum Slot : u32 {
Albedo = 0,
Normal = 1,
Specular = 2,
MetallicRoughness = 2,
Emissive = 3,
kSlotCount = 4,
Ao = 4,
kSlotCount = 5, // 纹理槽数
kParamsBinding = 5, // 参数 UBO binding
};
Material(Device& device,
Material(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures);
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params);
~Material();
Material(const Material&) = delete;
@@ -49,12 +64,14 @@ namespace Engine
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
// 材质缓存键:同贴图组合共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount]);
// 材质缓存键:同贴图组合+同因子共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params);
private:
std::array<Texture*, kSlotCount> m_Textures{};
vk::DescriptorSet m_DescriptorSet;
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
};
} // namespace Engine
+5 -3
View File
@@ -64,10 +64,11 @@ namespace Engine
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
// 材质缓存入口:同贴图组合共享同一 Material(一个描述符集),
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集),
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot(albedo/normal/specular/emissive)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount]);
// paths 下标 = Material::Slot;params = metallic/roughness 因子
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params);
Device& m_Device;
MemoryAllocator& m_Allocator;
@@ -83,6 +84,7 @@ namespace Engine
std::vector<Scope<Mesh>> m_Meshes;
std::string m_ModelPath;
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
};
} // namespace Engine
@@ -24,6 +24,7 @@ namespace Engine {
class DescriptorManager;
class ShaderManager;
class UniformBuffer;
class Cubemap;
namespace Passes {
@@ -40,8 +41,8 @@ namespace Engine {
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {albedo}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
@@ -60,6 +61,9 @@ namespace Engine {
void BindShadowMaps(RG::RenderGraph& rg,
const std::array<RG::TextureHandle, kMaxCascades>& maps);
// 环境辐照度(Set2 binding 3):view 全程稳定,绑一次即可
void BindEnvironment(const Cubemap& env);
private:
Device& m_Device;
DescriptorManager& m_Descriptors;
@@ -0,0 +1,61 @@
//
// Created by atdunbg on 2026/9/23.
//
// SkyboxPass:环境背景(IBL 阶段①)。
//
// HDRColor 的 first writer:承担清屏职责(loadOp Clear),有环境贴图时
// 再画天空——Forward 随之改为 loadOp Load,几何在天空之上合成。
// 职责收口对齐 UE:sky pass 是场景渲染的第一步,无深度附件,
// 深度测试关(几何完全覆盖由 Forward 的深度决定)。
//
// 环境贴图生命周期:Renderer 持有 Cubemap(声明序先于 pass),
// 本 pass 只引用。描述符绑定一次(cube view 全程稳定,不随 RG 重编译)。
//
#ifndef HELLOVULKAN_SKYBOXPASS_H
#define HELLOVULKAN_SKYBOXPASS_H
#include <Engine/Core/FrameContext.h>
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
class Device;
class DescriptorManager;
class ShaderManager;
class Cubemap;
namespace Passes {
class SkyboxPass final : public RenderPass {
public:
// env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色
SkyboxPass(Device& device, ShaderManager& shaders,
DescriptorManager& descriptors, FrameSceneData& frame,
RG::TextureHandle hdrColor, const Cubemap* env);
~SkyboxPass();
SkyboxPass(const SkyboxPass&) = delete;
SkyboxPass& operator=(const SkyboxPass&) = delete;
void Register(RG::RenderGraph& rg) override;
private:
Device& m_Device;
DescriptorManager& m_Descriptors;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
const Cubemap* m_Env; // 可空
vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube}
vk::PipelineLayout m_Layout; // push = invVP 64B
vk::ShaderEXT m_Vert{}, m_Frag{};
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_SKYBOXPASS_H
+5 -1
View File
@@ -41,6 +41,7 @@ namespace Engine {
class UniformBuffer;
class SceneRenderer;
class VulkanAssetUploader;
class Cubemap;
class ImGuiLayer;
namespace ECS { class Scene; }
@@ -49,7 +50,7 @@ namespace Engine {
class Renderer {
public:
Renderer(Window& window);
explicit Renderer(Window& window);
~Renderer();
Renderer(const Renderer&) = delete;
@@ -113,6 +114,9 @@ namespace Engine {
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
// 环境贴图(IBL 阶段①):HDR → cube,被 SkyboxPass 引用;
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
Scope<Cubemap> m_Environment;
Scope<Swapchain> m_Swapchain;
Scope<SyncManager> m_SyncManager;
Scope<CommandManager> m_CommandManager;
+9 -2
View File
@@ -37,10 +37,13 @@ namespace Engine {
constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
// 镜像 shader 侧 UBO 布局的灯光项
// 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。
// 字段未用部分置零——shader 按类型分支读取
struct LightData {
glm::vec4 direction; // xyz = 方向(已归一),w 未用
glm::vec4 color; // rgb = 颜色,a = 强度
glm::vec4 direction; // 方向光:xyz = 方向(已归一);点光未用
glm::vec4 position; // 点光:xyz = 世界位置(来自实体 Transform)
glm::vec4 params; // x = 类型(0=方向,1=点),y = 衰减半径,z/w 未用
};
// 镜像 simple_shader 的 UBO 布局(std140 语义由 shader 侧对齐)。
@@ -65,6 +68,10 @@ namespace Engine {
struct FrameSceneData {
UniformBufferObject ubo{};
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
glm::mat4 skyInvVP{1.0f};
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
std::vector<glm::mat4> instanceModels;
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
+1 -1
View File
@@ -29,7 +29,7 @@ namespace Engine {
vk::DeviceSize GetSize() const { return m_Size; }
// 只有 HOST_VISIBLE 的 Buffer 能调
void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0);
void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0) const;
private:
Device& m_Device;
@@ -28,10 +28,10 @@ namespace Engine {
CommandManager& operator=(const CommandManager&) = delete;
// 开始录制指定帧槽位的命令缓冲
vk::CommandBuffer BeginFrame(u32 frameSlot);
vk::CommandBuffer BeginFrame(u32 frameSlot) const;
// 结束录制
void EndFrame(u32 frameSlot);
void EndFrame(u32 frameSlot) const;
vk::CommandBuffer GetBuffer(u32 frameSlot) const { return m_Buffers[frameSlot]; }
@@ -28,21 +28,21 @@ namespace Engine {
DescriptorManager(const DescriptorManager&) = delete;
DescriptorManager& operator=(const DescriptorManager&) = delete;
vk::DescriptorSetLayout CreateLayout(const std::vector<DescriptorBinding>& bindings);
vk::DescriptorSetLayout CreateLayout(const std::vector<DescriptorBinding>& bindings) const;
vk::DescriptorSet AllocateDescriptorSet(vk::DescriptorSetLayout layout);
void UpdateUniformBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size);
vk::Buffer buffer, u64 size) const;
void UpdateStorageBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size);
vk::Buffer buffer, u64 size) const;
void UpdateTexture(vk::DescriptorSet set, u32 binding,
vk::Sampler sampler, vk::ImageView view);
vk::Sampler sampler, vk::ImageView view) const;
// 单个 set layout 的旧接口(保留)
vk::PipelineLayout CreatePipelineLayout(vk::DescriptorSetLayout layout,
u32 pushConstantSize);
u32 pushConstantSize) const;
// 多 set layout 的新接口。
// push constant range 按程序级 vertex|fragment 声明(与
@@ -50,7 +50,7 @@ namespace Engine {
// push 调用点必须带同样的双 stage(VUID-01796)
vk::PipelineLayout CreatePipelineLayout(
const std::vector<vk::DescriptorSetLayout>& layouts,
u32 pushConstantSize);
u32 pushConstantSize) const;
private:
Device& m_Device;
@@ -64,7 +64,7 @@ namespace Engine {
// 每块的容量(set 数;各类型描述符数同值)
static constexpr u32 kChunkSets = 4096;
vk::DescriptorPool CreateChunk();
vk::DescriptorPool CreateChunk() const;
};
} // namespace Engine
@@ -54,15 +54,15 @@ namespace Engine {
// 创建 + 分配 + 绑定一步完成(VMA 惯用法)
Allocation CreateBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
vk::MemoryPropertyFlags props);
vk::MemoryPropertyFlags props) const;
Allocation CreateImage(const vk::ImageCreateInfo& imageInfo,
vk::MemoryPropertyFlags props);
vk::MemoryPropertyFlags props) const;
// 销毁资源并释放分配(按 alloc.Buffer/Image 判断类型),随后 alloc 归零
void Destroy(Allocation& alloc);
void Destroy(Allocation& alloc) const;
// HOST_VISIBLE 非 coherent 写入后冲刷(VMA 内部对 coherent 自动跳过)
void Flush(const Allocation& alloc);
void Flush(const Allocation& alloc) const;
void PrintStats() const;
+3 -3
View File
@@ -39,12 +39,12 @@ namespace Engine {
ShaderObject CreateFromSPIRV(const std::vector<u32>& spirv,
vk::ShaderStageFlagBits stage,
const std::vector<vk::DescriptorSetLayout>& layouts = {},
u32 pushConstantSize = 0);
u32 pushConstantSize = 0) const;
void Destroy(const ShaderObject& shader);
void Destroy(const ShaderObject& shader) const;
private:
std::vector<u32> ReadSPIRV(const std::string& path);
static std::vector<u32> ReadSPIRV(const std::string& path);
Device& m_Device;
};
+2 -2
View File
@@ -35,8 +35,8 @@ namespace Engine {
vk::Fence GetFrameFence(u32 frameIndex) const { return m_FrameFences[frameIndex]; }
// 等待并重置这一帧的 Fence
void WaitFence(u32 frameIndex); // 只等,不 reset
void ResetFence(u32 frameIndex); // 只 reset
void WaitFence(u32 frameIndex) const; // 只等,不 reset
void ResetFence(u32 frameIndex) const; // 只 reset
private:
Device& m_Device;
+1 -1
View File
@@ -39,7 +39,7 @@ namespace Engine {
u64 GetSize() const { return m_Size; }
// 写入指定帧槽位的 UBO(仅在该槽位 fence 确认完成后调用)
void Update(u32 frameSlot, const void* data, u64 size);
void Update(u32 frameSlot, const void* data, u64 size) const;
private:
std::vector<Scope<Buffer>> m_Buffers;
+2 -2
View File
@@ -20,9 +20,9 @@
namespace Engine {
Application::Application(std::string title, u32 width, u32 height)
Application::Application(const std::string& title, u32 width, u32 height)
{
m_Window = CreateScope<Window>(std::move(title), width, height);
m_Window = CreateScope<Window>(title, width, height);
// 事件通道:GLFW 回调 → 本类 DispatchEvent → 派生类钩子
m_Window->SetEventCallback(
+16 -16
View File
@@ -18,7 +18,7 @@ namespace Engine
// ============ 资源计数 ============
void Stats::AddTexture(u64 bytes)
void Stats::AddTexture(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
m_TextureCount++;
@@ -27,7 +27,7 @@ namespace Engine
m_TotalAllocation += bytes;
}
void Stats::RemoveTexture(u64 bytes)
void Stats::RemoveTexture(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
if (m_TextureCount > 0) m_TextureCount--;
@@ -36,27 +36,27 @@ namespace Engine
m_TotalDeallocation += bytes;
}
void Stats::AddMesh(u64 vertexBytes, u64 indexBytes)
void Stats::AddMesh(const u64 vertexBytes, const u64 indexBytes)
{
std::lock_guard lock(m_Mutex);
m_MeshCount++;
u64 total = vertexBytes + indexBytes;
const u64 total = vertexBytes + indexBytes;
m_MeshBytes += total;
m_FrameAllocation += total;
m_TotalAllocation += total;
}
void Stats::RemoveMesh(u64 vertexBytes, u64 indexBytes)
void Stats::RemoveMesh(const u64 vertexBytes, const u64 indexBytes)
{
std::lock_guard lock(m_Mutex);
if (m_MeshCount > 0) m_MeshCount--;
u64 total = vertexBytes + indexBytes;
const u64 total = vertexBytes + indexBytes;
if (m_MeshBytes >= total) m_MeshBytes -= total;
m_FrameDeallocation += total;
m_TotalDeallocation += total;
}
void Stats::AddBuffer(u64 bytes)
void Stats::AddBuffer(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
m_BufferCount++;
@@ -65,7 +65,7 @@ namespace Engine
m_TotalAllocation += bytes;
}
void Stats::RemoveBuffer(u64 bytes)
void Stats::RemoveBuffer(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
if (m_BufferCount > 0) m_BufferCount--;
@@ -74,25 +74,25 @@ namespace Engine
m_TotalDeallocation += bytes;
}
void Stats::AddMemoryBlock(u64 bytes)
void Stats::AddMemoryBlock(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
m_MemoryBlockBytes += bytes;
}
void Stats::RemoveMemoryBlock(u64 bytes)
void Stats::RemoveMemoryBlock(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
if (m_MemoryBlockBytes >= bytes) m_MemoryBlockBytes -= bytes;
}
void Stats::AddMemoryUsed(u64 bytes)
void Stats::AddMemoryUsed(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
m_MemoryUsedBytes += bytes;
}
void Stats::RemoveMemoryUsed(u64 bytes)
void Stats::RemoveMemoryUsed(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
if (m_MemoryUsedBytes >= bytes) m_MemoryUsedBytes -= bytes;
@@ -111,14 +111,14 @@ namespace Engine
{
}
void Stats::AddAllocation(u64 bytes)
void Stats::AddAllocation(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
m_FrameAllocation += bytes;
m_TotalAllocation += bytes;
}
void Stats::AddDeallocation(u64 bytes)
void Stats::AddDeallocation(const u64 bytes)
{
std::lock_guard lock(m_Mutex);
m_FrameDeallocation += bytes;
@@ -206,13 +206,13 @@ namespace Engine
// ============ 打印 ============
static double MB(u64 bytes) { return bytes / (1024.0 * 1024.0); }
static double MB(const u64 bytes) { return bytes / (1024.0 * 1024.0); }
void Stats::Print() const
{
std::lock_guard lock(m_Mutex);
auto proc = ReadProcessMemory();
const auto proc = ReadProcessMemory();
ENGINE_INFO("========== Resource Stats ==========");
ENGINE_INFO("Textures: {:>4} ({:.2f} MB)", m_TextureCount, MB(m_TextureBytes));
+4 -4
View File
@@ -156,7 +156,7 @@ namespace Engine
glfwPollEvents();
}
vk::SurfaceKHR Window::CreateSurface(vk::Instance instance)
vk::SurfaceKHR Window::CreateSurface(const vk::Instance instance) const
{
VkSurfaceKHR surface;
if (glfwCreateWindowSurface(instance, m_Window, nullptr, &surface) != VK_SUCCESS)
@@ -167,7 +167,7 @@ namespace Engine
return surface;
}
void Window::DestroySurface(vk::Instance instance, vk::SurfaceKHR surface)
void Window::DestroySurface(const vk::Instance instance, const vk::SurfaceKHR surface)
{
instance.destroySurfaceKHR(surface);
}
@@ -184,14 +184,14 @@ namespace Engine
glfwGetFramebufferSize(m_Window, &width, &height);
}
bool Window::IsKeyPressed(int key) const
bool Window::IsKeyPressed(const int key) const
{
// UI 捕获键盘时,轮询接口同样静默(相机等轮询型消费方)
if (m_KeyboardCapture && m_KeyboardCapture()) return false;
return glfwGetKey(m_Window, key) == GLFW_PRESS;
}
void Window::GetMouseDelta(float& dx, float& dy)
void Window::GetMouseDelta(float& dx, float& dy) const
{
double mx, my;
glfwGetCursorPos(m_Window, &mx, &my);
+765
View File
@@ -0,0 +1,765 @@
//
// Created by atdunbg on 2026/9/23.
//
// Cubemap 实现:HDR 等距柱状 → CPU 预积分四件套 → fp16 上传。
//
// 一次性加载成本(秒级 CPU),换取运行时零积分开销;硬件 Cube 采样
//(filtering/边缘接缝由 driver 处理)。split-sum 预积分(LearnOpenGL/
// UE 拆分方案的离线端):
// irradiance —— 半球 Lambert 积分,漫反射 ambient
// prefilter —— GGX 重要性采样(Hammersley 序列),mip 分级 roughness
// BRDF LUT —— (NdotV, roughness) → (F 缩放, F 偏置),纯数学表
//
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Core/Log.h>
#include <stb/stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <vector>
namespace Engine
{
namespace
{
constexpr u32 kFaceCount = 6;
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
constexpr u32 kPhiSteps = 32; // 方位角采样数(512 采样/像素)
constexpr u32 kThetaSteps = 16; // 极角采样数
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
constexpr u32 kBrdfLutSamples = 128; // LUT 每像素采样数
// ---- GL/Vulkan cube 面方向表(与 GL spec 立方体选择表互逆)----
// (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位方向
glm::vec3 FaceDir(u32 face, f32 u, f32 v)
{
const f32 s = u * 2.0f - 1.0f;
const f32 t = v * 2.0f - 1.0f;
glm::vec3 dir;
switch (face) {
case 0: dir = { 1.0f, -t, -s }; break; // +X(面片顶行 = +Y)
case 1: dir = {-1.0f, -t, s }; break; // -X
case 2: dir = { s, 1.0f, t }; break; // +Y
case 3: dir = { s,-1.0f, -t }; break; // -Y
case 4: dir = { s, -t, 1.0f }; break; // +Z
default: dir = {-s, -t,-1.0f }; break; // -Z
}
return glm::normalize(dir); // equirect 查表要求单位方向
}
// 等距柱状双线性采样(x 环绕跨经度接缝,y 钳制到极点)。
// 图像行 0 = 天顶(+Y):v = acos(y)/π,与 stbi 行序一致,无需翻转
glm::vec3 SampleEquirect(const f32* img, u32 w, u32 h, const glm::vec3& dir)
{
const f32 u = std::atan2(dir.z, dir.x) * (0.5f / glm::pi<f32>()) + 0.5f;
const f32 v = std::acos(glm::clamp(dir.y, -1.0f, 1.0f)) / glm::pi<f32>();
const f32 fx = u * static_cast<f32>(w) - 0.5f;
const f32 fy = v * static_cast<f32>(h) - 0.5f;
const u32 x0 = static_cast<u32>(std::floor(fx));
const u32 y0 = static_cast<u32>(std::floor(fy));
const f32 tx = fx - std::floor(fx);
const f32 ty = fy - std::floor(fy);
auto fetch = [&](u32 x, u32 y) {
const u32 xc = ((x % w) + w) % w; // 经度环绕
const u32 yc = glm::clamp(y, 0u, h - 1u); // 纬度钳制
const size_t off = (static_cast<size_t>(yc) * w + xc) * 4;
return glm::vec3(img[off + 0], img[off + 1], img[off + 2]);
};
const glm::vec3 c00 = fetch(x0, y0);
const glm::vec3 c10 = fetch(x0 + 1, y0);
const glm::vec3 c01 = fetch(x0, y0 + 1);
const glm::vec3 c11 = fetch(x0 + 1, y0 + 1);
return glm::mix(glm::mix(c00, c10, tx), glm::mix(c01, c11, tx), ty);
}
// 漫反射辐照度半球积分(Lambert):E = ∫ L·cosθ·sinθ dθ dφ。
// 切线基 up×N 构造;均匀网格 ≈2048 采样/像素,对 32² 低频图足够
glm::vec3 ConvolveIrradiance(const f32* img, u32 w, u32 h,
const glm::vec3& N)
{
const glm::vec3 up = glm::abs(N.y) > 0.999f
? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
const glm::vec3 T = glm::normalize(glm::cross(up, N));
const glm::vec3 B = glm::cross(N, T);
const f32 dPhi = 2.0f * glm::pi<f32>() / static_cast<f32>(kPhiSteps);
const f32 dTheta = 0.5f * glm::pi<f32>() / static_cast<f32>(kThetaSteps);
glm::vec3 irradiance(0.0f);
for (u32 p = 0; p < kPhiSteps; ++p) {
const f32 phi = (static_cast<f32>(p) + 0.5f) * dPhi;
for (u32 t = 0; t < kThetaSteps; ++t) {
const f32 theta = (static_cast<f32>(t) + 0.5f) * dTheta;
const f32 sinT = std::sin(theta);
const f32 cosT = std::cos(theta);
const glm::vec3 sampleDir =
T * (sinT * std::cos(phi)) +
B * (sinT * std::sin(phi)) +
N * cosT;
irradiance += SampleEquirect(img, w, h, sampleDir) *
(cosT * sinT);
}
}
return irradiance * (dPhi * dTheta);
}
// ---- GGX 重要性采样(split-sum 镜面端)----
// van der Corput 基底反转(位反转技巧)。
// 注意是整数值转浮点(static_cast),不是位型重解读——
// bit_cast 会把反转后的整数当 IEEE754 解读,序列完全错乱
f32 RadicalInverseVdC(u32 bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return static_cast<f32>(bits) * 2.3283064365386963e-10f;
}
// n = 当次积分总采样数(xi.x = i/n 必须覆盖 [0,1] 全程)
glm::vec2 Hammersley(u32 i, u32 n)
{
return { static_cast<f32>(i) / static_cast<f32>(n),
RadicalInverseVdC(i) };
}
// GGX NDF 按概率密度取半程向量 H(切线空间 → 世界)
glm::vec3 ImportanceSampleGGX(const glm::vec2& xi, const glm::vec3& N,
f32 roughness)
{
const f32 a = roughness * roughness;
const f32 phi = 2.0f * glm::pi<f32>() * xi.x;
const f32 cosTheta =
std::sqrt((1.0f - xi.y) / (1.0f + (a * a - 1.0f) * xi.y));
const f32 sinTheta = std::sqrt(1.0f - cosTheta * cosTheta);
const glm::vec3 H(sinTheta * std::cos(phi),
sinTheta * std::sin(phi), cosTheta);
const glm::vec3 up = glm::abs(N.z) > 0.999f
? glm::vec3(1.0f, 0.0f, 0.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
const glm::vec3 T = glm::normalize(glm::cross(up, N));
const glm::vec3 B = glm::cross(N, T);
return glm::normalize(T * H.x + B * H.y + N * H.z);
}
// pre-filter 卷积:R(N) ≈ Σ L·NdotL / Σ NdotL(GGX 加权镜面均值)
glm::vec3 Prefilter(const f32* img, u32 w, u32 h,
const glm::vec3& N, f32 roughness, u32 samples)
{
const glm::vec3 V = N; // split-sum 约定:V 沿 N
glm::vec3 acc(0.0f);
f32 weight = 0.0f;
for (u32 i = 0; i < samples; ++i) {
const glm::vec3 H = ImportanceSampleGGX(
Hammersley(i, samples), N, roughness);
const glm::vec3 L = glm::normalize(2.0f * glm::dot(V, H) * H - V);
const f32 nDotL = glm::max(glm::dot(N, L), 0.0f);
if (nDotL > 0.0f) {
acc += SampleEquirect(img, w, h, L) * nDotL;
weight += nDotL;
}
}
return weight > 0.0f ? acc / weight : glm::vec3(0.0f);
}
// BRDF LUT 积分:(NdotV, roughness) → (A, B)。
// A/B 是 Fresnel 的缩放/偏置:specular = prefiltered·(F·A + B)
glm::vec2 IntegrateBrdf(f32 nDotV, f32 roughness)
{
// V 在以 N 为 +Z 的切线空间里(积分只依赖这两角度)
const glm::vec3 V(std::sqrt(1.0f - nDotV * nDotV), 0.0f, nDotV);
f32 a = 0.0f, b = 0.0f;
const f32 k = roughness * roughness * 0.5f; // IBL 版 Smith k
for (u32 i = 0; i < kBrdfLutSamples; ++i) {
const glm::vec2 xi = Hammersley(i, kBrdfLutSamples);
const glm::vec3 H = ImportanceSampleGGX(xi, glm::vec3(0, 0, 1),
roughness);
const glm::vec3 L = glm::normalize(
2.0f * glm::dot(V, H) * H - V);
const f32 nDotL = glm::max(L.z, 0.0f);
const f32 nDotH = glm::max(H.z, 0.0f);
const f32 vDotH = glm::max(glm::dot(V, H), 0.0f);
if (nDotL <= 0.0f) continue;
const f32 g = (nDotV / (nDotV * (1.0f - k) + k)) *
(nDotL / (nDotL * (1.0f - k) + k));
// 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),
// 0/0 = NaN 会累进 LUT——分母钳 epsilon
const f32 gVis = g * vDotH /
glm::max(nDotH * nDotV, 1e-6f);
const f32 fc = 1.0f - vDotH;
const f32 f = fc * fc * fc * fc * fc; // (1-vDotH)⁵,乘法链
a += (1.0f - f) * gVis;
b += f * gVis;
}
return { a / static_cast<f32>(kBrdfLutSamples),
b / static_cast<f32>(kBrdfLutSamples) };
}
// IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf)
u16 FloatToHalf(f32 value)
{
const u32 x = std::bit_cast<u32>(value);
const u16 sign = static_cast<u16>((x >> 16) & 0x8000u);
const u32 absVal = x & 0x7fffffffu;
if (absVal > 0x7f800000u) {
return static_cast<u16>(sign | 0x7e00u); // NaN
}
if (absVal >= 0x47800000u) {
return static_cast<u16>(sign | 0x7c00u); // Inf / 溢出饱和
}
if (absVal >= 0x38800000u) {
const u32 bits = absVal - 0x38000000u;
const u32 round = 0x0fffu + ((bits >> 13) & 1u);
return static_cast<u16>(sign | ((bits + round) >> 13));
}
if (absVal < 0x33000000u) {
return sign; // < 2^-25:舍入为 0
}
const u32 mantissa = (absVal & 0x007fffffu) | 0x00800000u;
const u32 shift = 126u - (absVal >> 23u);
const u32 m = (mantissa + (1u << (shift - 1))) >> shift;
return static_cast<u16>(sign | std::min(m, 0x400u));
}
// ---- 上传 ----
vk::ImageView CreateCubeView(Device& device, vk::Image image, u32 mips)
{
return device.GetDevice().createImageView(
vk::ImageViewCreateInfo{}
.setImage(image)
.setViewType(vk::ImageViewType::eCube)
.setFormat(vk::Format::eR16G16B16A16Sfloat)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, mips, 0, kFaceCount}));
}
vk::ImageView CreateLutView(Device& device, vk::Image image)
{
return device.GetDevice().createImageView(
vk::ImageViewCreateInfo{}
.setImage(image)
.setViewType(vk::ImageViewType::e2D)
.setFormat(vk::Format::eR16G16Sfloat)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1}));
}
// cube mip 链数据:[mip0: 6 面][mip1: 6 面]...,每面 faceSize²×8B
struct CubeMipData {
const u16* data; // 6 面连续
u32 faceSize;
};
// 上传 cube(含 mip 链)为 eCubeCompatible image(fp16),
// 返回完整资源单元(image + view + 显存分配)
EnvTexture UploadCube(Device& device, MemoryAllocator& allocator,
const std::vector<CubeMipData>& mips,
u32 viewMipCount)
{
const u32 mipCount = static_cast<u32>(mips.size());
// staging:各 mip 紧凑排布,记偏移
vk::DeviceSize total = 0;
for (const auto& mip : mips) {
total += static_cast<vk::DeviceSize>(mip.faceSize) *
mip.faceSize * 8 * kFaceCount;
}
auto stagingAlloc = allocator.CreateBuffer(
total,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
{
auto* dst = static_cast<u8*>(stagingAlloc.MappedPtr);
vk::DeviceSize off = 0;
for (const auto& mip : mips) {
const vk::DeviceSize bytes =
static_cast<vk::DeviceSize>(mip.faceSize) *
mip.faceSize * 8 * kFaceCount;
std::memcpy(dst + off, mip.data,
static_cast<size_t>(bytes));
off += bytes;
}
}
const u32 baseSize = mips[0].faceSize;
vk::ImageCreateInfo imageInfo;
imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(vk::Format::eR16G16B16A16Sfloat)
.setExtent({baseSize, baseSize, 1})
.setMipLevels(mipCount)
.setArrayLayers(kFaceCount)
.setSamples(vk::SampleCountFlagBits::e1)
.setTiling(vk::ImageTiling::eOptimal)
.setFlags(vk::ImageCreateFlagBits::eCubeCompatible)
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled)
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
auto imageAlloc = allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
const vk::Image image = imageAlloc.Image;
vk::CommandPoolCreateInfo poolInfo;
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
vk::CommandPool pool = device.GetDevice().createCommandPool(poolInfo);
vk::CommandBuffer cmd = device.GetDevice().allocateCommandBuffers(
vk::CommandBufferAllocateInfo{}
.setCommandPool(pool)
.setLevel(vk::CommandBufferLevel::ePrimary)
.setCommandBufferCount(1))[0];
cmd.begin(vk::CommandBufferBeginInfo{
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
// 全 mip × 全层一起转 layout
vk::ImageMemoryBarrier2 toTransfer;
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, mipCount, 0, kFaceCount});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
// 逐 mip 逐面拷贝;buffer 偏移必须按 mips[0..mip) 字节数累积
//(mip 面尺寸递减,用当前 dim 乘 mip 序号会错位 → 读到
// staging 未初始化数据 → fp16 NaN 污染整屏)
vk::DeviceSize offset = 0;
for (u32 mip = 0; mip < mipCount; ++mip) {
const u32 dim = mips[mip].faceSize;
for (u32 f = 0; f < kFaceCount; ++f) {
vk::BufferImageCopy region;
region.setBufferOffset(
offset + static_cast<vk::DeviceSize>(dim) *
dim * 8 * f)
.setBufferRowLength(0)
.setBufferImageHeight(0)
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
mip, f, 1})
.setImageOffset({0, 0, 0})
.setImageExtent({dim, dim, 1});
cmd.copyBufferToImage(stagingAlloc.Buffer, image,
vk::ImageLayout::eTransferDstOptimal,
region);
}
offset += static_cast<vk::DeviceSize>(dim) * dim * 8 *
kFaceCount;
}
vk::ImageMemoryBarrier2 toShader;
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, mipCount, 0, kFaceCount});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
cmd.end();
vk::Fence fence = device.GetDevice().createFence({});
device.GetGraphicsQueue().submit(
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
vk::Result::eSuccess) {
ENGINE_ERROR("Cubemap upload: waitForFences failed");
}
device.GetDevice().destroyFence(fence);
device.GetDevice().destroyCommandPool(pool);
allocator.Destroy(stagingAlloc);
return {imageAlloc.Image,
CreateCubeView(device, imageAlloc.Image, viewMipCount),
imageAlloc};
}
// 上传 BRDF LUT(2D,R16G16Sfloat,8B/px),返回完整资源单元
EnvTexture UploadTexture2D(Device& device,
MemoryAllocator& allocator,
const std::vector<u16>& texels,
u32 size, vk::Format format)
{
const vk::DeviceSize total =
static_cast<vk::DeviceSize>(size) * size * 8;
auto stagingAlloc = allocator.CreateBuffer(
total,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
std::memcpy(stagingAlloc.MappedPtr, texels.data(),
static_cast<size_t>(total));
vk::ImageCreateInfo imageInfo;
imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(format)
.setExtent({size, size, 1})
.setMipLevels(1)
.setArrayLayers(1)
.setSamples(vk::SampleCountFlagBits::e1)
.setTiling(vk::ImageTiling::eOptimal)
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled)
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
auto imageAlloc = allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
const vk::Image image = imageAlloc.Image;
vk::CommandPoolCreateInfo poolInfo;
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
vk::CommandPool pool = device.GetDevice().createCommandPool(poolInfo);
vk::CommandBuffer cmd = device.GetDevice().allocateCommandBuffers(
vk::CommandBufferAllocateInfo{}
.setCommandPool(pool)
.setLevel(vk::CommandBufferLevel::ePrimary)
.setCommandBufferCount(1))[0];
cmd.begin(vk::CommandBufferBeginInfo{
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
vk::ImageMemoryBarrier2 toTransfer;
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
vk::BufferImageCopy region;
region.setBufferOffset(0)
.setBufferRowLength(0)
.setBufferImageHeight(0)
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
0, 0, 1})
.setImageOffset({0, 0, 0})
.setImageExtent({size, size, 1});
cmd.copyBufferToImage(stagingAlloc.Buffer, image,
vk::ImageLayout::eTransferDstOptimal, region);
vk::ImageMemoryBarrier2 toShader;
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
cmd.end();
vk::Fence fence = device.GetDevice().createFence({});
device.GetGraphicsQueue().submit(
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
vk::Result::eSuccess) {
ENGINE_ERROR("BRDF LUT upload: waitForFences failed");
}
device.GetDevice().destroyFence(fence);
device.GetDevice().destroyCommandPool(pool);
allocator.Destroy(stagingAlloc);
return {imageAlloc.Image,
CreateLutView(device, imageAlloc.Image), imageAlloc};
}
// fp16 NaN 自检(指数全 1 且尾数非 0):预积分数据上传前的
// 最后一道防线——NaN 进 GPU 会污染整屏输出
bool ContainsNaNHalf(const u16* data, size_t count)
{
for (size_t i = 0; i < count; ++i) {
const u32 abs = data[i] & 0x7FFFu;
if (abs > 0x7C00u) return true;
}
return false;
}
vk::Sampler CreateEnvSampler(Device& device, u32 maxLod)
{
vk::SamplerCreateInfo samplerInfo;
samplerInfo.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge)
.setAnisotropyEnable(VK_FALSE)
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
.setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE)
.setMinLod(0.0f)
.setMaxLod(static_cast<f32>(maxLod));
return device.GetDevice().createSampler(samplerInfo);
}
// BRDF LUT 专用:clamp-to-edge(共享 border 白采样器会在 uv=1.0
// 处混入白色 border,高 roughness 镜面偏大)
vk::Sampler CreateLutSampler(Device& device)
{
return CreateEnvSampler(device, 0);
}
}
Scope<Cubemap> Cubemap::CreateFromHdrEquirect(Device& device,
MemoryAllocator& allocator,
const std::string& path,
u32 faceSize)
{
// ---- 1. CPU 加载 HDR(float RGBA)----
int w = 0, h = 0, channels = 0;
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
if (!pixels) {
ENGINE_ERROR("Cubemap: failed to load HDR: {}", path);
return nullptr;
}
const u32 srcW = static_cast<u32>(w);
const u32 srcH = static_cast<u32>(h);
// ---- 2a. 环境 cube:重采样 6 面 ----
const u32 facePixels = faceSize * faceSize;
std::vector<u16> envFaces(kFaceCount * facePixels * 4);
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = envFaces.data() + f * facePixels * 4;
for (u32 y = 0; y < faceSize; ++y) {
for (u32 x = 0; x < faceSize; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(faceSize);
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(faceSize);
const glm::vec3 rgb =
SampleEquirect(pixels, srcW, srcH, FaceDir(f, u, v));
u16* px = dst + (static_cast<size_t>(y) * faceSize + x) * 4;
px[0] = FloatToHalf(rgb.r);
px[1] = FloatToHalf(rgb.g);
px[2] = FloatToHalf(rgb.b);
px[3] = 0x3C00; // 1.0
}
}
}
// ---- 2b. irradiance cube:半球 Lambert 积分 ----
const u32 irrSize = kIrradianceFaceSize;
const u32 irrPixels = irrSize * irrSize;
std::vector<u16> irrFaces(kFaceCount * irrPixels * 4);
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = irrFaces.data() + f * irrPixels * 4;
for (u32 y = 0; y < irrSize; ++y) {
for (u32 x = 0; x < irrSize; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(irrSize);
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(irrSize);
const glm::vec3 rgb = ConvolveIrradiance(
pixels, srcW, srcH, FaceDir(f, u, v));
u16* px = dst + (static_cast<size_t>(y) * irrSize + x) * 4;
px[0] = FloatToHalf(rgb.r);
px[1] = FloatToHalf(rgb.g);
px[2] = FloatToHalf(rgb.b);
px[3] = 0x3C00;
}
}
}
// ---- 2c. pre-filter cube:GGX 重要性采样 × mip 分级 roughness ----
// 采样数随 mip 递增(高 roughness 分布更宽需要更多样本)。
// 注意:必须一次性分配总容量——逐 mip resize 会 realloc,使
// prefMips 里已记录的 data 指针悬空(上传读到垃圾 = NaN 全黑)
u32 prefTotal = 0;
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const u32 dim = kPrefilterFaceSize >> mip;
prefTotal += kFaceCount * dim * dim * 4;
}
std::vector<u16> prefFaces(prefTotal);
std::vector<CubeMipData> prefMips;
prefMips.reserve(kPrefilterMips);
size_t prefOffset = 0;
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const u32 dim = kPrefilterFaceSize >> mip;
const f32 roughness =
static_cast<f32>(mip) / static_cast<f32>(kPrefilterMips - 1);
const u32 samples = std::min(32u << mip, 256u);
u16* base = prefFaces.data() + prefOffset;
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = base + f * dim * dim * 4;
for (u32 y = 0; y < dim; ++y) {
for (u32 x = 0; x < dim; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(dim);
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(dim);
const glm::vec3 rgb = Prefilter(
pixels, srcW, srcH, FaceDir(f, u, v),
roughness, samples);
u16* px = dst + (static_cast<size_t>(y) * dim + x) * 4;
px[0] = FloatToHalf(rgb.r);
px[1] = FloatToHalf(rgb.g);
px[2] = FloatToHalf(rgb.b);
px[3] = 0x3C00;
}
}
}
prefMips.push_back({base, dim});
prefOffset += kFaceCount * dim * dim * 4;
}
stbi_image_free(pixels); // 源数据消费完毕
// ---- 2d. BRDF LUT:纯数学积分表(与环境内容无关)----
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
for (u32 y = 0; y < kBrdfLutSize; ++y) {
const f32 roughness = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(kBrdfLutSize);
for (u32 x = 0; x < kBrdfLutSize; ++x) {
const f32 nDotV = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(kBrdfLutSize);
const glm::vec2 ab = IntegrateBrdf(nDotV, roughness);
u16* px = lutTexels.data() + (static_cast<size_t>(y) * kBrdfLutSize + x) * 2;
px[0] = FloatToHalf(ab.x);
px[1] = FloatToHalf(ab.y);
}
}
// ---- 3. 上传:env / irradiance / prefilter / LUT(NaN 自检)----
if (ContainsNaNHalf(envFaces.data(), envFaces.size()) ||
ContainsNaNHalf(irrFaces.data(), irrFaces.size()) ||
ContainsNaNHalf(prefFaces.data(), prefFaces.size())) {
ENGINE_ERROR("Cubemap: NaN in pre-integrated data, "
"skipping IBL upload");
return nullptr;
}
if (ContainsNaNHalf(lutTexels.data(), lutTexels.size())) {
ENGINE_ERROR("Cubemap: NaN in BRDF LUT");
return nullptr;
}
EnvTexture env = UploadCube(device, allocator,
{{envFaces.data(), faceSize}}, 1);
EnvTexture irr = UploadCube(device, allocator,
{{irrFaces.data(), irrSize}}, 1);
EnvTexture pref = UploadCube(device, allocator, prefMips,
kPrefilterMips);
EnvTexture lut = UploadTexture2D(device, allocator, lutTexels,
kBrdfLutSize,
vk::Format::eR16G16Sfloat);
envFaces.clear(); envFaces.shrink_to_fit();
irrFaces.clear(); irrFaces.shrink_to_fit();
prefFaces.clear(); prefFaces.shrink_to_fit();
// cube sampler 共享:maxLod 覆盖 pre-filter mip 链(textureLod 显式
// 采样会被 clamp 到 maxLod,必须 ≥ mipCount-1)
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);
vk::Sampler lutSampler = CreateLutSampler(device);
ENGINE_INFO("Environment IBL: {} ({}x6 env, {}x6 irradiance, "
"{}x6 prefilter {} mips, {}² BRDF LUT)",
path, faceSize, irrSize, kPrefilterFaceSize,
kPrefilterMips, kBrdfLutSize);
return Scope<Cubemap>(new Cubemap(device, allocator,
env, irr, pref, lut, sampler, lutSampler));
}
Scope<Cubemap> Cubemap::CreateNeutral(Device& device,
MemoryAllocator& allocator,
const glm::vec3& rgb)
{
// 1×1×6 常数 cube(prefilter 仅 1 mip——1px 图不能建 mip 链,
// 采样越界 lod 会被 view range clamp 到 mip0,效果等同);
// BRDF LUT 仍为真实积分(纯数学,无环境依赖)——描述符恒有效
const u16 px[4] = { FloatToHalf(rgb.r), FloatToHalf(rgb.g),
FloatToHalf(rgb.b), 0x3C00 };
std::vector<u16> faces(kFaceCount * 4, 0);
for (u32 f = 0; f < kFaceCount; ++f) {
std::memcpy(faces.data() + f * 4, px, sizeof(px));
}
std::vector<u16> prefFaces(kFaceCount * 4, 0);
std::vector<CubeMipData> prefMips = {{prefFaces.data(), 1}};
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
for (u32 y = 0; y < kBrdfLutSize; ++y) {
const f32 roughness = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(kBrdfLutSize);
for (u32 x = 0; x < kBrdfLutSize; ++x) {
const f32 nDotV = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(kBrdfLutSize);
const glm::vec2 ab = IntegrateBrdf(nDotV, roughness);
u16* t = lutTexels.data() + (static_cast<size_t>(y) * kBrdfLutSize + x) * 2;
t[0] = FloatToHalf(ab.x);
t[1] = FloatToHalf(ab.y);
}
}
EnvTexture env = UploadCube(device, allocator, {{faces.data(), 1}}, 1);
EnvTexture irr = UploadCube(device, allocator, {{faces.data(), 1}}, 1);
EnvTexture pref = UploadCube(device, allocator, prefMips, 1);
EnvTexture lut = UploadTexture2D(device, allocator, lutTexels,
kBrdfLutSize, vk::Format::eR16G16Sfloat);
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);
vk::Sampler lutSampler = CreateLutSampler(device);
ENGINE_INFO("Neutral IBL environment (constant fallback, real BRDF LUT)");
return Scope<Cubemap>(new Cubemap(device, allocator,
env, irr, pref, lut, sampler, lutSampler));
}
Cubemap::~Cubemap()
{
const auto& vkDevice = m_Device.GetDevice();
vkDevice.destroySampler(m_EnvSampler);
vkDevice.destroySampler(m_LutSampler);
// 三元组成对释放:view → (image + 显存) 经 VMA 成对销毁
for (EnvTexture* tex : {&m_Env, &m_Irr, &m_Pref, &m_Lut}) {
vkDevice.destroyImageView(tex->view);
m_Allocator.Destroy(tex->allocation);
}
}
} // namespace Engine
+20 -4
View File
@@ -7,14 +7,17 @@
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/Texture.h>
#include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Core/FrameContext.h>
#include <Engine/Core/Log.h>
namespace Engine
{
Material::Material(Device& device,
Material::Material(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures)
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params)
: m_Textures(textures)
{
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
@@ -25,18 +28,31 @@ namespace Engine
m_Textures[slot]->GetSampler(),
m_Textures[slot]->GetView());
}
// 材质参数:per-slot ring,创建时写死(参数静态,无每帧更新)
m_ParamsBuffer = CreateScope<UniformBuffer>(
device, allocator, sizeof(MaterialParams), kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_ParamsBuffer->Update(slot, &params, sizeof(params));
descriptorManager.UpdateUniformBuffer(
m_DescriptorSet, kParamsBinding,
m_ParamsBuffer->Get(slot), m_ParamsBuffer->GetSize());
}
}
Material::~Material() = default;
std::string Material::MakeCacheKey(const std::string paths[kSlotCount])
std::string Material::MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params)
{
std::string key;
for (u32 slot = 0; slot < kSlotCount; ++slot) {
if (slot) key += '\n';
key += paths[slot];
key += '\n';
}
key += "f:" + std::to_string(params.factors.x) +
"," + std::to_string(params.factors.y);
return key;
}
} // namespace Engine
+86 -12
View File
@@ -38,15 +38,17 @@ Model::~Model() = default;
void Model::InitDefaultTextures()
{
// 各材质槽位的兜底纹理(1x1,开销可忽略):
// albedo 白(受光) / normal 平法线 (0.5,0.5,1) / specular 黑 / emissive 黑
// 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::Specular] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0, 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, 255, 255, false);
}
Model::Model(Device& device, MemoryAllocator& allocator,
@@ -74,7 +76,7 @@ Model::Model(Device& device, MemoryAllocator& allocator,
// 材质/纹理生命周期随模型(缓存持有)
std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths); // 全默认材质
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat));
@@ -94,10 +96,24 @@ 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;
}();
m_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 |
aiProcess_FlipUVs |
(m_IsGltf ? 0 : aiProcess_FlipUVs) |
aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices);
@@ -148,9 +164,10 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
return ptr;
}
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount])
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params)
{
const std::string key = Material::MakeCacheKey(paths);
const std::string key = Material::MakeCacheKey(paths, params);
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
return it->second.get();
}
@@ -169,7 +186,8 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
}
auto mat = CreateScope<Material>(
m_Device, m_DescriptorManager, m_TextureLayout, textures);
m_Device, m_Allocator, m_DescriptorManager, m_TextureLayout,
textures, params);
Material* ptr = mat.get();
m_MaterialCache.emplace(key, std::move(mat));
return ptr;
@@ -225,32 +243,88 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
}
}
// 读取材质纹理路径(4 槽:albedo/normal/specular/emissive)
// 读取材质纹理路径(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)。
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
// 先到先得,数组顺序即优先级
const std::pair<aiTextureType, u32> slots[] = {
{aiTextureType_DIFFUSE, Material::Albedo},
{aiTextureType_BASE_COLOR, Material::Albedo},
{aiTextureType_NORMALS, Material::Normal},
{aiTextureType_SPECULAR, Material::Specular},
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
{aiTextureType_METALNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::MetallicRoughness},
{aiTextureType_UNKNOWN, Material::MetallicRoughness},
{aiTextureType_SHININESS, 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={} 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::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 (m_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);
} else {
f32 roughnessFactor = 1.0f;
if (texPaths[Material::MetallicRoughness].empty()) {
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);
}
params.factors = glm::vec4(0.0f, roughnessFactor, 0.0f, 0.0f);
}
}
// 材质缓存:同贴图组合 → 同一 Material(一个描述符集),mesh 数 ≠ 集数
Material* mat = GetOrCreateMaterial(texPaths);
// 材质缓存:同贴图组合+因子 → 同一 Material(一个描述符集)
Material* mat = GetOrCreateMaterial(texPaths, params);
return CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat);
+2 -1
View File
@@ -22,9 +22,10 @@ namespace Engine::Passes
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
vk::RenderingAttachmentInfo colorAttachments[2];
// HDR 附件由 Skybox pass 清屏并画背景,这里 Load 在其上合成几何
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
+30 -3
View File
@@ -4,6 +4,7 @@
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
@@ -32,23 +33,32 @@ namespace Engine::Passes
};
uboSetLayout = descriptors.CreateLayout(uboBindings);
// Set 1: 材质纹理槽(binding = Material::Slot:0 albedo / 1 normal /
// 2 specular / 3 emissive)
// Set 1: PBR 材质纹理槽(binding = Material::Slot:0 albedo /
// 1 normal / 2 metallicRoughness / 3 emissive)+ 参数 UBO(binding 4)
std::vector<DescriptorBinding> texBindings;
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
texBindings.push_back(
{slot, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment});
}
texBindings.push_back(
{Material::kParamsBinding, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eFragment});
textureSetLayout = descriptors.CreateLayout(texBindings);
// Set 2: CSM 级联阴影图(fragment 采样,binding = 级联序号)
// Set 2: CSM 级联阴影图(0/1/2)+ IBL 环境三件套(3 irradiance
// / 4 pre-filtered / 5 BRDF LUT),均 fragment 采样
std::vector<DescriptorBinding> shadowBindings;
for (u32 c = 0; c < kMaxCascades; ++c) {
shadowBindings.push_back(
{c, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment});
}
for (const u32 b : {3u, 4u, 5u}) {
shadowBindings.push_back(
{b, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment});
}
shadowSetLayout = descriptors.CreateLayout(shadowBindings);
// Forward pipeline layout:三个 set;无 push(GPUDriven 经 SSBO)
@@ -154,4 +164,21 @@ namespace Engine::Passes
}
}
void SceneGpu::BindEnvironment(const Cubemap& env)
{
// IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。
// cube 资源必须用带 mip 的环境采样器(textureLod 显式 lod 会被
// clamp 到 maxLod;shadowSampler maxLod=0 会让 pre-filter 永远
// 停在最锐的 mip)
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Descriptors.UpdateTexture(shadowSets[slot], 3, env.GetSampler(),
env.GetIrradianceView());
m_Descriptors.UpdateTexture(shadowSets[slot], 4, env.GetSampler(),
env.GetPrefilteredView());
m_Descriptors.UpdateTexture(shadowSets[slot], 5,
env.GetBrdfLutSampler(),
env.GetBrdfLutView());
}
}
} // namespace Engine::Passes
+177
View File
@@ -0,0 +1,177 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Core/Log.h>
#include <glm/glm.hpp>
#include <tracy/Tracy.hpp>
#include <array>
namespace Engine::Passes
{
SkyboxPass::SkyboxPass(Device& device, ShaderManager& shaders,
DescriptorManager& descriptors, FrameSceneData& frame,
RG::TextureHandle hdrColor, const Cubemap* env)
: m_Device(device), m_Descriptors(descriptors), m_Frame(frame),
m_HDRColor(hdrColor), m_Env(env)
{
// Set0 = {samplerCube}(fragment 采样)
std::vector<DescriptorBinding> bindings = {
{0, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
};
m_SetLayout = descriptors.CreateLayout(bindings);
// push = invVP 64B(skybox.vert/.frag 声明一致,VUID-08878)
m_Layout = descriptors.CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
sizeof(glm::mat4));
const std::string shaderDir = "shaders/";
auto vert = shaders.CreateFromFile(
shaderDir + "skybox.vert.spv",
vk::ShaderStageFlagBits::eVertex,
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
sizeof(glm::mat4));
auto frag = shaders.CreateFromFile(
shaderDir + "skybox.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
sizeof(glm::mat4));
m_Vert = vert.Handle;
m_Frag = frag.Handle;
if (!m_Vert || !m_Frag) {
ENGINE_FATAL("SkyboxPass: failed to create shader objects");
}
// per-slot set:cube view 全程稳定,构造期绑定一次即可
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Sets[slot] = descriptors.AllocateDescriptorSet(m_SetLayout);
if (m_Env) {
descriptors.UpdateTexture(m_Sets[slot], 0,
m_Env->GetSampler(),
m_Env->GetView());
}
}
}
SkyboxPass::~SkyboxPass()
{
const auto& vkDevice = m_Device.GetDevice();
if (m_Vert) vkDevice.destroyShaderEXT(m_Vert);
if (m_Frag) vkDevice.destroyShaderEXT(m_Frag);
vkDevice.destroyPipelineLayout(m_Layout);
vkDevice.destroyDescriptorSetLayout(m_SetLayout);
}
void SkyboxPass::Register(RG::RenderGraph& rg)
{
rg.AddPass("Skybox", [this](RG::RenderGraphContext& ctx) {
ZoneScopedN("Skybox");
const vk::CommandBuffer cmd = ctx.cmd;
vk::ClearValue clear;
clear.color = vk::ClearColorValue(
std::array<float, 4>{0.1f, 0.1f, 0.1f, 1.0f});
vk::RenderingAttachmentInfo color;
color.setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear) // 清屏职责在本 pass
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clear);
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(color); // 无深度附件
cmd.beginRendering(renderingInfo);
if (m_Env) {
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {m_Vert, m_Frag};
cmd.bindShadersEXT(stages, shaders);
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Layout, 0,
m_Sets[ctx.frame.frameSlot], {});
cmd.pushConstants<glm::mat4>(
m_Layout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, m_Frame.skyInvVP);
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_FALSE);
cmd.setDepthWriteEnable(VK_FALSE);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
cmd.setDepthBiasEnable(VK_FALSE);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
// 全屏三角:无顶点输入
cmd.setVertexInputEXT({}, {});
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent));
// 无混合直写(天空不透明)
vk::ColorBlendEquationEXT blendEquation;
blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
// ArrayProxyNoTemporaries 拒绝临时量:具名局部变量传入
vk::Bool32 blendEnable = VK_FALSE;
vk::ColorComponentFlags colorWriteMask =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
cmd.setColorBlendEnableEXT(0, blendEnable);
cmd.setColorBlendEquationEXT(0, blendEquation);
cmd.setColorWriteMaskEXT(0, colorWriteMask);
cmd.draw(3, 1, 0, 0);
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment);
}
} // namespace Engine::Passes
+21
View File
@@ -12,8 +12,10 @@
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Passes/ShadowPass.h>
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Renderer/Passes/TAAPass.h>
#include <Engine/Renderer/Passes/BloomPass.h>
#include <Engine/Renderer/Passes/TonemapPass.h>
@@ -38,6 +40,7 @@
#include <glm/glm.hpp>
#include <array>
#include <filesystem>
#include <limits>
namespace Engine
@@ -71,6 +74,18 @@ namespace Engine
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager);
// 3.5 环境贴图(IBL):HDR 等距柱状 → cube + 预积分。
// 加载失败/文件缺失一律中性常数兜底(描述符恒有效)
const std::string envPath = "assets/textures/venice_dawn_1_4k.hdr";
if (std::filesystem::exists(envPath)) {
m_Environment = Cubemap::CreateFromHdrEquirect(
*m_Device, *m_Allocator, envPath, 1024);
}
if (!m_Environment) {
m_Environment = Cubemap::CreateNeutral(
*m_Device, *m_Allocator, glm::vec3(0.1f));
}
// 4. Swapchain(只管呈现;渲染目标统一归 RenderGraph)
m_Swapchain = CreateScope<Swapchain>(*m_Device, *m_Allocator, m_Window);
m_Extent = m_Swapchain->GetExtent();
@@ -96,6 +111,8 @@ namespace Engine
for (auto& pass : m_Passes) {
pass->OnTargetsRebuilt(*m_RenderGraph);
}
// 环境辐照度绑定(view 全程稳定,绑一次)
m_SceneGpu->BindEnvironment(*m_Environment);
// 9. GPU timestamp:每 pass 两枚(前后),按帧槽位分区
m_QueriesPerFrame = static_cast<u32>(m_RenderGraph->GetPassCount()) * 2;
@@ -228,6 +245,10 @@ namespace Engine
FrameSceneData& frame = m_SceneRenderer->Frame();
m_Passes.push_back(CreateScope<Passes::ShadowPass>(
*m_SceneGpu, frame, m_ShadowMaps));
// Skybox = HDRColor 的 first writer(清屏 + 画背景),先于 Forward 注册
m_Passes.push_back(CreateScope<Passes::SkyboxPass>(
*m_Device, *m_ShaderManager, *m_DescriptorManager, frame,
m_HDRColor, m_Environment.get()));
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps));
m_Passes.push_back(CreateScope<Passes::TAAPass>(
+30 -10
View File
@@ -168,24 +168,44 @@ namespace Engine
ubo.ambientColor = scene.GetSettings().ambientColor;
// ---- 2. 灯光(第一个灯作为阴影光源)----
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
// 反投影远平面点 = 相机相对世界方向,frag normalize 后采样 cube
m_Frame.skyInvVP =
glm::inverse(ubo.projNoJitter * glm::mat4(glm::mat3(ubo.view)));
// ---- 2. 灯光(第一个方向光作为 CSM 阴影光源;点光位置来自 Transform)----
u32 lightCount = 0;
glm::vec3 shadowDir{0.0f, -1.0f, 0.0f}; // 无灯兜底:头顶正射
scene.Each<LightComponent>([&](Entity, LightComponent& light) {
scene.Each<TransformComponent, LightComponent>(
[&](Entity, TransformComponent& t, LightComponent& light) {
if (lightCount >= kMaxLights) return;
glm::vec3 dir = glm::length(light.direction) > 0.0f
? glm::normalize(light.direction)
: glm::vec3(0.0f);
ubo.lights[lightCount].direction = glm::vec4(dir, 0.0f);
ubo.lights[lightCount].color = light.color;
auto& dst = ubo.lights[lightCount];
dst.color = light.color;
dst.params = glm::vec4(
light.type == LightComponent::Type::Point ? 1.0f : 0.0f,
light.radius, 0.0f, 0.0f);
if (lightCount == 0) shadowDir = dir;
if (light.type == LightComponent::Type::Point) {
dst.position = glm::vec4(t.GetPosition(), 1.0f);
dst.direction = glm::vec4(0.0f);
} else {
glm::vec3 dir = glm::length(light.direction) > 0.0f
? glm::normalize(light.direction) : glm::vec3(0.0f);
dst.direction = glm::vec4(dir, 0.0f);
dst.position = glm::vec4(0.0f);
}
// CSM 阴影光源 = 第一个方向光(点光不做级联阴影)
if (lightCount == 0 &&
light.type == LightComponent::Type::Directional) {
shadowDir = glm::length(light.direction) > 0.0f
? glm::normalize(light.direction) : shadowDir;
}
++lightCount;
});
for (u32 i = lightCount; i < kMaxLights; ++i) {
ubo.lights[i].direction = glm::vec4(0.0f);
ubo.lights[i].color = glm::vec4(0.0f);
ubo.lights[i] = {};
}
ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f);
+4 -4
View File
@@ -14,9 +14,9 @@
namespace Engine {
Buffer::Buffer(Device& device, MemoryAllocator& allocator,
vk::DeviceSize size, vk::BufferUsageFlags usage,
vk::MemoryPropertyFlags memProps)
: m_Device(device), m_Allocator(allocator) , m_Size(size)
const vk::DeviceSize size, const vk::BufferUsageFlags usage,
const vk::MemoryPropertyFlags memProps)
: m_Device(device), m_Size(size) , m_Allocator(allocator)
{
// VMA 惯用法:创建 + 分配 + 绑定一步完成;
// HOST_VISIBLE 时持久映射,MappedPtr 直接可用
@@ -33,7 +33,7 @@ namespace Engine {
STATS.RemoveBuffer(m_Size);
}
void Buffer::Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset)
void Buffer::Upload(const void* data, const vk::DeviceSize size, const vk::DeviceSize offset) const
{
if (!m_MappedPtr) {
ENGINE_ERROR("Buffer::Upload: buffer is not host visible");
+3 -3
View File
@@ -35,9 +35,9 @@ namespace Engine {
m_Device.GetDevice().destroyCommandPool(m_Pool);
}
vk::CommandBuffer CommandManager::BeginFrame(u32 frameSlot)
vk::CommandBuffer CommandManager::BeginFrame(const u32 frameSlot) const
{
auto cmd = m_Buffers[frameSlot];
const auto cmd = m_Buffers[frameSlot];
cmd.reset();
vk::CommandBufferBeginInfo beginInfo;
@@ -47,7 +47,7 @@ namespace Engine {
return cmd;
}
void CommandManager::EndFrame(u32 frameSlot)
void CommandManager::EndFrame(const u32 frameSlot) const
{
m_Buffers[frameSlot].end();
}
+11 -11
View File
@@ -18,12 +18,12 @@ namespace Engine
DescriptorManager::~DescriptorManager()
{
for (auto pool : m_Pools) {
for (const auto pool : m_Pools) {
m_Device.GetDevice().destroyDescriptorPool(pool);
}
}
vk::DescriptorPool DescriptorManager::CreateChunk()
vk::DescriptorPool DescriptorManager::CreateChunk() const
{
std::array<vk::DescriptorPoolSize, 3> poolSizes = {
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kChunkSets),
@@ -39,7 +39,7 @@ namespace Engine
return m_Device.GetDevice().createDescriptorPool(poolInfo);
}
vk::DescriptorSetLayout DescriptorManager::CreateLayout(const std::vector<DescriptorBinding>& bindings)
vk::DescriptorSetLayout DescriptorManager::CreateLayout(const std::vector<DescriptorBinding>& bindings) const
{
std::vector<vk::DescriptorSetLayoutBinding> vkBindings;
for (auto& b : bindings)
@@ -80,8 +80,8 @@ namespace Engine
}
}
void DescriptorManager::UpdateUniformBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size)
void DescriptorManager::UpdateUniformBuffer(const vk::DescriptorSet set, const u32 binding,
const vk::Buffer buffer, const u64 size) const
{
vk::DescriptorBufferInfo bufferInfo;
bufferInfo.setBuffer(buffer).setOffset(0).setRange(size);
@@ -97,8 +97,8 @@ namespace Engine
m_Device.GetDevice().updateDescriptorSets(write, {});
}
void DescriptorManager::UpdateStorageBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size)
void DescriptorManager::UpdateStorageBuffer(const vk::DescriptorSet set, const u32 binding,
const vk::Buffer buffer, const u64 size) const
{
vk::DescriptorBufferInfo bufferInfo;
bufferInfo.setBuffer(buffer).setOffset(0).setRange(size);
@@ -114,8 +114,8 @@ namespace Engine
m_Device.GetDevice().updateDescriptorSets(write, {});
}
void DescriptorManager::UpdateTexture(vk::DescriptorSet set, u32 binding,
vk::Sampler sampler, vk::ImageView view)
void DescriptorManager::UpdateTexture(const vk::DescriptorSet set, const u32 binding,
const vk::Sampler sampler, const vk::ImageView view) const
{
vk::DescriptorImageInfo imageInfo;
imageInfo.setSampler(sampler)
@@ -134,7 +134,7 @@ namespace Engine
}
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(vk::DescriptorSetLayout layout,
const u32 pushConstantSize)
const u32 pushConstantSize) const
{
vk::PipelineLayoutCreateInfo info;
info.setSetLayouts(layout);
@@ -154,7 +154,7 @@ namespace Engine
}
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(const std::vector<vk::DescriptorSetLayout>& layouts,
u32 pushConstantSize)
const u32 pushConstantSize) const
{
vk::PipelineLayoutCreateInfo info;
info.setSetLayouts(layouts);
+4 -4
View File
@@ -61,7 +61,7 @@ namespace Engine {
}
MemoryAllocator::Allocation MemoryAllocator::CreateBuffer(
vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags props)
vk::DeviceSize size, const vk::BufferUsageFlags usage, const vk::MemoryPropertyFlags props) const
{
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bi.size = size;
@@ -92,7 +92,7 @@ namespace Engine {
}
MemoryAllocator::Allocation MemoryAllocator::CreateImage(
const vk::ImageCreateInfo& imageInfo, vk::MemoryPropertyFlags props)
const vk::ImageCreateInfo& imageInfo, const vk::MemoryPropertyFlags props) const
{
VkImageCreateInfo ii = imageInfo; // 同布局,隐式转换
@@ -116,7 +116,7 @@ namespace Engine {
return out;
}
void MemoryAllocator::Destroy(Allocation& alloc)
void MemoryAllocator::Destroy(Allocation& alloc) const
{
if (!alloc.Valid) return;
@@ -131,7 +131,7 @@ namespace Engine {
alloc = {};
}
void MemoryAllocator::Flush(const Allocation& alloc)
void MemoryAllocator::Flush(const Allocation& alloc) const
{
if (!alloc.Valid || !alloc.Handle) return;
// HOST_COHERENT 时 VMA 内部自动跳过
+8 -8
View File
@@ -21,11 +21,11 @@ ShaderManager::~ShaderManager()
}
ShaderObject ShaderManager::CreateFromFile(const std::string& spvPath,
vk::ShaderStageFlagBits stage,
const vk::ShaderStageFlagBits stage,
const std::vector<vk::DescriptorSetLayout>& layouts,
u32 pushConstantSize)
const u32 pushConstantSize)
{
auto spirv = ReadSPIRV(spvPath);
const auto spirv = ReadSPIRV(spvPath);
if (spirv.empty()) {
ENGINE_ERROR("Failed to read SPIR-V: {}", spvPath);
return { nullptr, stage };
@@ -34,9 +34,9 @@ ShaderObject ShaderManager::CreateFromFile(const std::string& spvPath,
}
ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
vk::ShaderStageFlagBits stage,
const vk::ShaderStageFlagBits stage,
const std::vector<vk::DescriptorSetLayout>& layouts,
u32 pushConstantSize)
const u32 pushConstantSize) const
{
vk::ShaderStageFlags nextStage = {};
if (stage == vk::ShaderStageFlagBits::eVertex) {
@@ -70,7 +70,7 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
vk::ShaderEXT shader;
try {
auto result = m_Device.GetDevice().createShadersEXT(createInfo);
const auto result = m_Device.GetDevice().createShadersEXT(createInfo);
shader = result.value[0];
}
catch (const vk::SystemError& e) {
@@ -81,7 +81,7 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
return { shader, stage };
}
void ShaderManager::Destroy(const ShaderObject& shader)
void ShaderManager::Destroy(const ShaderObject& shader) const
{
if (shader.Handle) {
m_Device.GetDevice().destroyShaderEXT(shader.Handle);
@@ -96,7 +96,7 @@ std::vector<u32> ShaderManager::ReadSPIRV(const std::string& path)
return {};
}
size_t size = file.tellg();
const size_t size = file.tellg();
std::vector<u32> buffer(size / sizeof(u32));
file.seekg(0);
file.read(reinterpret_cast<char*>(buffer.data()), size);
+3 -3
View File
@@ -14,7 +14,7 @@ namespace Engine {
SyncManager::SyncManager(Device& device, u32 maxFramesInFlight, u32 imageCount)
: m_Device(device)
{
vk::SemaphoreCreateInfo semInfo;
const vk::SemaphoreCreateInfo semInfo;
// 每帧一个 ImageAvailable
m_ImageAvailable.resize(maxFramesInFlight);
@@ -48,7 +48,7 @@ namespace Engine {
for (auto s : m_RenderFinished) vkDevice.destroySemaphore(s);
}
void SyncManager::WaitFence(u32 frameIndex)
void SyncManager::WaitFence(const u32 frameIndex) const
{
auto result = m_Device.GetDevice().waitForFences(
m_FrameFences[frameIndex], VK_TRUE, std::numeric_limits<u64>::max());
@@ -57,7 +57,7 @@ namespace Engine {
}
}
void SyncManager::ResetFence(u32 frameIndex)
void SyncManager::ResetFence(const u32 frameIndex) const
{
m_Device.GetDevice().resetFences(m_FrameFences[frameIndex]);
}
+3 -2
View File
@@ -219,7 +219,7 @@ namespace Engine
// DDS → Vulkan 格式映射。sRGB 语义由贴图槽位决定(渲染侧职责):
// 颜色图(BaseColor/Emissive)→ SRGB 变体
// 数据图(Normal/Specular 等线性槽位)→ UNORM
vk::Format ToVkFormat(Assets::Dds::DdsFormat f, bool srgb)
vk::Format ToVkFormat(const Assets::Dds::DdsFormat f, const bool srgb)
{
using DF = Assets::Dds::DdsFormat;
switch (f) {
@@ -279,9 +279,10 @@ namespace Engine
std::vector<vk::DeviceSize> offsets(m_MipLevels);
{
vk::DeviceSize off = 0;
auto* dst = static_cast<u8*>(stagingAlloc.MappedPtr);
for (u32 mip = 0; mip < m_MipLevels; ++mip) {
offsets[mip] = off;
std::memcpy(stagingAlloc.MappedPtr + off,
std::memcpy(dst + off,
data.Mips[mip].data(), data.Mips[mip].size());
off += data.Mips[mip].size();
}
+1 -1
View File
@@ -26,7 +26,7 @@ namespace Engine {
ENGINE_INFO("UniformBuffer created: {} bytes x {} frames", size, frameCount);
}
void UniformBuffer::Update(u32 frameSlot, const void* data, u64 size)
void UniformBuffer::Update(const u32 frameSlot, const void* data, const u64 size) const
{
m_Buffers[frameSlot]->Upload(data, size);
}
+107 -21
View File
@@ -1,10 +1,13 @@
#version 450
const int MAX_LIGHTS = 4;
const float PI = 3.14159265359;
struct Light {
vec4 direction;
vec4 color;
vec4 color; // rgb = 颜色,a = 强度
vec4 direction; // 方向光:xyz = 方向(已归一)
vec4 position; // 点光:xyz = 世界位置
vec4 params; // x = 类型(0=方向,1=点),y = 衰减半径
};
layout(location = 0) in vec2 fragUV;
@@ -33,18 +36,24 @@ layout(set = 0, binding = 0) uniform UniformBufferObject {
// 材质纹理槽(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 = 2) uniform sampler2D metallicRoughnessMap; // G=粗糙度 B=金属度
layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
layout(set = 1, binding = 4) uniform sampler2D aoMap; // 环境光遮蔽(R 通道)
layout(set = 1, binding = 5) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子
} matParams;
layout(set = 2, binding = 0) uniform sampler2D shadowMap0; // CSM 级联 0(最近)
layout(set = 2, binding = 1) uniform sampler2D shadowMap1; // 级联 1
layout(set = 2, binding = 2) uniform sampler2D shadowMap2; // 级联 2(最远)
layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②)
layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③)
layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias)
// 第 0 盏灯(KeyLight)的 CSM 阴影因子:0.3 影中 / 1.0 受光。
// 第 0 盏方向光(KeyLight)的 CSM 阴影因子:0.3 影中 / 1.0 受光。
// 级联按视图深度选择;采样器均 clamp-to-border 白色:
// 光视锥外自动 1.0(无阴影),省出界分支。
float ShadowFactor(vec3 N, float viewDepth) {
// 级联选择:第一个 viewDepth < split 的级联
int cascade = 2;
for (int i = 0; i < 3; ++i) {
if (viewDepth < ubo.cascadeSplits[i]) { cascade = i; break; }
@@ -52,7 +61,6 @@ float ShadowFactor(vec3 N, float viewDepth) {
vec4 ls = ubo.lightSpace[cascade] * vec4(fragWorldPos, 1.0);
vec3 proj = ls.xyz / ls.w;
// NDC [-1,1] → UV [0,1](ortho 的 w=1)
vec2 uv = proj.xy * 0.5 + 0.5;
float closest;
@@ -60,7 +68,6 @@ float ShadowFactor(vec3 N, float viewDepth) {
else if (cascade == 1) closest = texture(shadowMap1, uv).r;
else closest = texture(shadowMap2, uv).r;
// 斜率缩放 bias:掠射面用更大的 bias 压 acne
vec3 L = normalize(-ubo.lights[0].direction.xyz);
float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012);
@@ -81,31 +88,110 @@ vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
}
// ---- Cook-Torrance BRDF 各项 ----
float DistributionGGX(float NdotH, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
float GeometrySmith(float NdotV, float NdotL, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0; // 解析光源用此 k(IBL 才用 IBL 版本)
float gv = NdotV / (NdotV * (1.0 - k) + k);
float gl = NdotL / (NdotL * (1.0 - k) + k);
return gv * gl;
}
vec3 FresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// 粗糙度感知的 Fresnel(IBL 用):粗糙表面掠射边缘的 F 上升更平缓
vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
vec3 Fmax = max(vec3(1.0 - roughness), F0);
return F0 + (Fmax - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
void main() {
vec3 albedo = texture(albedoMap, fragUV).rgb;
float specIntensity = texture(specularMap, fragUV).r;
vec4 mr = texture(metallicRoughnessMap, fragUV);
float roughness = clamp(mr.g * matParams.factors.y, 0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
vec3 emissive = texture(emissiveMap, fragUV).rgb;
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
float NdotV = max(dot(N, V), 1e-4);
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
vec3 result = ubo.ambientColor.rgb * albedo;
vec3 Lo = vec3(0.0);
for (int i = 0; i < MAX_LIGHTS; ++i) {
vec3 L = normalize(-ubo.lights[i].direction.xyz);
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;
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a *
(albedo * diff + vec3(spec)) * shade;
// 按类型分支:方向光无衰减;点光方向指向光源 + 平方距离衰减
vec3 L;
float atten = 1.0;
if (ubo.lights[i].params.x > 0.5) {
vec3 toLight = ubo.lights[i].position.xyz - fragWorldPos;
float d = length(toLight);
if (d >= ubo.lights[i].params.y) continue;
L = toLight / d;
float a = clamp(1.0 - (d * d) /
(ubo.lights[i].params.y * ubo.lights[i].params.y),
0.0, 1.0);
atten = a * a;
} else {
L = normalize(-ubo.lights[i].direction.xyz);
}
// 自发光不经光照/阴影直接叠加
result += emissive;
vec3 radiance = ubo.lights[i].color.rgb * ubo.lights[i].color.a * atten;
float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0) continue;
outColor = vec4(result, 1.0);
// Cook-Torrance 镜面项
vec3 H = normalize(V + L);
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = max(dot(H, V), 0.0);
float D = DistributionGGX(NdotH, roughness);
float G = GeometrySmith(NdotV, NdotL, roughness);
vec3 F = FresnelSchlick(NdotH2, F0);
vec3 kS = F;
vec3 kD = (1.0 - kS) * (1.0 - metallic); // 金属不参与漫反射
vec3 specular = (D * G * F) /
max(4.0 * NdotV * NdotL, 1e-4);
// 阴影只来自第 0 盏方向光(CSM)
float shade = (i == 0 && ubo.lights[0].params.x < 0.5)
? ShadowFactor(N, fragViewDepth) : 1.0;
Lo += (kD * albedo / PI + specular) * radiance * NdotL * shade;
}
// 环境光:IBL split-sum(阶段②③)。
// 漫反射:N 采样 irradiance cube(半球 Lambert 预卷积)
// 镜面:R 采样 pre-filtered cube(mip = roughness 分级)
// × BRDF LUT 查表得 F 缩放/偏置
// ambientColor = 用户可调强度/色调乘子
vec3 F_amb = FresnelSchlickRoughness(NdotV, F0, roughness);
vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic);
vec3 irradiance = texture(irradianceMap, N).rgb;
float ao = texture(aoMap, fragUV).r;
vec3 diffuse = kD_amb * irradiance * albedo * ao;
vec3 R = reflect(-V, N);
const float kMaxPrefilterMip = 4.0; // 与 Cubemap::kPrefilterMips - 1 同步
vec3 prefilteredColor = textureLod(prefilteredMap, R,
roughness * kMaxPrefilterMip).rgb;
vec2 envBRDF = texture(brdfLut, vec2(NdotV, roughness)).rg;
vec3 specular = prefilteredColor * (F_amb * envBRDF.x + envBRDF.y);
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb;
vec3 color = ambient + Lo + emissive;
outColor = vec4(color, 1.0);
outVelocity = fragVelocity;
}
+4 -2
View File
@@ -3,8 +3,10 @@
const int MAX_LIGHTS = 4;
struct Light {
vec4 direction;
vec4 color;
vec4 color; // rgb = 颜色,a = 强度
vec4 direction; // 方向光:xyz = 方向(已归一)
vec4 position; // 点光:xyz = 世界位置
vec4 params; // x = 类型(0=方向,1=点),y = 衰减半径
};
layout(location = 0) in vec3 inPosition;
+21
View File
@@ -0,0 +1,21 @@
#version 450
// 天空盒像素:NDC → 世界方向 → cube 采样。
// invVP = inverse(projNoJitter × mat4(mat3(view))):去掉平移的视图,
// 反投影结果即"相机相对"的世界方向,normalize 后直接采样 cube。
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform samplerCube envMap;
// push 与 skybox.vert 一致声明(VUID-08878)
layout(push_constant) uniform PushConstants {
mat4 invVP;
} pc;
void main() {
vec4 target = pc.invVP * vec4(fragUV * 2.0 - 1.0, 1.0, 1.0);
vec3 dir = normalize(target.xyz / target.w);
outColor = vec4(texture(envMap, dir).rgb, 1.0);
}
+21
View File
@@ -0,0 +1,21 @@
#version 450
// 天空盒顶点:全屏三角形(无顶点缓冲),UV 交给 fragment 反投影。
// 方向在 fragment 逐像素计算(invVP 是投影变换,对 NDC 线性插值方向
// 不精确;UV 插值是仿射的,逐像素重建方向零误差)。
layout(location = 0) out vec2 fragUV;
// push 与 skybox.frag 一致声明(VUID-08878:shader object 接口一致)
layout(push_constant) uniform PushConstants {
mat4 invVP;
} pc;
void main() {
vec2 p = vec2(
float((gl_VertexIndex << 1) & 2),
float(gl_VertexIndex & 2));
fragUV = p;
// 远平面:z = 1(Vulkan [0,1] 深度),无深度附件,只关心覆盖屏幕
gl_Position = vec4(p * 2.0 - 1.0, 1.0, 1.0);
}