feat: IBL 预积分 GPU compute 化 + 环境资产入 Assets 层(动态切换)
CPU 逐像素卷积(venice 4k 阻塞 7.36s)→ 4 个 compute shader 一次性 提交:equirect→cube / irradiance / prefilter×5mip / BRDF LUT。 Cubemap 瘦身为纯资源持有类,生成职责移交 IblGenerator; 环境经 AssetManager 缓存(EnvRef),Renderer::SetEnvironment 动态换绑(RenderPass::OnEnvironmentChanged 广播)。 调试视图 +4:EnvCube / Irradiance / Prefilter / BRDF LUT。 性能前后对比(RTX 5060 Laptop,venice_dawn_1_4k.hdr): - 改造前 CPU:7.36 s(主线程阻塞) - 改造后 GPU:HDR decode 127-134ms + 资源 22-35ms + compute 17-19ms ≈ 165-190ms(约 40×);Neutral 兜底 ~6ms - 运行中动态切换环境同量级(ms 级 GPU,decode 占大头) 冒烟:两构建目录 0 警告;运行期 0 validation(含两次环境切换 + 调试视图 9-12 轮播自动化验证)。
This commit is contained in:
@@ -145,6 +145,11 @@ namespace Engine {
|
||||
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
|
||||
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||
EnvRef LoadEnvironment(const std::string& path);
|
||||
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
||||
|
||||
AssetCache<Model>& Models() { return m_Models; }
|
||||
|
||||
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
||||
@@ -154,6 +159,7 @@ namespace Engine {
|
||||
IAssetUploader& m_Uploader;
|
||||
|
||||
AssetCache<Model> m_Models;
|
||||
AssetCache<Cubemap> m_Environments;
|
||||
};
|
||||
|
||||
} // namespace Assets
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
|
||||
namespace Engine::Assets {
|
||||
|
||||
@@ -31,6 +32,9 @@ namespace Engine::Assets {
|
||||
|
||||
// 程序化纯色平面(size 为半边长)
|
||||
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||
};
|
||||
|
||||
} // namespace Engine::Assets
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
// Engine::Assets::Engine::Model 这类错误嵌套命名空间)
|
||||
namespace Engine {
|
||||
class Model;
|
||||
class Cubemap;
|
||||
class Texture;
|
||||
class Material;
|
||||
}
|
||||
@@ -39,6 +40,7 @@ namespace Engine {
|
||||
|
||||
// 常用别名(新增资产类型时在这里加一行)
|
||||
using ModelRef = AssetRef<Model>;
|
||||
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// Cubemap:环境立方体贴图 + IBL 预积分资源(阶段②③,split-sum)。
|
||||
// Cubemap:环境立方体贴图 + IBL 预积分资源(split-sum 四件套)。
|
||||
//
|
||||
// 纯 GPU 资源持有类(对齐 Model 的定位):只管 image/view/采样器的
|
||||
// 同生共死,不做任何生成/上传。预积分由 IblGenerator(compute 链)
|
||||
// 产出后注入——环境切换的入口在 Assets 层(AssetManager::LoadEnvironment)。
|
||||
//
|
||||
// 从等距柱状 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(动态环境零卡顿);本类的
|
||||
// 方向表/上传路径届时整体迁移,接口不变。
|
||||
// 全部 fp16,运行时采样零积分开销。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CUBEMAP_H
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
@@ -37,18 +36,6 @@ namespace Engine {
|
||||
|
||||
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;
|
||||
@@ -67,6 +54,8 @@ namespace Engine {
|
||||
vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; }
|
||||
|
||||
private:
|
||||
friend class IblGenerator; // 生成端唯一构造入口
|
||||
|
||||
Cubemap(Device& device, MemoryAllocator& allocator,
|
||||
EnvTexture env, EnvTexture irr, EnvTexture pref,
|
||||
EnvTexture lut, vk::Sampler envSampler, vk::Sampler lutSampler)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/24.
|
||||
//
|
||||
// IblGenerator:IBL 预积分的 compute 生成端(原 Cubemap CPU 积分路径的
|
||||
// GPU 化,"动态环境切换零卡顿"第一步)。
|
||||
//
|
||||
// 管线:等距柱状 HDR → 上传 equirect 中间纹理(免 CPU 逐像素转换)
|
||||
// → 一次性提交四段 dispatch:
|
||||
// ① equirect_to_cube(硬件过滤重采样 6 面)
|
||||
// ② irradiance(半球 Lambert 卷积,采样 env cube)
|
||||
// ③ prefilter × mip(GGX 重要性采样,push roughness)
|
||||
// ④ BRDF LUT(纯数学)
|
||||
// → fence 等待 → 产出 Cubemap(四件套 fp16 资源)。
|
||||
//
|
||||
// 为什么不进 RenderGraph:RG 管理的是帧内 2D 瞬时资源(无 cube/数组层/
|
||||
// storage 写/条件执行),而预积分是"资产加载时跑一次"的持久资源——
|
||||
// 独立一次性提交与原上传路径同形态,侵入最小。残留卡顿 = fence 等待
|
||||
// (毫秒级),异步 compute queue / IO 线程解码是后续演进点。
|
||||
//
|
||||
// 产出与消费端契约(SkyboxPass / SceneGpu set2 binding3-5)不变:
|
||||
// view 形态与原 CPU 路径完全一致,绑定代码零改动。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_IBLGENERATOR_H
|
||||
#define HELLOVULKAN_IBLGENERATOR_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class DescriptorManager;
|
||||
class ShaderManager;
|
||||
class Cubemap;
|
||||
|
||||
class IblGenerator {
|
||||
public:
|
||||
IblGenerator(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors, ShaderManager& shaders);
|
||||
~IblGenerator();
|
||||
|
||||
IblGenerator(const IblGenerator&) = delete;
|
||||
IblGenerator& operator=(const IblGenerator&) = delete;
|
||||
|
||||
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
|
||||
Scope<Cubemap> FromHdrEquirect(const std::string& path, u32 faceSize);
|
||||
|
||||
// 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效)
|
||||
Scope<Cubemap> Neutral(const glm::vec3& rgb);
|
||||
|
||||
private:
|
||||
// cube 兼容 image(2D array 写 + cube 采样双 view)
|
||||
struct CubeTarget {
|
||||
vk::Image image{};
|
||||
std::vector<vk::ImageView> storageViews; // 每 mip 一个(storage 写必须单 mip)
|
||||
vk::ImageView cubeView{}; // eCube 全 mip(采样端)
|
||||
MemoryAllocator::Allocation allocation;
|
||||
};
|
||||
|
||||
// 从 HDR 像素数据生成(Neutral 走同一链,仅 equirect 为 1×1 常数)
|
||||
Scope<Cubemap> Generate(const f32* pixels, u32 w, u32 h,
|
||||
u32 faceSize, const std::string& debugName);
|
||||
|
||||
CubeTarget CreateCubeTarget(u32 size, u32 mips) const;
|
||||
void ReleaseStorageViews(const CubeTarget& t) const;
|
||||
|
||||
vk::Sampler CreateEnvSampler(u32 maxLod) const;
|
||||
vk::Sampler CreateLutSampler() const;
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
ShaderManager& m_Shaders;
|
||||
|
||||
// set 布局:A = {0 采样输入, 1 storage 输出},B = {0 storage}
|
||||
vk::DescriptorSetLayout m_IblSetLayout;
|
||||
vk::DescriptorSetLayout m_LutSetLayout;
|
||||
// 管线布局:无 push(equirect/irradiance/LUT)与 4B push(prefilter)
|
||||
vk::PipelineLayout m_IblLayout;
|
||||
vk::PipelineLayout m_IblLayoutPush;
|
||||
vk::PipelineLayout m_LutLayout;
|
||||
|
||||
vk::ShaderEXT m_ToCubeShader{};
|
||||
vk::ShaderEXT m_IrrShader{};
|
||||
vk::ShaderEXT m_PrefShader{};
|
||||
vk::ShaderEXT m_LutShader{};
|
||||
|
||||
vk::Sampler m_EquirectSampler; // S=repeat(经度环绕)T=clamp
|
||||
vk::CommandPool m_Pool; // 持久复用,每次生成 reset
|
||||
vk::CommandBuffer m_Cmd;
|
||||
vk::Fence m_Fence;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_IBLGENERATOR_H
|
||||
@@ -52,6 +52,10 @@ namespace Engine {
|
||||
kDebugShadowMap0, // CSM 级联 0 深度(近=白)
|
||||
kDebugShadowMap1, // 级联 1
|
||||
kDebugShadowMap2, // 级联 2
|
||||
kDebugEnvCube, // IBL 环境 cube(经纬展开)
|
||||
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
|
||||
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
|
||||
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
|
||||
kDebugViewCount
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
|
||||
#include <Engine/RenderGraph/RenderGraph.h>
|
||||
|
||||
namespace Engine {
|
||||
class Cubemap;
|
||||
}
|
||||
|
||||
namespace Engine::Passes {
|
||||
|
||||
class RenderPass {
|
||||
@@ -27,6 +31,10 @@ namespace Engine::Passes {
|
||||
|
||||
virtual void Register(RG::RenderGraph& rg) = 0;
|
||||
virtual void OnTargetsRebuilt(RG::RenderGraph& rg) { (void)rg; }
|
||||
|
||||
// 环境资产换绑(动态切换):引用环境描述符的 pass 重绑自己的 set。
|
||||
// 默认无环境影响;env 恒非空(Renderer 有中性兜底)
|
||||
virtual void OnEnvironmentChanged(const Cubemap* /*env*/) {}
|
||||
};
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Engine {
|
||||
SkyboxPass& operator=(const SkyboxPass&) = delete;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnEnvironmentChanged(const Cubemap* env) override;
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
|
||||
@@ -15,39 +15,50 @@
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Passes {
|
||||
namespace Engine {
|
||||
|
||||
class TonemapPass final : public RenderPass {
|
||||
public:
|
||||
TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
|
||||
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer,
|
||||
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
|
||||
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
|
||||
RG::TextureHandle shadowMap2);
|
||||
~TonemapPass() override = default;
|
||||
class Cubemap;
|
||||
class DescriptorManager;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
namespace Passes {
|
||||
|
||||
private:
|
||||
FullscreenGpu& m_Gpu;
|
||||
PostProcessParams& m_Params;
|
||||
FrameSceneData& m_Frame; // debugView / bloomEnabled(帧契约)
|
||||
RG::TextureHandle m_HDR;
|
||||
RG::TextureHandle m_A;
|
||||
RG::TextureHandle m_TemporalDiff;
|
||||
RG::TextureHandle m_Backbuffer;
|
||||
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
|
||||
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
|
||||
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
|
||||
RG::TextureHandle m_Velocity;
|
||||
RG::TextureHandle m_ShadowMaps[3];
|
||||
class TonemapPass final : public RenderPass {
|
||||
public:
|
||||
TonemapPass(FullscreenGpu& gpu, DescriptorManager& descriptors,
|
||||
PostProcessParams& params,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
|
||||
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer,
|
||||
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
|
||||
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
|
||||
RG::TextureHandle shadowMap2);
|
||||
~TonemapPass() override = default;
|
||||
|
||||
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
||||
};
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
void OnEnvironmentChanged(const Cubemap* env) override;
|
||||
|
||||
} // namespace Engine::Passes
|
||||
private:
|
||||
FullscreenGpu& m_Gpu;
|
||||
DescriptorManager& m_Descriptors;
|
||||
PostProcessParams& m_Params;
|
||||
FrameSceneData& m_Frame; // debugView / bloomEnabled(帧契约)
|
||||
RG::TextureHandle m_HDR;
|
||||
RG::TextureHandle m_A;
|
||||
RG::TextureHandle m_TemporalDiff;
|
||||
RG::TextureHandle m_Backbuffer;
|
||||
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
|
||||
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
|
||||
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
|
||||
RG::TextureHandle m_Velocity;
|
||||
RG::TextureHandle m_ShadowMaps[3];
|
||||
// ---- 环境调试视图源(binding 8-11;随环境切换重绑)----
|
||||
const Cubemap* m_Env = nullptr;
|
||||
|
||||
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_TONEMAPPASS_H
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Engine {
|
||||
class SceneRenderer;
|
||||
class VulkanAssetUploader;
|
||||
class Cubemap;
|
||||
class IblGenerator;
|
||||
class ImGuiLayer;
|
||||
|
||||
namespace ECS { class Scene; }
|
||||
@@ -95,6 +96,12 @@ namespace Engine {
|
||||
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
|
||||
const std::vector<const char*>& GetDebugViewNames() const;
|
||||
|
||||
// ---- 动态环境切换(资产归 Assets 层,Renderer 只消费)----
|
||||
// env 传 AssetManager::GetEnvironment(ref) 的指针;nullptr 回中性兜底。
|
||||
// 内部 waitIdle 后统一换绑(引用环境描述符的 pass 全部同步);
|
||||
// compute 预积分已把生成成本压到毫秒级,waitIdle 尾巴 ≈ 单帧
|
||||
void SetEnvironment(const Cubemap* env);
|
||||
|
||||
// pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应)
|
||||
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
|
||||
const std::vector<f32>& GetPassGpuMs() const { return m_PassGpuMs; }
|
||||
@@ -124,7 +131,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 引用;
|
||||
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
|
||||
Scope<IblGenerator> m_Ibl;
|
||||
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
|
||||
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
||||
Scope<Cubemap> m_Environment;
|
||||
Scope<Swapchain> m_Swapchain;
|
||||
|
||||
@@ -19,23 +19,28 @@ namespace Engine {
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
class DescriptorManager;
|
||||
class IblGenerator;
|
||||
|
||||
class VulkanAssetUploader final : public Assets::IAssetUploader {
|
||||
public:
|
||||
VulkanAssetUploader(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
vk::DescriptorSetLayout textureLayout)
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
IblGenerator& ibl)
|
||||
: m_Device(device), m_Allocator(allocator),
|
||||
m_Descriptors(descriptors), m_TextureLayout(textureLayout) {}
|
||||
m_Descriptors(descriptors), m_TextureLayout(textureLayout),
|
||||
m_Ibl(ibl) {}
|
||||
|
||||
Scope<Model> LoadModel(const std::string& path) override;
|
||||
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
vk::DescriptorSetLayout m_TextureLayout;
|
||||
IblGenerator& m_Ibl; // 环境 IBL 预积分的 compute 生成端
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -40,17 +40,25 @@ namespace Engine {
|
||||
void UpdateTexture(vk::DescriptorSet set, u32 binding,
|
||||
vk::Sampler sampler, vk::ImageView view) const;
|
||||
|
||||
// storage image 写入(compute 输出;layout = eGeneral)
|
||||
void UpdateStorageImage(vk::DescriptorSet set, u32 binding,
|
||||
vk::ImageView view) const;
|
||||
|
||||
// 单个 set layout 的旧接口(保留)
|
||||
vk::PipelineLayout CreatePipelineLayout(vk::DescriptorSetLayout layout,
|
||||
u32 pushConstantSize) const;
|
||||
|
||||
// 多 set layout 的新接口。
|
||||
// push constant range 按程序级 vertex|fragment 声明(与
|
||||
// push constant range 按程序级声明(默认 vertex|fragment,与
|
||||
// ShaderManager 的 shader EXT 声明一致,VUID-08878);
|
||||
// push 调用点必须带同样的双 stage(VUID-01796)
|
||||
// push 调用点必须带同样的 stage(VUID-01796)。
|
||||
// compute 管线传 vk::ShaderStageFlagBits::eCompute
|
||||
vk::PipelineLayout CreatePipelineLayout(
|
||||
const std::vector<vk::DescriptorSetLayout>& layouts,
|
||||
u32 pushConstantSize) const;
|
||||
u32 pushConstantSize,
|
||||
vk::ShaderStageFlags pushStages =
|
||||
vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment) const;
|
||||
|
||||
private:
|
||||
Device& m_Device;
|
||||
|
||||
@@ -30,4 +30,11 @@ namespace Engine::Assets {
|
||||
[&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
|
||||
}
|
||||
|
||||
EnvRef AssetManager::LoadEnvironment(const std::string& path)
|
||||
{
|
||||
// GPU 生成(compute 预积分)经接口发生在渲染层实现里
|
||||
return m_Environments.Load(path,
|
||||
[this](const std::string& p) { return m_Uploader.LoadEnvironment(p); });
|
||||
}
|
||||
|
||||
} // namespace Engine::Assets
|
||||
|
||||
@@ -1,753 +1,17 @@
|
||||
//
|
||||
// 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 偏置),纯数学表
|
||||
// Cubemap 实现:纯资源持有(构造注入 + 成对析构)。
|
||||
// 预积分生成见 IblGenerator(compute 链);CPU 积分路径已整体移除,
|
||||
// 环境切换从秒级阻塞降到毫秒级 GPU 提交。
|
||||
//
|
||||
|
||||
#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();
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/24.
|
||||
//
|
||||
// IblGenerator 实现:一次性 compute 提交完成全部预积分。
|
||||
//
|
||||
// 同步与生命周期模型:
|
||||
// - 提交用独立 command buffer + fence,与帧管线(RG 提交)互不干扰;
|
||||
// - fence 等待后所有产物 layout 恒为 eShaderReadOnlyOptimal,后续
|
||||
// 帧提交经队列序自动可见(无需额外 barrier);
|
||||
// - 中间资源(equirect 纹理 / staging / storage view)用完即毁;
|
||||
// - 描述符集不回收(growable pool 吸收;环境加载是低频事件)。
|
||||
//
|
||||
// 计时打点输出各段耗时,作为 CPU→GPU 迁移的前后对比依据。
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/IblGenerator.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 <stb/stb_image.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr u32 kFaceCount = 6;
|
||||
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
|
||||
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
|
||||
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
|
||||
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
|
||||
constexpr u32 kGroupSize = 8; // compute local_size(shader 同步)
|
||||
|
||||
f32 MsSince(const std::chrono::steady_clock::time_point& t0)
|
||||
{
|
||||
return std::chrono::duration<f32, std::milli>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
}
|
||||
}
|
||||
|
||||
IblGenerator::IblGenerator(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
ShaderManager& shaders)
|
||||
: m_Device(device), m_Allocator(allocator),
|
||||
m_Descriptors(descriptors), m_Shaders(shaders)
|
||||
{
|
||||
// Set A:{0 采样输入(cube/2D 共用 combined 槽),1 storage 输出}
|
||||
const std::vector<DescriptorBinding> iblBindings = {
|
||||
{0, vk::DescriptorType::eCombinedImageSampler,
|
||||
vk::ShaderStageFlagBits::eCompute},
|
||||
{1, vk::DescriptorType::eStorageImage,
|
||||
vk::ShaderStageFlagBits::eCompute},
|
||||
};
|
||||
m_IblSetLayout = descriptors.CreateLayout(iblBindings);
|
||||
|
||||
// Set B:LUT 只有 storage 输出
|
||||
const std::vector<DescriptorBinding> lutBindings = {
|
||||
{0, vk::DescriptorType::eStorageImage,
|
||||
vk::ShaderStageFlagBits::eCompute},
|
||||
};
|
||||
m_LutSetLayout = descriptors.CreateLayout(lutBindings);
|
||||
|
||||
// 管线布局:push 仅 prefilter 用(compute 单独按 eCompute 声明)
|
||||
m_IblLayout = descriptors.CreatePipelineLayout(
|
||||
{m_IblSetLayout}, 0, vk::ShaderStageFlagBits::eCompute);
|
||||
m_IblLayoutPush = descriptors.CreatePipelineLayout(
|
||||
{m_IblSetLayout}, sizeof(f32), vk::ShaderStageFlagBits::eCompute);
|
||||
m_LutLayout = descriptors.CreatePipelineLayout(
|
||||
{m_LutSetLayout}, 0, vk::ShaderStageFlagBits::eCompute);
|
||||
|
||||
const std::string dir = "shaders/";
|
||||
m_ToCubeShader = shaders.CreateFromFile(
|
||||
dir + "ibl_equirect_to_cube.comp.spv",
|
||||
vk::ShaderStageFlagBits::eCompute,
|
||||
{m_IblSetLayout}).Handle;
|
||||
m_IrrShader = shaders.CreateFromFile(
|
||||
dir + "ibl_irradiance.comp.spv",
|
||||
vk::ShaderStageFlagBits::eCompute,
|
||||
{m_IblSetLayout}).Handle;
|
||||
m_PrefShader = shaders.CreateFromFile(
|
||||
dir + "ibl_prefilter.comp.spv",
|
||||
vk::ShaderStageFlagBits::eCompute,
|
||||
{m_IblSetLayout}, sizeof(f32)).Handle;
|
||||
m_LutShader = shaders.CreateFromFile(
|
||||
dir + "ibl_brdf_lut.comp.spv",
|
||||
vk::ShaderStageFlagBits::eCompute,
|
||||
{m_LutSetLayout}).Handle;
|
||||
|
||||
if (!m_ToCubeShader || !m_IrrShader || !m_PrefShader || !m_LutShader) {
|
||||
ENGINE_FATAL("IblGenerator: failed to create compute shaders");
|
||||
}
|
||||
|
||||
// equirect 中间纹理采样器:硬件双线性,S=repeat 吃掉经度接缝
|
||||
vk::SamplerCreateInfo sInfo;
|
||||
sInfo.setMagFilter(vk::Filter::eLinear)
|
||||
.setMinFilter(vk::Filter::eLinear)
|
||||
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
|
||||
.setAddressModeU(vk::SamplerAddressMode::eRepeat)
|
||||
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
|
||||
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge);
|
||||
m_EquirectSampler = device.GetDevice().createSampler(sInfo);
|
||||
|
||||
// 持久命令设施:每次生成 reset 复用(避免反复建池)
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient |
|
||||
vk::CommandPoolCreateFlagBits::eResetCommandBuffer);
|
||||
m_Pool = device.GetDevice().createCommandPool(poolInfo);
|
||||
m_Cmd = device.GetDevice().allocateCommandBuffers(
|
||||
vk::CommandBufferAllocateInfo{}
|
||||
.setCommandPool(m_Pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1))[0];
|
||||
m_Fence = device.GetDevice().createFence(
|
||||
vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled});
|
||||
}
|
||||
|
||||
IblGenerator::~IblGenerator()
|
||||
{
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
|
||||
for (const vk::ShaderEXT s : {m_ToCubeShader, m_IrrShader,
|
||||
m_PrefShader, m_LutShader}) {
|
||||
if (s) vkDevice.destroyShaderEXT(s);
|
||||
}
|
||||
vkDevice.destroySampler(m_EquirectSampler);
|
||||
vkDevice.destroyPipelineLayout(m_IblLayout);
|
||||
vkDevice.destroyPipelineLayout(m_IblLayoutPush);
|
||||
vkDevice.destroyPipelineLayout(m_LutLayout);
|
||||
vkDevice.destroyDescriptorSetLayout(m_IblSetLayout);
|
||||
vkDevice.destroyDescriptorSetLayout(m_LutSetLayout);
|
||||
vkDevice.destroyFence(m_Fence);
|
||||
vkDevice.freeCommandBuffers(m_Pool, m_Cmd);
|
||||
vkDevice.destroyCommandPool(m_Pool);
|
||||
}
|
||||
|
||||
// ---- cube 兼容目标:storage(2DArray per-mip view)+ 采样(eCube view)----
|
||||
|
||||
IblGenerator::CubeTarget IblGenerator::CreateCubeTarget(u32 size, u32 mips) const
|
||||
{
|
||||
// fp16 storage 支持(桌面 GPU 普遍具备,显式校验防退化平台)
|
||||
const vk::Format format = m_Device.FindSupportedFormat(
|
||||
{vk::Format::eR16G16B16A16Sfloat},
|
||||
vk::ImageTiling::eOptimal,
|
||||
vk::FormatFeatureFlagBits::eStorageImage);
|
||||
|
||||
vk::ImageCreateInfo info;
|
||||
info.setImageType(vk::ImageType::e2D)
|
||||
.setFormat(format)
|
||||
.setExtent({size, size, 1})
|
||||
.setMipLevels(mips)
|
||||
.setArrayLayers(kFaceCount)
|
||||
.setSamples(vk::SampleCountFlagBits::e1)
|
||||
.setTiling(vk::ImageTiling::eOptimal)
|
||||
.setFlags(vk::ImageCreateFlagBits::eCubeCompatible)
|
||||
.setUsage(vk::ImageUsageFlagBits::eStorage |
|
||||
vk::ImageUsageFlagBits::eSampled)
|
||||
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||
|
||||
CubeTarget t;
|
||||
t.allocation = m_Allocator.CreateImage(info,
|
||||
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||
t.image = t.allocation.Image;
|
||||
|
||||
// storage 写:2DArray view,每 mip 一个(storage 必须单 mip 层)
|
||||
for (u32 mip = 0; mip < mips; ++mip) {
|
||||
t.storageViews.push_back(m_Device.GetDevice().createImageView(
|
||||
vk::ImageViewCreateInfo{}
|
||||
.setImage(t.image)
|
||||
.setViewType(vk::ImageViewType::e2DArray)
|
||||
.setFormat(format)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
mip, 1, 0, kFaceCount})));
|
||||
}
|
||||
// 采样读:eCube view 全 mip 链(消费端契约与原 CPU 路径一致)
|
||||
t.cubeView = m_Device.GetDevice().createImageView(
|
||||
vk::ImageViewCreateInfo{}
|
||||
.setImage(t.image)
|
||||
.setViewType(vk::ImageViewType::eCube)
|
||||
.setFormat(format)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, mips, 0, kFaceCount}));
|
||||
return t;
|
||||
}
|
||||
|
||||
// 只释放 storage 中转 view(image/cubeView/显存交给 Cubemap 接管)
|
||||
void IblGenerator::ReleaseStorageViews(const CubeTarget& t) const
|
||||
{
|
||||
for (const vk::ImageView v : t.storageViews) {
|
||||
m_Device.GetDevice().destroyImageView(v);
|
||||
}
|
||||
}
|
||||
|
||||
Scope<Cubemap> IblGenerator::FromHdrEquirect(const std::string& path,
|
||||
u32 faceSize)
|
||||
{
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
|
||||
int w = 0, h = 0, channels = 0;
|
||||
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
|
||||
if (!pixels) {
|
||||
ENGINE_ERROR("IblGenerator: failed to load HDR: {}", path);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const f32 decodeMs = MsSince(t0);
|
||||
auto cubemap = Generate(pixels, static_cast<u32>(w),
|
||||
static_cast<u32>(h), faceSize, path);
|
||||
stbi_image_free(pixels);
|
||||
|
||||
ENGINE_INFO("IblGenerator: HDR decode {:.2f} ms",
|
||||
decodeMs);
|
||||
return cubemap;
|
||||
}
|
||||
|
||||
Scope<Cubemap> IblGenerator::Neutral(const glm::vec3& rgb)
|
||||
{
|
||||
// 1×1 常数 equirect:全链复用(LUT 仍是真实积分,纯数学)
|
||||
const f32 pixel[4] = {rgb.r, rgb.g, rgb.b, 1.0f};
|
||||
return Generate(pixel, 1, 1, 1, "neutral");
|
||||
}
|
||||
|
||||
Scope<Cubemap> IblGenerator::Generate(const f32* pixels, u32 w, u32 h,
|
||||
u32 faceSize,
|
||||
const std::string& debugName)
|
||||
{
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
|
||||
// ---- 1. equirect 中间纹理(R32G32B32A32,免 CPU fp16 转换)----
|
||||
const vk::DeviceSize eqBytes =
|
||||
static_cast<vk::DeviceSize>(w) * h * 16;
|
||||
auto staging = m_Allocator.CreateBuffer(
|
||||
eqBytes,
|
||||
vk::BufferUsageFlagBits::eTransferSrc,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
std::memcpy(staging.MappedPtr, pixels, static_cast<size_t>(eqBytes));
|
||||
|
||||
vk::ImageCreateInfo eqInfo;
|
||||
eqInfo.setImageType(vk::ImageType::e2D)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setExtent({w, h, 1})
|
||||
.setMipLevels(1)
|
||||
.setArrayLayers(1)
|
||||
.setSamples(vk::SampleCountFlagBits::e1)
|
||||
.setTiling(vk::ImageTiling::eOptimal)
|
||||
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
|
||||
vk::ImageUsageFlagBits::eSampled)
|
||||
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||
auto eqAlloc = m_Allocator.CreateImage(eqInfo,
|
||||
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||
vk::ImageView eqView = vkDevice.createImageView(
|
||||
vk::ImageViewCreateInfo{}
|
||||
.setImage(eqAlloc.Image)
|
||||
.setViewType(vk::ImageViewType::e2D)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, 1}));
|
||||
|
||||
// ---- 2. 输出资源 + 消费端采样器 ----
|
||||
CubeTarget env = CreateCubeTarget(faceSize, 1);
|
||||
CubeTarget irr = CreateCubeTarget(kIrradianceFaceSize, 1);
|
||||
CubeTarget pref = CreateCubeTarget(kPrefilterFaceSize, kPrefilterMips);
|
||||
|
||||
const vk::Sampler envSampler = CreateEnvSampler(kPrefilterMips - 1);
|
||||
const vk::Sampler lutSampler = CreateLutSampler();
|
||||
|
||||
const vk::Format lutFormat = m_Device.FindSupportedFormat(
|
||||
{vk::Format::eR16G16Sfloat},
|
||||
vk::ImageTiling::eOptimal,
|
||||
vk::FormatFeatureFlagBits::eStorageImage);
|
||||
vk::ImageCreateInfo lutInfo;
|
||||
lutInfo.setImageType(vk::ImageType::e2D)
|
||||
.setFormat(lutFormat)
|
||||
.setExtent({kBrdfLutSize, kBrdfLutSize, 1})
|
||||
.setMipLevels(1)
|
||||
.setArrayLayers(1)
|
||||
.setSamples(vk::SampleCountFlagBits::e1)
|
||||
.setTiling(vk::ImageTiling::eOptimal)
|
||||
.setUsage(vk::ImageUsageFlagBits::eStorage |
|
||||
vk::ImageUsageFlagBits::eSampled)
|
||||
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||
auto lutAlloc = m_Allocator.CreateImage(lutInfo,
|
||||
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||
vk::ImageView lutView = vkDevice.createImageView(
|
||||
vk::ImageViewCreateInfo{}
|
||||
.setImage(lutAlloc.Image)
|
||||
.setViewType(vk::ImageViewType::e2D)
|
||||
.setFormat(lutFormat)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, 1}));
|
||||
|
||||
const auto resourceMs = MsSince(t0);
|
||||
|
||||
// ---- 3. 描述符(低频事件,growable pool 吸收,不回收)----
|
||||
vk::DescriptorSet setToCube =
|
||||
m_Descriptors.AllocateDescriptorSet(m_IblSetLayout);
|
||||
vk::DescriptorSet setIrr =
|
||||
m_Descriptors.AllocateDescriptorSet(m_IblSetLayout);
|
||||
vk::DescriptorSet setLut =
|
||||
m_Descriptors.AllocateDescriptorSet(m_LutSetLayout);
|
||||
std::array<vk::DescriptorSet, kPrefilterMips> setPref{};
|
||||
for (u32 m = 0; m < kPrefilterMips; ++m) {
|
||||
setPref[m] = m_Descriptors.AllocateDescriptorSet(m_IblSetLayout);
|
||||
}
|
||||
|
||||
m_Descriptors.UpdateTexture(setToCube, 0, m_EquirectSampler, eqView);
|
||||
m_Descriptors.UpdateStorageImage(setToCube, 1, env.storageViews[0]);
|
||||
// cube 采样必须用带 mip 的 env 采样器(textureLod 被 clamp 到 maxLod)
|
||||
m_Descriptors.UpdateTexture(setIrr, 0, envSampler, env.cubeView);
|
||||
m_Descriptors.UpdateStorageImage(setIrr, 1, irr.storageViews[0]);
|
||||
for (u32 m = 0; m < kPrefilterMips; ++m) {
|
||||
m_Descriptors.UpdateTexture(setPref[m], 0, envSampler, env.cubeView);
|
||||
m_Descriptors.UpdateStorageImage(setPref[m], 1, pref.storageViews[m]);
|
||||
}
|
||||
m_Descriptors.UpdateStorageImage(setLut, 0, lutView);
|
||||
|
||||
// ---- 4. 录制 + 一次性提交 ----
|
||||
vkDevice.resetFences(m_Fence);
|
||||
vkDevice.resetCommandPool(m_Pool);
|
||||
m_Cmd.begin(vk::CommandBufferBeginInfo{
|
||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||
|
||||
// 4a. equirect staging → image → shader 读 layout
|
||||
vk::ImageMemoryBarrier2 eqToTransfer;
|
||||
eqToTransfer.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(eqAlloc.Image)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, 1});
|
||||
m_Cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(eqToTransfer));
|
||||
|
||||
vk::BufferImageCopy eqCopy;
|
||||
eqCopy.setBufferOffset(0)
|
||||
.setBufferRowLength(0)
|
||||
.setBufferImageHeight(0)
|
||||
.setImageSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1})
|
||||
.setImageOffset({0, 0, 0})
|
||||
.setImageExtent({w, h, 1});
|
||||
m_Cmd.copyBufferToImage(staging.Buffer, eqAlloc.Image,
|
||||
vk::ImageLayout::eTransferDstOptimal, eqCopy);
|
||||
|
||||
vk::ImageMemoryBarrier2 eqToShader;
|
||||
eqToShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
||||
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||
.setImage(eqAlloc.Image)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, 1});
|
||||
m_Cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(eqToShader));
|
||||
|
||||
// 4b. 输出资源 Undefined → General(compute storage 写的要求)
|
||||
std::array<vk::ImageMemoryBarrier2, 4> toGeneral{};
|
||||
const vk::Image images[4] = {env.image, irr.image, pref.image,
|
||||
lutAlloc.Image};
|
||||
const u32 mipCounts[4] = {1, 1, kPrefilterMips, 1};
|
||||
for (u32 i = 0; i < 4; ++i) {
|
||||
toGeneral[i]
|
||||
.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
||||
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||
.setDstAccessMask(vk::AccessFlagBits2::eShaderWrite)
|
||||
.setOldLayout(vk::ImageLayout::eUndefined)
|
||||
.setNewLayout(vk::ImageLayout::eGeneral)
|
||||
.setImage(images[i])
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, mipCounts[i], 0, kFaceCount});
|
||||
}
|
||||
toGeneral[3].setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, 1}); // LUT 是 2D
|
||||
m_Cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(toGeneral));
|
||||
|
||||
// 4c. ① equirect → env cube
|
||||
std::array<vk::ShaderStageFlagBits, 1> computeStage = {
|
||||
vk::ShaderStageFlagBits::eCompute};
|
||||
std::array<vk::ShaderEXT, 1> toCubeShaders = {m_ToCubeShader};
|
||||
m_Cmd.bindShadersEXT(computeStage, toCubeShaders);
|
||||
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||
m_IblLayout, 0, setToCube, {});
|
||||
m_Cmd.dispatch((faceSize + kGroupSize - 1) / kGroupSize,
|
||||
(faceSize + kGroupSize - 1) / kGroupSize, kFaceCount);
|
||||
|
||||
// env 转采样读(②③的输入)
|
||||
vk::ImageMemoryBarrier2 envToRead;
|
||||
envToRead.setSrcStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eShaderWrite)
|
||||
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||
.setOldLayout(vk::ImageLayout::eGeneral)
|
||||
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||
.setImage(env.image)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, 1, 0, kFaceCount});
|
||||
m_Cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(envToRead));
|
||||
|
||||
// 4d. ② irradiance(env → irr)
|
||||
std::array<vk::ShaderEXT, 1> irrShaders = {m_IrrShader};
|
||||
m_Cmd.bindShadersEXT(computeStage, irrShaders);
|
||||
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||
m_IblLayout, 0, setIrr, {});
|
||||
m_Cmd.dispatch((kIrradianceFaceSize + kGroupSize - 1) / kGroupSize,
|
||||
(kIrradianceFaceSize + kGroupSize - 1) / kGroupSize,
|
||||
kFaceCount);
|
||||
|
||||
// 4e. ③ prefilter(env → pref,每 mip 一次 dispatch + push roughness)
|
||||
std::array<vk::ShaderEXT, 1> prefShaders = {m_PrefShader};
|
||||
m_Cmd.bindShadersEXT(computeStage, prefShaders);
|
||||
for (u32 m = 0; m < kPrefilterMips; ++m) {
|
||||
const u32 dim = kPrefilterFaceSize >> m;
|
||||
const f32 roughness =
|
||||
static_cast<f32>(m) / static_cast<f32>(kPrefilterMips - 1);
|
||||
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||
m_IblLayoutPush, 0, setPref[m], {});
|
||||
m_Cmd.pushConstants<f32>(m_IblLayoutPush,
|
||||
vk::ShaderStageFlagBits::eCompute,
|
||||
0, roughness);
|
||||
m_Cmd.dispatch((dim + kGroupSize - 1) / kGroupSize,
|
||||
(dim + kGroupSize - 1) / kGroupSize, kFaceCount);
|
||||
}
|
||||
|
||||
// 4f. ④ BRDF LUT(独立纯数学)
|
||||
std::array<vk::ShaderEXT, 1> lutShaders = {m_LutShader};
|
||||
m_Cmd.bindShadersEXT(computeStage, lutShaders);
|
||||
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||
m_LutLayout, 0, setLut, {});
|
||||
m_Cmd.dispatch((kBrdfLutSize + kGroupSize - 1) / kGroupSize,
|
||||
(kBrdfLutSize + kGroupSize - 1) / kGroupSize, 1);
|
||||
|
||||
// 4g. irr/pref/lut 收尾:General → ShaderReadOnly(消费端采样 layout;
|
||||
// fence 等待后对后续队列提交自动可见)
|
||||
std::array<vk::ImageMemoryBarrier2, 3> toRead{};
|
||||
const vk::Image readImages[3] = {irr.image, pref.image,
|
||||
lutAlloc.Image};
|
||||
const u32 readMips[3] = {1, kPrefilterMips, 1};
|
||||
const u32 readLayers[3] = {kFaceCount, kFaceCount, 1}; // LUT 是 2D
|
||||
for (u32 i = 0; i < 3; ++i) {
|
||||
toRead[i]
|
||||
.setSrcStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eShaderWrite)
|
||||
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||
.setOldLayout(vk::ImageLayout::eGeneral)
|
||||
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||
.setImage(readImages[i])
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, readMips[i], 0, readLayers[i]});
|
||||
}
|
||||
m_Cmd.pipelineBarrier2(
|
||||
vk::DependencyInfo{}.setImageMemoryBarriers(toRead));
|
||||
|
||||
m_Cmd.end();
|
||||
|
||||
m_Device.GetGraphicsQueue().submit(
|
||||
vk::SubmitInfo{}.setCommandBuffers(m_Cmd), m_Fence);
|
||||
if (vkDevice.waitForFences(m_Fence, VK_TRUE, UINT64_MAX) !=
|
||||
vk::Result::eSuccess) {
|
||||
ENGINE_ERROR("IblGenerator: waitForFences failed ({})", debugName);
|
||||
}
|
||||
|
||||
// ---- 5. 中间资源即毁;image/cubeView/显存交棒 Cubemap ----
|
||||
vkDevice.destroyImageView(eqView);
|
||||
m_Allocator.Destroy(eqAlloc);
|
||||
m_Allocator.Destroy(staging);
|
||||
ReleaseStorageViews(env);
|
||||
ReleaseStorageViews(irr);
|
||||
ReleaseStorageViews(pref);
|
||||
|
||||
const f32 computeMs = MsSince(t0) - resourceMs;
|
||||
ENGINE_INFO("IblGenerator[{}]: {}x6 env, {}x6 irradiance, "
|
||||
"{}x6 prefilter {} mips, {}² BRDF LUT — "
|
||||
"resources {:.2f} ms, compute+wait {:.2f} ms",
|
||||
debugName, faceSize, kIrradianceFaceSize,
|
||||
kPrefilterFaceSize, kPrefilterMips, kBrdfLutSize,
|
||||
resourceMs, computeMs);
|
||||
|
||||
return Scope<Cubemap>(new Cubemap(
|
||||
m_Device, m_Allocator,
|
||||
EnvTexture{env.image, env.cubeView, env.allocation},
|
||||
EnvTexture{irr.image, irr.cubeView, irr.allocation},
|
||||
EnvTexture{pref.image, pref.cubeView, pref.allocation},
|
||||
EnvTexture{lutAlloc.Image, lutView, lutAlloc},
|
||||
envSampler, lutSampler));
|
||||
}
|
||||
|
||||
vk::Sampler IblGenerator::CreateEnvSampler(u32 maxLod) const
|
||||
{
|
||||
vk::SamplerCreateInfo sInfo;
|
||||
sInfo.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 m_Device.GetDevice().createSampler(sInfo);
|
||||
}
|
||||
|
||||
vk::Sampler IblGenerator::CreateLutSampler() const
|
||||
{
|
||||
// clamp-to-edge:共享 border 白采样器会在 uv=1.0 处混入白色 border
|
||||
return CreateEnvSampler(0);
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
@@ -21,7 +21,8 @@ namespace Engine::Passes
|
||||
// 后 6 个 binding 供 Tonemap 调试视图使用;其余全屏 shader
|
||||
// 静态使用的 binding 是布局的子集,合法
|
||||
std::vector<DescriptorBinding> bindings;
|
||||
for (u32 b = 0; b < 8; ++b) {
|
||||
// 0-7 帧 RG 中间目标 + 8-11 环境调试视图(cube 三件套 + LUT)
|
||||
for (u32 b = 0; b < 12; ++b) {
|
||||
bindings.push_back(
|
||||
{b, vk::DescriptorType::eCombinedImageSampler,
|
||||
vk::ShaderStageFlagBits::eFragment});
|
||||
|
||||
@@ -74,6 +74,20 @@ namespace Engine::Passes
|
||||
vkDevice.destroyDescriptorSetLayout(m_SetLayout);
|
||||
}
|
||||
|
||||
void SkyboxPass::OnEnvironmentChanged(const Cubemap* env)
|
||||
{
|
||||
m_Env = env;
|
||||
if (!m_Env) return;
|
||||
|
||||
// cube view 全程稳定语义是"单个环境内";切换环境 = 新 view,
|
||||
// per-slot set 全部重绑
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Descriptors.UpdateTexture(m_Sets[slot], 0,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetView());
|
||||
}
|
||||
}
|
||||
|
||||
void SkyboxPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
rg.AddPass("Skybox", [this](RG::RenderGraphContext& ctx) {
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/TonemapPass.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Core/FrameContext.h>
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
TonemapPass::TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
|
||||
TonemapPass::TonemapPass(FullscreenGpu& gpu, DescriptorManager& descriptors,
|
||||
PostProcessParams& params,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
|
||||
RG::TextureHandle temporalDiff,
|
||||
@@ -17,7 +20,7 @@ namespace Engine::Passes
|
||||
RG::TextureHandle shadowMap0,
|
||||
RG::TextureHandle shadowMap1,
|
||||
RG::TextureHandle shadowMap2)
|
||||
: m_Gpu(gpu), m_Params(params), m_Frame(frame),
|
||||
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
|
||||
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
|
||||
m_Backbuffer(backbuffer),
|
||||
m_RawHDR(rawHdr), m_Velocity(velocity),
|
||||
@@ -27,6 +30,8 @@ namespace Engine::Passes
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Sets[slot] = m_Gpu.AllocateSet();
|
||||
}
|
||||
// 环境调试视图源(binding 8-11)由 OnEnvironmentChanged 填充
|
||||
//(Renderer 构造尾部统一广播一次)
|
||||
}
|
||||
|
||||
void TonemapPass::Register(RG::RenderGraph& rg)
|
||||
@@ -80,4 +85,27 @@ namespace Engine::Passes
|
||||
}
|
||||
}
|
||||
|
||||
void TonemapPass::OnEnvironmentChanged(const Cubemap* env)
|
||||
{
|
||||
m_Env = env;
|
||||
if (!m_Env) return;
|
||||
|
||||
// 环境调试视图源(binding 8-11):cube 三件套 + LUT。
|
||||
// 静态绑定超集模式——只在切换环境时重绑,每帧零成本
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Descriptors.UpdateTexture(m_Sets[slot], 8,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetView());
|
||||
m_Descriptors.UpdateTexture(m_Sets[slot], 9,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetIrradianceView());
|
||||
m_Descriptors.UpdateTexture(m_Sets[slot], 10,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetPrefilteredView());
|
||||
m_Descriptors.UpdateTexture(m_Sets[slot], 11,
|
||||
m_Env->GetBrdfLutSampler(),
|
||||
m_Env->GetBrdfLutView());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <Engine/Renderer/Passes/ShadowPass.h>
|
||||
#include <Engine/Renderer/Passes/ForwardPass.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Renderer/IblGenerator.h>
|
||||
#include <Engine/Renderer/Passes/TAAPass.h>
|
||||
#include <Engine/Renderer/Passes/BloomPass.h>
|
||||
#include <Engine/Renderer/Passes/TonemapPass.h>
|
||||
@@ -40,7 +41,6 @@
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
|
||||
namespace Engine
|
||||
@@ -74,17 +74,12 @@ 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));
|
||||
}
|
||||
// 3.5 环境预积分生成端(compute 链)+ 中性兜底四件套。
|
||||
// 真实环境由 Application 经 AssetManager 加载后 SetEnvironment
|
||||
// (资产路径归资产层,Renderer 不感知具体文件)
|
||||
m_Ibl = CreateScope<IblGenerator>(
|
||||
*m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager);
|
||||
m_Environment = m_Ibl->Neutral(glm::vec3(0.1f));
|
||||
|
||||
// 4. Swapchain(只管呈现;渲染目标统一归 RenderGraph)
|
||||
m_Swapchain = CreateScope<Swapchain>(*m_Device, *m_Allocator, m_Window);
|
||||
@@ -101,7 +96,7 @@ namespace Engine
|
||||
// 7. 资产上传服务(渲染层对外提供的 IAssetUploader 实现)
|
||||
m_AssetUploader = CreateScope<VulkanAssetUploader>(
|
||||
*m_Device, *m_Allocator, *m_DescriptorManager,
|
||||
m_SceneGpu->textureSetLayout);
|
||||
m_SceneGpu->textureSetLayout, *m_Ibl);
|
||||
|
||||
// 8. RenderGraph:资源声明 + pass 注册 + 首次编译
|
||||
m_PostParams = CreateScope<Passes::PostProcessParams>();
|
||||
@@ -111,8 +106,12 @@ namespace Engine
|
||||
for (auto& pass : m_Passes) {
|
||||
pass->OnTargetsRebuilt(*m_RenderGraph);
|
||||
}
|
||||
// 环境辐照度绑定(view 全程稳定,绑一次)
|
||||
// 环境辐照度绑定(view 在单个环境内全程稳定,绑一次)
|
||||
m_SceneGpu->BindEnvironment(*m_Environment);
|
||||
// 环境消费端 pass(Skybox 背景 / Tonemap 调试视图)同步初始绑定
|
||||
for (auto& pass : m_Passes) {
|
||||
pass->OnEnvironmentChanged(m_Environment.get());
|
||||
}
|
||||
|
||||
// 9. GPU timestamp:每 pass 两枚(前后),按帧槽位分区
|
||||
m_QueriesPerFrame = static_cast<u32>(m_RenderGraph->GetPassCount()) * 2;
|
||||
@@ -185,13 +184,28 @@ namespace Engine
|
||||
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
|
||||
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
|
||||
|
||||
void Renderer::SetEnvironment(const Cubemap* env)
|
||||
{
|
||||
if (!env) env = m_Environment.get();
|
||||
|
||||
// 描述符 host 更新要求 set 不被 in-flight 帧使用(validation 严格
|
||||
// 校验):waitIdle 两槽位全停飞后统一换绑。环境切换低频,等待
|
||||
// 成本 ≈ 单帧 GPU 尾巴;生成端(compute 预积分)已在资产加载期完成
|
||||
m_Device->GetDevice().waitIdle();
|
||||
m_SceneGpu->BindEnvironment(*env);
|
||||
for (auto& pass : m_Passes) {
|
||||
pass->OnEnvironmentChanged(env);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<const char*>& Renderer::GetDebugViewNames() const
|
||||
{
|
||||
static const std::vector<const char*> kNames = {
|
||||
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
|
||||
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2",
|
||||
"EnvCube", "Irradiance", "Prefilter", "BrdfLut",
|
||||
};
|
||||
static_assert(int(Passes::kDebugViewCount) == 9,
|
||||
static_assert(int(Passes::kDebugViewCount) == 13,
|
||||
"DebugView 枚举与名表同步");
|
||||
return kNames;
|
||||
}
|
||||
@@ -285,9 +299,9 @@ namespace Engine
|
||||
*m_FullscreenGpu, *m_PostParams, frame, m_TAAColor, m_BloomA,
|
||||
m_BloomB));
|
||||
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
|
||||
*m_FullscreenGpu, *m_PostParams, frame, m_TAAColor, m_BloomA,
|
||||
m_TemporalDiff, m_Backbuffer, m_HDRColor, m_Velocity,
|
||||
m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
|
||||
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame,
|
||||
m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer, m_HDRColor,
|
||||
m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
|
||||
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
|
||||
&m_ImGuiLayer, m_Backbuffer));
|
||||
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <Engine/Renderer/VulkanAssetUploader.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/IblGenerator.h>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
@@ -20,4 +21,10 @@ namespace Engine {
|
||||
m_TextureLayout, halfSize, r, g, b);
|
||||
}
|
||||
|
||||
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
|
||||
{
|
||||
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
|
||||
return m_Ibl.FromHdrEquirect(path, 1024);
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -25,10 +25,11 @@ namespace Engine
|
||||
|
||||
vk::DescriptorPool DescriptorManager::CreateChunk() const
|
||||
{
|
||||
std::array<vk::DescriptorPoolSize, 3> poolSizes = {
|
||||
std::array<vk::DescriptorPoolSize, 4> poolSizes = {
|
||||
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kChunkSets),
|
||||
vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, kChunkSets),
|
||||
vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, kChunkSets),
|
||||
vk::DescriptorPoolSize(vk::DescriptorType::eStorageImage, kChunkSets),
|
||||
};
|
||||
|
||||
vk::DescriptorPoolCreateInfo poolInfo;
|
||||
@@ -133,6 +134,26 @@ namespace Engine
|
||||
m_Device.GetDevice().updateDescriptorSets(write, {});
|
||||
}
|
||||
|
||||
void DescriptorManager::UpdateStorageImage(const vk::DescriptorSet set,
|
||||
const u32 binding,
|
||||
const vk::ImageView view) const
|
||||
{
|
||||
// storage image 写(compute 输出):layout 必须 eGeneral
|
||||
vk::DescriptorImageInfo imageInfo;
|
||||
imageInfo.setImageView(view)
|
||||
.setImageLayout(vk::ImageLayout::eGeneral);
|
||||
|
||||
vk::WriteDescriptorSet write;
|
||||
write.setDstSet(set)
|
||||
.setDstBinding(binding)
|
||||
.setDstArrayElement(0)
|
||||
.setDescriptorType(vk::DescriptorType::eStorageImage)
|
||||
.setDescriptorCount(1)
|
||||
.setImageInfo(imageInfo);
|
||||
|
||||
m_Device.GetDevice().updateDescriptorSets(write, {});
|
||||
}
|
||||
|
||||
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(vk::DescriptorSetLayout layout,
|
||||
const u32 pushConstantSize) const
|
||||
{
|
||||
@@ -154,18 +175,18 @@ namespace Engine
|
||||
}
|
||||
|
||||
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(const std::vector<vk::DescriptorSetLayout>& layouts,
|
||||
const u32 pushConstantSize) const
|
||||
const u32 pushConstantSize,
|
||||
const vk::ShaderStageFlags pushStages) const
|
||||
{
|
||||
vk::PipelineLayoutCreateInfo info;
|
||||
info.setSetLayouts(layouts);
|
||||
|
||||
std::array<vk::PushConstantRange, 1> pushRanges;
|
||||
if (pushConstantSize > 0) {
|
||||
// 与 ShaderManager 一致:程序级 vertex|fragment 声明。
|
||||
// 与 ShaderManager 一致:程序级声明(默认 vertex|fragment)。
|
||||
// 注意 vkCmdPushConstants 的 stageFlags 必须覆盖 range 的
|
||||
// 全部 stage(VUID-01796),因此 push 调用点也要带双 stage
|
||||
pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
// 全部 stage(VUID-01796),因此 push 调用点也要带同样的 stage
|
||||
pushRanges[0].setStageFlags(pushStages)
|
||||
.setOffset(0)
|
||||
.setSize(pushConstantSize);
|
||||
info.setPushConstantRanges(pushRanges);
|
||||
|
||||
@@ -42,6 +42,7 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
|
||||
if (stage == vk::ShaderStageFlagBits::eVertex) {
|
||||
nextStage = vk::ShaderStageFlagBits::eFragment;
|
||||
}
|
||||
// compute 单阶段:无 nextStage(VUID-09183 只约束 graphics 链)
|
||||
|
||||
vk::ShaderCreateInfoEXT createInfo;
|
||||
createInfo.setStage(stage)
|
||||
@@ -60,9 +61,12 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
|
||||
// push range 是"程序级"声明:shader object 要求同一程序绑定的
|
||||
// 全部 shader(vert+frag)push range 定义完全一致(VUID-08878),
|
||||
// 因此统一按 vertex|fragment 声明;shader 实际读取哪些字节由
|
||||
// GLSL 的 push_constant block 决定,声明范围只是能力上限
|
||||
pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
// GLSL 的 push_constant block 决定,声明范围只是能力上限。
|
||||
// compute 无程序链,单独按 eCompute 声明(layout/push 调用点一致)
|
||||
pushRanges[0].setStageFlags(stage == vk::ShaderStageFlagBits::eCompute
|
||||
? vk::ShaderStageFlagBits::eCompute
|
||||
: vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.setOffset(0)
|
||||
.setSize(pushConstantSize);
|
||||
createInfo.setPushConstantRanges(pushRanges);
|
||||
|
||||
Reference in New Issue
Block a user