diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 03a8411..92d2a9c 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -33,7 +33,8 @@ file(GLOB_RECURSE SHADER_SOURCES CONFIGURE_DEPENDS # 新增 shader 时自动重配 RELATIVE ${SHADER_SRC_DIR} ${SHADER_SRC_DIR}/*.vert - ${SHADER_SRC_DIR}/*.frag) + ${SHADER_SRC_DIR}/*.frag + ${SHADER_SRC_DIR}/*.comp) foreach (f ${SHADER_SOURCES}) auto_compile_shader(${f}) diff --git a/App/main.cpp b/App/main.cpp index e9010f2..95703f7 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -30,6 +30,20 @@ using namespace Engine::ECS; namespace { + // 可切换环境(IBL 动态切换演示):路径 → AssetManager 缓存去重, + // 重复选择同一环境零成本;首选路径触发一次 compute 预积分 + constexpr const char* kEnvPaths[] = { + "assets/textures/venice_dawn_1_4k.hdr", + "assets/textures/rooitou_park_4k.hdr", + "assets/textures/pink_sunrise_4k.hdr", + "assets/textures/birchwood_4k.hdr", + }; + constexpr const char* kEnvNames[] = { + "Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood", + }; + constexpr int kEnvCount = 4; + constexpr int kDefaultEnv = 0; + class SandboxApp final : public Application { public: @@ -48,6 +62,14 @@ namespace // ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义) EnableImGui(); + // 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级), + // 其余环境在 ImGui 里按需 lazy 加载后切换 + const auto env = GetAssets().LoadEnvironment( + kEnvPaths[kDefaultEnv]); + if (env.Valid()) { + GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env)); + } + // 模型资产(PBR 验证场景:backpack 为旧式贴图资产, // 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5) const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); @@ -199,6 +221,21 @@ namespace renderer.SetBloomEnabled(bloom); } + // 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重) + ImGui::Separator(); + ImGui::Text("Environment (IBL)"); + int envIdx = m_EnvIndex; + if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount)) + { + const auto ref = GetAssets().LoadEnvironment(kEnvPaths[envIdx]); + if (ref.Valid()) + { + renderer.SetEnvironment(GetAssets().GetEnvironment(ref)); + m_EnvIndex = envIdx; + } + } + ImGui::Text("(switch = compute pre-integrate, ms-level)"); + // 调试视图:单看某个 pass 的中间输出(类 UE pass debug) const auto& viewNames = renderer.GetDebugViewNames(); int view = renderer.GetDebugView(); @@ -324,6 +361,7 @@ namespace std::vector m_Spinners; f32 m_Time = 0.0f; bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用) + int m_EnvIndex = kDefaultEnv; }; } diff --git a/Engine/include/Engine/Assets/AssetManager.h b/Engine/include/Engine/Assets/AssetManager.h index 1e2f3bf..12da4b7 100644 --- a/Engine/include/Engine/Assets/AssetManager.h +++ b/Engine/include/Engine/Assets/AssetManager.h @@ -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& Models() { return m_Models; } // 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle @@ -154,6 +159,7 @@ namespace Engine { IAssetUploader& m_Uploader; AssetCache m_Models; + AssetCache m_Environments; }; } // namespace Assets diff --git a/Engine/include/Engine/Assets/IAssetUploader.h b/Engine/include/Engine/Assets/IAssetUploader.h index 6c53c61..b81f892 100644 --- a/Engine/include/Engine/Assets/IAssetUploader.h +++ b/Engine/include/Engine/Assets/IAssetUploader.h @@ -19,6 +19,7 @@ #include #include +#include namespace Engine::Assets { @@ -31,6 +32,9 @@ namespace Engine::Assets { // 程序化纯色平面(size 为半边长) virtual Scope CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0; + + // 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr) + virtual Scope LoadEnvironment(const std::string& path) = 0; }; } // namespace Engine::Assets diff --git a/Engine/include/Engine/Core/AssetRef.h b/Engine/include/Engine/Core/AssetRef.h index 5bad523..ff7c183 100644 --- a/Engine/include/Engine/Core/AssetRef.h +++ b/Engine/include/Engine/Core/AssetRef.h @@ -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; + using EnvRef = AssetRef; // 环境(IBL 四件套) } // namespace Engine diff --git a/Engine/include/Engine/Renderer/Cubemap.h b/Engine/include/Engine/Renderer/Cubemap.h index 1af705f..04d86b6 100644 --- a/Engine/include/Engine/Renderer/Cubemap.h +++ b/Engine/include/Engine/Renderer/Cubemap.h @@ -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 #include -#include #include namespace Engine { @@ -37,18 +36,6 @@ namespace Engine { class Cubemap { public: - // 从等距柱状 HDR 生成(CPU 重采样 + irradiance/prefilter/BRDF 预积分) - static Scope CreateFromHdrEquirect(Device& device, - MemoryAllocator& allocator, - const std::string& path, - u32 faceSize); - - // 中性兜底(无 HDR 资产时):cube 均为常数色;BRDF LUT 仍为 - // 真实积分(纯数学)。保证引用侧描述符恒有效(validation) - static Scope 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) diff --git a/Engine/include/Engine/Renderer/IblGenerator.h b/Engine/include/Engine/Renderer/IblGenerator.h new file mode 100644 index 0000000..d0f6030 --- /dev/null +++ b/Engine/include/Engine/Renderer/IblGenerator.h @@ -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 +#include +#include +#include +#include +#include + +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 FromHdrEquirect(const std::string& path, u32 faceSize); + + // 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效) + Scope Neutral(const glm::vec3& rgb); + + private: + // cube 兼容 image(2D array 写 + cube 采样双 view) + struct CubeTarget { + vk::Image image{}; + std::vector storageViews; // 每 mip 一个(storage 写必须单 mip) + vk::ImageView cubeView{}; // eCube 全 mip(采样端) + MemoryAllocator::Allocation allocation; + }; + + // 从 HDR 像素数据生成(Neutral 走同一链,仅 equirect 为 1×1 常数) + Scope 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 diff --git a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h index aedbb34..9b2ad7f 100644 --- a/Engine/include/Engine/Renderer/Passes/FullscreenGpu.h +++ b/Engine/include/Engine/Renderer/Passes/FullscreenGpu.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 }; diff --git a/Engine/include/Engine/Renderer/Passes/RenderPass.h b/Engine/include/Engine/Renderer/Passes/RenderPass.h index 539cdef..87c4f40 100644 --- a/Engine/include/Engine/Renderer/Passes/RenderPass.h +++ b/Engine/include/Engine/Renderer/Passes/RenderPass.h @@ -19,6 +19,10 @@ #include +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 diff --git a/Engine/include/Engine/Renderer/Passes/SkyboxPass.h b/Engine/include/Engine/Renderer/Passes/SkyboxPass.h index f879d61..e4899aa 100644 --- a/Engine/include/Engine/Renderer/Passes/SkyboxPass.h +++ b/Engine/include/Engine/Renderer/Passes/SkyboxPass.h @@ -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; diff --git a/Engine/include/Engine/Renderer/Passes/TonemapPass.h b/Engine/include/Engine/Renderer/Passes/TonemapPass.h index 7c046dc..574dcc1 100644 --- a/Engine/include/Engine/Renderer/Passes/TonemapPass.h +++ b/Engine/include/Engine/Renderer/Passes/TonemapPass.h @@ -15,39 +15,50 @@ #include #include -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 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 m_Sets; // per-slot + }; + + } // namespace Passes +} // namespace Engine #endif //HELLOVULKAN_TONEMAPPASS_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 07876b0..9295279 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.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& GetDebugViewNames() const; + // ---- 动态环境切换(资产归 Assets 层,Renderer 只消费)---- + // env 传 AssetManager::GetEnvironment(ref) 的指针;nullptr 回中性兜底。 + // 内部 waitIdle 后统一换绑(引用环境描述符的 pass 全部同步); + // compute 预积分已把生成成本压到毫秒级,waitIdle 尾巴 ≈ 单帧 + void SetEnvironment(const Cubemap* env); + // pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应) const std::vector& GetPassNames() const { return m_PassNames; } const std::vector& GetPassGpuMs() const { return m_PassGpuMs; } @@ -124,7 +131,9 @@ namespace Engine { Scope m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) Scope m_SceneGpu; // 场景类 pass 共享装配 Scope m_FullscreenGpu; // 全屏 pass 共享装配 - // 环境贴图(IBL 阶段①):HDR → cube,被 SkyboxPass 引用; + // 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造 + Scope m_Ibl; + // 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底; // 声明在 m_Passes 之前 → 析构晚于 pass(引用安全) Scope m_Environment; Scope m_Swapchain; diff --git a/Engine/include/Engine/Renderer/VulkanAssetUploader.h b/Engine/include/Engine/Renderer/VulkanAssetUploader.h index 6760ab1..747a9bc 100644 --- a/Engine/include/Engine/Renderer/VulkanAssetUploader.h +++ b/Engine/include/Engine/Renderer/VulkanAssetUploader.h @@ -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 LoadModel(const std::string& path) override; Scope CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override; + Scope 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 diff --git a/Engine/include/Engine/Vulkan/DescriptorManager.h b/Engine/include/Engine/Vulkan/DescriptorManager.h index 1141cd4..c32bd96 100644 --- a/Engine/include/Engine/Vulkan/DescriptorManager.h +++ b/Engine/include/Engine/Vulkan/DescriptorManager.h @@ -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& layouts, - u32 pushConstantSize) const; + u32 pushConstantSize, + vk::ShaderStageFlags pushStages = + vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment) const; private: Device& m_Device; diff --git a/Engine/src/Assets/AssetManager.cpp b/Engine/src/Assets/AssetManager.cpp index 9ad6caf..7f98716 100644 --- a/Engine/src/Assets/AssetManager.cpp +++ b/Engine/src/Assets/AssetManager.cpp @@ -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 diff --git a/Engine/src/Renderer/Cubemap.cpp b/Engine/src/Renderer/Cubemap.cpp index cbc42ba..6046143 100644 --- a/Engine/src/Renderer/Cubemap.cpp +++ b/Engine/src/Renderer/Cubemap.cpp @@ -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 #include #include -#include - -#include - -#include -#include - -#include -#include -#include -#include -#include 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()) + 0.5f; - const f32 v = std::acos(glm::clamp(dir.y, -1.0f, 1.0f)) / glm::pi(); - - const f32 fx = u * static_cast(w) - 0.5f; - const f32 fy = v * static_cast(h) - 0.5f; - const u32 x0 = static_cast(std::floor(fx)); - const u32 y0 = static_cast(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(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() / static_cast(kPhiSteps); - const f32 dTheta = 0.5f * glm::pi() / static_cast(kThetaSteps); - - glm::vec3 irradiance(0.0f); - for (u32 p = 0; p < kPhiSteps; ++p) { - const f32 phi = (static_cast(p) + 0.5f) * dPhi; - for (u32 t = 0; t < kThetaSteps; ++t) { - const f32 theta = (static_cast(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(bits) * 2.3283064365386963e-10f; - } - - // n = 当次积分总采样数(xi.x = i/n 必须覆盖 [0,1] 全程) - glm::vec2 Hammersley(u32 i, u32 n) - { - return { static_cast(i) / static_cast(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() * 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(kBrdfLutSamples), - b / static_cast(kBrdfLutSamples) }; - } - - // IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf) - u16 FloatToHalf(f32 value) - { - const u32 x = std::bit_cast(value); - const u16 sign = static_cast((x >> 16) & 0x8000u); - const u32 absVal = x & 0x7fffffffu; - - if (absVal > 0x7f800000u) { - return static_cast(sign | 0x7e00u); // NaN - } - if (absVal >= 0x47800000u) { - return static_cast(sign | 0x7c00u); // Inf / 溢出饱和 - } - if (absVal >= 0x38800000u) { - const u32 bits = absVal - 0x38000000u; - const u32 round = 0x0fffu + ((bits >> 13) & 1u); - return static_cast(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(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& mips, - u32 viewMipCount) - { - const u32 mipCount = static_cast(mips.size()); - - // staging:各 mip 紧凑排布,记偏移 - vk::DeviceSize total = 0; - for (const auto& mip : mips) { - total += static_cast(mip.faceSize) * - mip.faceSize * 8 * kFaceCount; - } - - auto stagingAlloc = allocator.CreateBuffer( - total, - vk::BufferUsageFlagBits::eTransferSrc, - vk::MemoryPropertyFlagBits::eHostVisible | - vk::MemoryPropertyFlagBits::eHostCoherent); - { - auto* dst = static_cast(stagingAlloc.MappedPtr); - vk::DeviceSize off = 0; - for (const auto& mip : mips) { - const vk::DeviceSize bytes = - static_cast(mip.faceSize) * - mip.faceSize * 8 * kFaceCount; - std::memcpy(dst + off, mip.data, - static_cast(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(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(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& texels, - u32 size, vk::Format format) - { - const vk::DeviceSize total = - static_cast(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(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(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::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(w); - const u32 srcH = static_cast(h); - - // ---- 2a. 环境 cube:重采样 6 面 ---- - const u32 facePixels = faceSize * faceSize; - std::vector 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(x) + 0.5f) / static_cast(faceSize); - const f32 v = (static_cast(y) + 0.5f) / static_cast(faceSize); - const glm::vec3 rgb = - SampleEquirect(pixels, srcW, srcH, FaceDir(f, u, v)); - u16* px = dst + (static_cast(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 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(x) + 0.5f) / static_cast(irrSize); - const f32 v = (static_cast(y) + 0.5f) / static_cast(irrSize); - const glm::vec3 rgb = ConvolveIrradiance( - pixels, srcW, srcH, FaceDir(f, u, v)); - u16* px = dst + (static_cast(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 prefFaces(prefTotal); - std::vector 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(mip) / static_cast(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(x) + 0.5f) / static_cast(dim); - const f32 v = (static_cast(y) + 0.5f) / static_cast(dim); - const glm::vec3 rgb = Prefilter( - pixels, srcW, srcH, FaceDir(f, u, v), - roughness, samples); - u16* px = dst + (static_cast(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 lutTexels(kBrdfLutSize * kBrdfLutSize * 2); - for (u32 y = 0; y < kBrdfLutSize; ++y) { - const f32 roughness = (static_cast(y) + 0.5f) / static_cast(kBrdfLutSize); - for (u32 x = 0; x < kBrdfLutSize; ++x) { - const f32 nDotV = (static_cast(x) + 0.5f) / static_cast(kBrdfLutSize); - const glm::vec2 ab = IntegrateBrdf(nDotV, roughness); - u16* px = lutTexels.data() + (static_cast(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(new Cubemap(device, allocator, - env, irr, pref, lut, sampler, lutSampler)); - } - - Scope 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 faces(kFaceCount * 4, 0); - for (u32 f = 0; f < kFaceCount; ++f) { - std::memcpy(faces.data() + f * 4, px, sizeof(px)); - } - - std::vector prefFaces(kFaceCount * 4, 0); - std::vector prefMips = {{prefFaces.data(), 1}}; - - std::vector lutTexels(kBrdfLutSize * kBrdfLutSize * 2); - for (u32 y = 0; y < kBrdfLutSize; ++y) { - const f32 roughness = (static_cast(y) + 0.5f) / static_cast(kBrdfLutSize); - for (u32 x = 0; x < kBrdfLutSize; ++x) { - const f32 nDotV = (static_cast(x) + 0.5f) / static_cast(kBrdfLutSize); - const glm::vec2 ab = IntegrateBrdf(nDotV, roughness); - u16* t = lutTexels.data() + (static_cast(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(new Cubemap(device, allocator, - env, irr, pref, lut, sampler, lutSampler)); - } - Cubemap::~Cubemap() { const auto& vkDevice = m_Device.GetDevice(); diff --git a/Engine/src/Renderer/IblGenerator.cpp b/Engine/src/Renderer/IblGenerator.cpp new file mode 100644 index 0000000..c5566ec --- /dev/null +++ b/Engine/src/Renderer/IblGenerator.cpp @@ -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 +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +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( + 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 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 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 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(w), + static_cast(h), faceSize, path); + stbi_image_free(pixels); + + ENGINE_INFO("IblGenerator: HDR decode {:.2f} ms", + decodeMs); + return cubemap; + } + + Scope 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 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(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(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 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 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 computeStage = { + vk::ShaderStageFlagBits::eCompute}; + std::array 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 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 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(m) / static_cast(kPrefilterMips - 1); + m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute, + m_IblLayoutPush, 0, setPref[m], {}); + m_Cmd.pushConstants(m_IblLayoutPush, + vk::ShaderStageFlagBits::eCompute, + 0, roughness); + m_Cmd.dispatch((dim + kGroupSize - 1) / kGroupSize, + (dim + kGroupSize - 1) / kGroupSize, kFaceCount); + } + + // 4f. ④ BRDF LUT(独立纯数学) + std::array 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 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(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(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 diff --git a/Engine/src/Renderer/Passes/FullscreenGpu.cpp b/Engine/src/Renderer/Passes/FullscreenGpu.cpp index 8e3386c..7b98bf1 100644 --- a/Engine/src/Renderer/Passes/FullscreenGpu.cpp +++ b/Engine/src/Renderer/Passes/FullscreenGpu.cpp @@ -21,7 +21,8 @@ namespace Engine::Passes // 后 6 个 binding 供 Tonemap 调试视图使用;其余全屏 shader // 静态使用的 binding 是布局的子集,合法 std::vector 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}); diff --git a/Engine/src/Renderer/Passes/SkyboxPass.cpp b/Engine/src/Renderer/Passes/SkyboxPass.cpp index e882988..e32f82b 100644 --- a/Engine/src/Renderer/Passes/SkyboxPass.cpp +++ b/Engine/src/Renderer/Passes/SkyboxPass.cpp @@ -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) { diff --git a/Engine/src/Renderer/Passes/TonemapPass.cpp b/Engine/src/Renderer/Passes/TonemapPass.cpp index ee633e3..ea8c62d 100644 --- a/Engine/src/Renderer/Passes/TonemapPass.cpp +++ b/Engine/src/Renderer/Passes/TonemapPass.cpp @@ -3,11 +3,14 @@ // #include +#include +#include #include 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 diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 1005628..7f9c877 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -40,7 +41,6 @@ #include #include -#include #include namespace Engine @@ -74,17 +74,12 @@ namespace Engine m_FullscreenGpu = CreateScope( *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( + *m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager); + m_Environment = m_Ibl->Neutral(glm::vec3(0.1f)); // 4. Swapchain(只管呈现;渲染目标统一归 RenderGraph) m_Swapchain = CreateScope(*m_Device, *m_Allocator, m_Window); @@ -101,7 +96,7 @@ namespace Engine // 7. 资产上传服务(渲染层对外提供的 IAssetUploader 实现) m_AssetUploader = CreateScope( *m_Device, *m_Allocator, *m_DescriptorManager, - m_SceneGpu->textureSetLayout); + m_SceneGpu->textureSetLayout, *m_Ibl); // 8. RenderGraph:资源声明 + pass 注册 + 首次编译 m_PostParams = CreateScope(); @@ -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(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& Renderer::GetDebugViewNames() const { static const std::vector 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( - *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( &m_ImGuiLayer, m_Backbuffer)); m_Passes.push_back(CreateScope(m_Backbuffer)); diff --git a/Engine/src/Renderer/VulkanAssetUploader.cpp b/Engine/src/Renderer/VulkanAssetUploader.cpp index edaee8d..ecbe113 100644 --- a/Engine/src/Renderer/VulkanAssetUploader.cpp +++ b/Engine/src/Renderer/VulkanAssetUploader.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace Engine { @@ -20,4 +21,10 @@ namespace Engine { m_TextureLayout, halfSize, r, g, b); } + Scope VulkanAssetUploader::LoadEnvironment(const std::string& path) + { + // compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT + return m_Ibl.FromHdrEquirect(path, 1024); + } + } // namespace Engine diff --git a/Engine/src/Vulkan/DescriptorManager.cpp b/Engine/src/Vulkan/DescriptorManager.cpp index 7e8706d..99979d2 100644 --- a/Engine/src/Vulkan/DescriptorManager.cpp +++ b/Engine/src/Vulkan/DescriptorManager.cpp @@ -25,10 +25,11 @@ namespace Engine vk::DescriptorPool DescriptorManager::CreateChunk() const { - std::array poolSizes = { + std::array 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& layouts, - const u32 pushConstantSize) const + const u32 pushConstantSize, + const vk::ShaderStageFlags pushStages) const { vk::PipelineLayoutCreateInfo info; info.setSetLayouts(layouts); std::array 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); diff --git a/Engine/src/Vulkan/ShaderManager.cpp b/Engine/src/Vulkan/ShaderManager.cpp index 03b8bdb..2474196 100644 --- a/Engine/src/Vulkan/ShaderManager.cpp +++ b/Engine/src/Vulkan/ShaderManager.cpp @@ -42,6 +42,7 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector& 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& 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); diff --git a/shaders/ibl_brdf_lut.comp b/shaders/ibl_brdf_lut.comp new file mode 100644 index 0000000..51524cf --- /dev/null +++ b/shaders/ibl_brdf_lut.comp @@ -0,0 +1,54 @@ +#version 450 + +// IBL ④:BRDF 积分 LUT(2D,纯数学,与环境内容无关)。 +// (NdotV, roughness) → (F 缩放 A, F 偏置 B):specular = prefiltered·(F·A + B)。 +// 与环境切换无关,但随生成链一起跑(GPU 亚毫秒级),省去跨环境缓存。 + +#include "ibl_common.glsl" + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = 0, binding = 0, rg16f) uniform restrict writeonly image2D outLut; + +const uint SAMPLES = 128; + +void main() +{ + ivec2 gid = ivec2(gl_GlobalInvocationID); + if (gid.x >= imageSize(outLut).x || gid.y >= imageSize(outLut).y) return; + + vec2 uv = (vec2(gid) + 0.5) / vec2(imageSize(outLut)); + float nDotV = uv.x; + float roughness = uv.y; + + // V 在以 N 为 +Z 的切线空间里(积分只依赖这两角度) + vec3 V = vec3(sqrt(1.0 - nDotV * nDotV), 0.0, nDotV); + vec3 Nz = vec3(0.0, 0.0, 1.0); + + float a = 0.0, b = 0.0; + float k = roughness * roughness * 0.5; // IBL 版 Smith k + + for (uint i = 0u; i < SAMPLES; ++i) { + vec3 H = iblImportanceSampleGGX(iblHammersley(i, SAMPLES), Nz, roughness); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + + float lDotN = L.z; + float nDotL = max(lDotN, 0.0); + float nDotH = max(H.z, 0.0); + float vDotH = max(dot(V, H), 0.0); + if (nDotL <= 0.0) continue; + + float g = (nDotV / (nDotV * (1.0 - k) + k)) * + (nDotL / (nDotL * (1.0 - k) + k)); + // 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),0/0 = NaN + // 会累进 LUT——分母钳 epsilon(与 CPU 版一致) + float gVis = g * vDotH / max(nDotH * nDotV, 1e-6); + float fc = 1.0 - vDotH; + float f = fc * fc * fc * fc * fc; // (1-vDotH)⁵ + + a += (1.0 - f) * gVis; + b += f * gVis; + } + + imageStore(outLut, gid, vec4(a / float(SAMPLES), b / float(SAMPLES), 0.0, 1.0)); +} diff --git a/shaders/ibl_common.glsl b/shaders/ibl_common.glsl new file mode 100644 index 0000000..b28063f --- /dev/null +++ b/shaders/ibl_common.glsl @@ -0,0 +1,60 @@ +// ibl_common:IBL compute 预积分共享数学(与原 CPU 实现逐式对齐)。 +// +// 被 4 个 .comp 包含(glslc 相对 include)。改造自 Cubemap.cpp 的 +// CPU 积分路径——公式一致保证视觉无回归,可直接对比验证。 + +#ifndef IBL_COMMON_GLSL +#define IBL_COMMON_GLSL + +const float IBL_PI = 3.14159265358979; + +// ---- GL/Vulkan cube 面方向表(与 GL spec 立方体选择表互逆)---- +// (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位方向 +vec3 iblFaceDir(uint face, float u, float v) +{ + float s = u * 2.0 - 1.0; + float t = v * 2.0 - 1.0; + vec3 dir; + if (face == 0u) dir = vec3( 1.0, -t, -s); // +X(面片顶行 = +Y) + else if (face == 1u) dir = vec3(-1.0, -t, s); // -X + else if (face == 2u) dir = vec3( s, 1.0, t); // +Y + else if (face == 3u) dir = vec3( s,-1.0, -t); // -Y + else if (face == 4u) dir = vec3( s, -t, 1.0); // +Z + else dir = vec3(-s, -t,-1.0); // -Z + return normalize(dir); // equirect 查表要求单位方向 +} + +// 单位方向 → 等距柱状 uv。行 0 = 天顶(+Y):v = acos(y)/π, +// 与 stbi 行序一致;u 经度环绕交给采样器 S = eRepeat 处理接缝 +vec2 iblDirToEquirectUv(vec3 dir) +{ + float u = atan(dir.z, dir.x) / (2.0 * IBL_PI) + 0.5; + float v = acos(clamp(dir.y, -1.0, 1.0)) / IBL_PI; + return vec2(u, v); +} + +// van der Corput 基底反转。硬件位反转指令替代 CPU 手写交换链; +// 整数值转浮点(不是位型重解读),序列与 CPU 版一致 +vec2 iblHammersley(uint i, uint n) +{ + return vec2(float(i) / float(n), + float(bitfieldReverse(i)) * 2.3283064365386963e-10); +} + +// GGX NDF 按概率密度取半程向量 H(切线空间 → 世界) +vec3 iblImportanceSampleGGX(vec2 xi, vec3 N, float roughness) +{ + float a = roughness * roughness; + float phi = 2.0 * IBL_PI * xi.x; + float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + + vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); + + vec3 up = abs(N.z) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0); + vec3 T = normalize(cross(up, N)); + vec3 B = cross(N, T); + return normalize(T * H.x + B * H.y + N * H.z); +} + +#endif // IBL_COMMON_GLSL diff --git a/shaders/ibl_equirect_to_cube.comp b/shaders/ibl_equirect_to_cube.comp new file mode 100644 index 0000000..ed1c903 --- /dev/null +++ b/shaders/ibl_equirect_to_cube.comp @@ -0,0 +1,27 @@ +#version 450 + +// IBL ①:等距柱状 HDR → 环境 cube(6 面重采样)。 +// 替代原 CPU 逐像素 SampleEquirect 双线性查表——GPU 上直接用硬件 +// 线性过滤采样(经度接缝由 S=eRepeat 处理),观感与 CPU 版一致。 +// 输出为 image2DArray(layer = face),图像本体带 CUBE_COMPATIBLE, +// 采样端另建 eCube view。 + +#include "ibl_common.glsl" + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = 0, binding = 0) uniform sampler2D equirect; +layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube; + +void main() +{ + ivec3 gid = ivec3(gl_GlobalInvocationID); + ivec2 size = imageSize(outCube).xy; + if (gid.x >= size.x || gid.y >= size.y) return; + + vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size); + vec3 dir = iblFaceDir(uint(gid.z), uv.x, uv.y); + vec3 rgb = texture(equirect, iblDirToEquirectUv(dir)).rgb; + + imageStore(outCube, gid, vec4(rgb, 1.0)); +} diff --git a/shaders/ibl_irradiance.comp b/shaders/ibl_irradiance.comp new file mode 100644 index 0000000..7e0ac8d --- /dev/null +++ b/shaders/ibl_irradiance.comp @@ -0,0 +1,48 @@ +#version 450 + +// IBL ②:环境 cube → 漫反射辐照度 cube(半球 Lambert 卷积)。 +// E = ∫ L·cosθ·sinθ dθ dφ,切线基 up×N 构造。64×32 = 2048 采样/像素 +// (与 CPU 版同级精度),32² 低频输出上 GPU 耗时可忽略。 + +#include "ibl_common.glsl" + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = 0, binding = 0) uniform samplerCube env; +layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube; + +const int PHI_STEPS = 64; +const int THETA_STEPS = 32; + +void main() +{ + ivec3 gid = ivec3(gl_GlobalInvocationID); + ivec2 size = imageSize(outCube).xy; + if (gid.x >= size.x || gid.y >= size.y) return; + + vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size); + vec3 N = iblFaceDir(uint(gid.z), uv.x, uv.y); + + vec3 up = abs(N.y) > 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(0.0, 1.0, 0.0); + vec3 T = normalize(cross(up, N)); + vec3 B = cross(N, T); + + float dPhi = 2.0 * IBL_PI / float(PHI_STEPS); + float dTheta = 0.5 * IBL_PI / float(THETA_STEPS); + + vec3 irradiance = vec3(0.0); + for (int p = 0; p < PHI_STEPS; ++p) { + float phi = (float(p) + 0.5) * dPhi; + for (int t = 0; t < THETA_STEPS; ++t) { + float theta = (float(t) + 0.5) * dTheta; + float sinT = sin(theta); + float cosT = cos(theta); + vec3 sampleDir = T * (sinT * cos(phi)) + + B * (sinT * sin(phi)) + + N * cosT; + irradiance += texture(env, sampleDir).rgb * (cosT * sinT); + } + } + + imageStore(outCube, gid, vec4(irradiance * (dPhi * dTheta), 1.0)); +} diff --git a/shaders/ibl_prefilter.comp b/shaders/ibl_prefilter.comp new file mode 100644 index 0000000..4e070eb --- /dev/null +++ b/shaders/ibl_prefilter.comp @@ -0,0 +1,44 @@ +#version 450 + +// IBL ③:环境 cube → pre-filtered cube(GGX 重要性采样,split-sum 镜面端)。 +// R(N) ≈ Σ L·NdotL / Σ NdotL。每 mip 一次 dispatch(storage view 必须 +// 单 mip),roughness 经 push constant 传入;256 采样统一(高 roughness +// 分布更宽,取 CPU 版采样数上限保证收敛)。 + +#include "ibl_common.glsl" + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = 0, binding = 0) uniform samplerCube env; +layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube; + +layout(push_constant) uniform Push { + float roughness; +} pc; + +const uint SAMPLES = 256; + +void main() +{ + ivec3 gid = ivec3(gl_GlobalInvocationID); + ivec2 size = imageSize(outCube).xy; + if (gid.x >= size.x || gid.y >= size.y) return; + + vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size); + vec3 N = iblFaceDir(uint(gid.z), uv.x, uv.y); + vec3 V = N; // split-sum 约定:V 沿 N + + vec3 acc = vec3(0.0); + float weight = 0.0; + for (uint i = 0u; i < SAMPLES; ++i) { + vec3 H = iblImportanceSampleGGX(iblHammersley(i, SAMPLES), N, pc.roughness); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + float nDotL = max(dot(N, L), 0.0); + if (nDotL > 0.0) { + acc += texture(env, L).rgb * nDotL; + weight += nDotL; + } + } + + imageStore(outCube, gid, vec4(weight > 0.0 ? acc / weight : vec3(0.0), 1.0)); +} diff --git a/shaders/tonemap.frag b/shaders/tonemap.frag index 3b23f2e..d3956c1 100644 --- a/shaders/tonemap.frag +++ b/shaders/tonemap.frag @@ -20,6 +20,11 @@ layout(set = 0, binding = 4) uniform sampler2D velocityImage; layout(set = 0, binding = 5) uniform sampler2D shadowMap0Image; // D32 深度(r 分量) layout(set = 0, binding = 6) uniform sampler2D shadowMap1Image; layout(set = 0, binding = 7) uniform sampler2D shadowMap2Image; +// 环境调试视图源(随环境切换重绑,静态绑定超集) +layout(set = 0, binding = 8) uniform samplerCube envCubeImage; +layout(set = 0, binding = 9) uniform samplerCube irradianceImage; +layout(set = 0, binding = 10) uniform samplerCube prefilterImage; +layout(set = 0, binding = 11) uniform sampler2D brdfLutImage; // 与 Passes::DebugView 枚举同步(FullscreenGpu.h) const int VIEW_FINAL = 0; @@ -31,6 +36,10 @@ const int VIEW_VELOCITY = 5; const int VIEW_SHADOW0 = 6; const int VIEW_SHADOW1 = 7; const int VIEW_SHADOW2 = 8; +const int VIEW_ENV_CUBE = 9; +const int VIEW_IRRADIANCE = 10; +const int VIEW_PREFILTER = 11; +const int VIEW_BRDF_LUT = 12; // 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) layout(push_constant) uniform PushConstants { @@ -56,6 +65,19 @@ vec3 depthView(sampler2D depthImage) { return vec3(pow(1.0 - d, 2.2)); } +// 全屏 uv → 球面方向(cube 调试视图的经纬展开:横向经度、纵向极角) +vec3 latLongDir(vec2 uv) { + float phi = (uv.x * 2.0 - 1.0) * 3.14159265358979; // -π..π 经度 + float theta = (1.0 - uv.y) * 3.14159265358979; // 0..π 极角(顶=天顶) + return vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi)); +} + +// cube 视图统一走 tonemap(HDR 域,观感与场景一致); +// prefilter 纵向映射 mip 链(上锐下糙,直观验证 roughness 分级) +vec3 cubeView(samplerCube cube, float lod) { + return tonemap(textureLod(cube, latLongDir(fragUV), lod).rgb); +} + void main() { int view = int(pc.debugView + 0.5); @@ -81,6 +103,15 @@ void main() { case VIEW_SHADOW0: outColor = vec4(depthView(shadowMap0Image), 1.0); return; case VIEW_SHADOW1: outColor = vec4(depthView(shadowMap1Image), 1.0); return; case VIEW_SHADOW2: outColor = vec4(depthView(shadowMap2Image), 1.0); return; + case VIEW_ENV_CUBE: outColor = vec4(cubeView(envCubeImage, 0.0), 1.0); return; + case VIEW_IRRADIANCE: outColor = vec4(cubeView(irradianceImage, 0.0), 1.0); return; + case VIEW_PREFILTER: outColor = vec4(cubeView(prefilterImage, fragUV.y * 4.0), 1.0); return; + case VIEW_BRDF_LUT: { + // LUT 是 (F 缩放, F 偏置) 双通道,R/G 直读可视化 + vec2 ab = texture(brdfLutImage, fragUV).rg; + outColor = vec4(ab, 0.0, 1.0); + return; + } } }