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:
2026-09-24 13:23:59 +08:00
parent 705250df76
commit 1d690067bd
30 changed files with 1162 additions and 824 deletions
@@ -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
+2
View File
@@ -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
+8 -19
View File
@@ -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
+10 -1
View File
@@ -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;