From 4b5990e6c885ef8f84069285369253d1579c4341 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Wed, 23 Sep 2026 18:37:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=8E=AF=E5=A2=83=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E7=AE=A1=E7=BA=BF=EF=BC=88IBL=20=E9=98=B6=E6=AE=B5=E2=91=A0?= =?UTF-8?q?=EF=BC=89=E2=80=94=E2=80=94HDR=20=E7=AD=89=E8=B7=9D=E6=9F=B1?= =?UTF-8?q?=E7=8A=B6=E8=BD=AC=20cube=20+=20Skybox=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cubemap:stbi_loadf 加载 .hdr,CPU 双线性重采样 6 面(GL 规范方向表), f32→fp16 转换后整块上传 eCubeCompatible image(R16G16B16A16Sfloat) - SkyboxPass:HDRColor 的 first writer,承担清屏职责;无环境贴图自动 退化为纯清屏(Forward 不受影响) - frag 逐像素 NDC 反投影取世界方向(invVP push,投影变换不可线性插值) - Forward color 附件 Clear→Load:几何合成在天空之上 - SceneRenderer 产出 skyInvVP = inverse(proj × mat4(mat3(view))) --- Engine/include/Engine/Renderer/Cubemap.h | 59 ++++ .../Engine/Renderer/Passes/SkyboxPass.h | 61 ++++ Engine/include/Engine/Renderer/Renderer.h | 4 + Engine/include/Engine/Renderer/SceneTypes.h | 4 + Engine/src/Renderer/Cubemap.cpp | 293 ++++++++++++++++++ Engine/src/Renderer/Passes/ForwardPass.cpp | 3 +- Engine/src/Renderer/Passes/SkyboxPass.cpp | 177 +++++++++++ Engine/src/Renderer/Renderer.cpp | 15 + Engine/src/Renderer/SceneRenderer.cpp | 5 + shaders/skybox.frag | 21 ++ shaders/skybox.vert | 21 ++ 11 files changed, 662 insertions(+), 1 deletion(-) create mode 100644 Engine/include/Engine/Renderer/Cubemap.h create mode 100644 Engine/include/Engine/Renderer/Passes/SkyboxPass.h create mode 100644 Engine/src/Renderer/Cubemap.cpp create mode 100644 Engine/src/Renderer/Passes/SkyboxPass.cpp create mode 100644 shaders/skybox.frag create mode 100644 shaders/skybox.vert diff --git a/Engine/include/Engine/Renderer/Cubemap.h b/Engine/include/Engine/Renderer/Cubemap.h new file mode 100644 index 0000000..aa7c15b --- /dev/null +++ b/Engine/include/Engine/Renderer/Cubemap.h @@ -0,0 +1,59 @@ +// +// Created by atdunbg on 2026/9/23. +// +// Cubemap:环境立方体贴图。 +// +// 从等距柱状 HDR(.hdr,polyhaven 等)生成:CPU 重采样到 6 面 +//(一次性加载成本,亚秒级),转 fp16 后整块上传,硬件 Cube 采样。 +// 后续 IBL 的 irradiance/prefilter 卷积也基于本类的数据形态。 +// + +#ifndef HELLOVULKAN_CUBEMAP_H +#define HELLOVULKAN_CUBEMAP_H + +#include +#include +#include + +namespace Engine { + + class Device; + + class Cubemap { + public: + // 从等距柱状 HDR 生成(CPU 重采样到 6 面,faceSize 为每面边长) + static Scope CreateFromHdrEquirect(Device& device, + MemoryAllocator& allocator, + const std::string& path, + u32 faceSize); + + ~Cubemap(); + + Cubemap(const Cubemap&) = delete; + Cubemap& operator=(const Cubemap&) = delete; + + vk::Image Get() const { return m_Image; } + vk::ImageView GetView() const { return m_View; } + vk::Sampler GetSampler() const { return m_Sampler; } + u32 GetFaceSize() const { return m_FaceSize; } + + private: + Cubemap(Device& device, MemoryAllocator& allocator, + vk::Image image, vk::ImageView view, vk::Sampler sampler, + MemoryAllocator::Allocation allocation, u32 faceSize) + : m_Device(device), m_Allocator(allocator), + m_Image(image), m_View(view), m_Sampler(sampler), + m_Allocation(allocation), m_FaceSize(faceSize) {} + + Device& m_Device; + MemoryAllocator& m_Allocator; + vk::Image m_Image; + vk::ImageView m_View; + vk::Sampler m_Sampler; + MemoryAllocator::Allocation m_Allocation; + u32 m_FaceSize; + }; + +} // namespace Engine + +#endif //HELLOVULKAN_CUBEMAP_H diff --git a/Engine/include/Engine/Renderer/Passes/SkyboxPass.h b/Engine/include/Engine/Renderer/Passes/SkyboxPass.h new file mode 100644 index 0000000..f879d61 --- /dev/null +++ b/Engine/include/Engine/Renderer/Passes/SkyboxPass.h @@ -0,0 +1,61 @@ +// +// Created by atdunbg on 2026/9/23. +// +// SkyboxPass:环境背景(IBL 阶段①)。 +// +// HDRColor 的 first writer:承担清屏职责(loadOp Clear),有环境贴图时 +// 再画天空——Forward 随之改为 loadOp Load,几何在天空之上合成。 +// 职责收口对齐 UE:sky pass 是场景渲染的第一步,无深度附件, +// 深度测试关(几何完全覆盖由 Forward 的深度决定)。 +// +// 环境贴图生命周期:Renderer 持有 Cubemap(声明序先于 pass), +// 本 pass 只引用。描述符绑定一次(cube view 全程稳定,不随 RG 重编译)。 +// + +#ifndef HELLOVULKAN_SKYBOXPASS_H +#define HELLOVULKAN_SKYBOXPASS_H + +#include +#include +#include +#include + +namespace Engine { + + class Device; + class DescriptorManager; + class ShaderManager; + class Cubemap; + + namespace Passes { + + class SkyboxPass final : public RenderPass { + public: + // env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色 + SkyboxPass(Device& device, ShaderManager& shaders, + DescriptorManager& descriptors, FrameSceneData& frame, + RG::TextureHandle hdrColor, const Cubemap* env); + ~SkyboxPass(); + + SkyboxPass(const SkyboxPass&) = delete; + SkyboxPass& operator=(const SkyboxPass&) = delete; + + void Register(RG::RenderGraph& rg) override; + + private: + Device& m_Device; + DescriptorManager& m_Descriptors; + FrameSceneData& m_Frame; + RG::TextureHandle m_HDRColor; + const Cubemap* m_Env; // 可空 + + vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube} + vk::PipelineLayout m_Layout; // push = invVP 64B + vk::ShaderEXT m_Vert{}, m_Frag{}; + std::array m_Sets{}; + }; + + } // namespace Passes +} // namespace Engine + +#endif //HELLOVULKAN_SKYBOXPASS_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index feb32d5..3ef25c0 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -41,6 +41,7 @@ namespace Engine { class UniformBuffer; class SceneRenderer; class VulkanAssetUploader; + class Cubemap; class ImGuiLayer; namespace ECS { class Scene; } @@ -113,6 +114,9 @@ namespace Engine { Scope m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) Scope m_SceneGpu; // 场景类 pass 共享装配 Scope m_FullscreenGpu; // 全屏 pass 共享装配 + // 环境贴图(IBL 阶段①):HDR → cube,被 SkyboxPass 引用; + // 声明在 m_Passes 之前 → 析构晚于 pass(引用安全) + Scope m_Environment; Scope m_Swapchain; Scope m_SyncManager; Scope m_CommandManager; diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index d8b0f00..6c31ccf 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -68,6 +68,10 @@ namespace Engine { struct FrameSceneData { UniformBufferObject ubo{}; + // 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view))) + //(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push) + glm::mat4 skyInvVP{1.0f}; + // 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO) std::vector instanceModels; // 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一 diff --git a/Engine/src/Renderer/Cubemap.cpp b/Engine/src/Renderer/Cubemap.cpp new file mode 100644 index 0000000..752d152 --- /dev/null +++ b/Engine/src/Renderer/Cubemap.cpp @@ -0,0 +1,293 @@ +// +// Created by atdunbg on 2026/9/23. +// +// Cubemap 实现:HDR 等距柱状 → CPU 重采样 6 面 → fp16 → cube image。 +// +// 一次性加载成本(亚秒级 CPU 重采样),换取运行时零转换开销; +// 硬件 Cube 采样(filtering/边缘接缝由 driver 处理)。 +// 后续 IBL 阶段的 irradiance/prefilter 卷积复用 FaceDir 方向表。 +// + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace Engine +{ + namespace + { + constexpr u32 kFaceCount = 6; + + // GL/Vulkan 规范的 cube 面方向表(与 GL spec 立方体选择表互逆)。 + // (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位化前方向。 + // Vulkan 面序与 GL 一致:+X -X +Y -Y +Z -Z + 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; + switch (face) { + case 0: return { 1.0f, -t, -s }; // +X(面片顶行 = +Y) + case 1: return {-1.0f, -t, s }; // -X + case 2: return { s, 1.0f, t }; // +Y + case 3: return { s,-1.0f, -t }; // -Y + case 4: return { s, -t, 1.0f }; // +Z + default: return {-s, -t,-1.0f }; // -Z + } + } + + // 等距柱状双线性采样(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); + } + + // IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf)。 + // HDR 环境值域内常规路径足够,次正规/NaN 路径只为数学完备 + 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) { + // 常规数:指数重偏(127→15),尾数右移 13 位并舍入到最近偶数 + 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 + } + // half 次正规 [2^-24, 2^-14):补隐含位后按目标精度舍入 + const u32 mantissa = (absVal & 0x007fffffu) | 0x00800000u; + const u32 shift = 126u - (absVal >> 23u); + const u32 m = (mantissa + (1u << (shift - 1))) >> shift; + // 舍入进位到 0x400 恰好是最小正规 half 的位型,无需特判 + return static_cast(sign | std::min(m, 0x400u)); + } + } + + 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); + + // ---- 2. 重采样 6 面 + fp16(RGBA16F = 每像素 8B)---- + const u32 facePixels = faceSize * faceSize; + std::vector faces(kFaceCount * facePixels * 4); + for (u32 f = 0; f < kFaceCount; ++f) { + u16* dst = faces.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 + } + } + } + stbi_image_free(pixels); + + // ---- 3. staging(HOST_VISIBLE 持久映射,逐面偏移拷入)---- + const vk::DeviceSize faceBytes = + static_cast(facePixels) * 8; + const vk::DeviceSize total = faceBytes * kFaceCount; + + auto stagingAlloc = allocator.CreateBuffer( + total, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent); + std::memcpy(stagingAlloc.MappedPtr, faces.data(), + static_cast(total)); + faces.clear(); + faces.shrink_to_fit(); // CPU 侧临时数据尽早释放 + + // ---- 4. cube image(eCubeCompatible,6 层,fp16)---- + vk::ImageCreateInfo imageInfo; + imageInfo.setImageType(vk::ImageType::e2D) + .setFormat(vk::Format::eR16G16B16A16Sfloat) + .setExtent({faceSize, faceSize, 1}) + .setMipLevels(1) + .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; + + // ---- 5. cube view + 环境采样器(clamp-to-edge:cube 采样寻址由面自动处理)---- + vk::ImageViewCreateInfo viewInfo; + viewInfo.setImage(image) + .setViewType(vk::ImageViewType::eCube) + .setFormat(vk::Format::eR16G16B16A16Sfloat) + .setSubresourceRange({ + vk::ImageAspectFlagBits::eColor, 0, 1, 0, kFaceCount}); + vk::ImageView view = device.GetDevice().createImageView(viewInfo); + + 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(0.0f); + vk::Sampler sampler = device.GetDevice().createSampler(samplerInfo); + + // ---- 6. 一次性命令缓冲:整 cube → TransferDst,逐面拷贝 → ShaderRead ---- + 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}); + + // 6 层一起转 layout(subresource range layerCount = 6) + 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, kFaceCount}); + cmd.pipelineBarrier2( + vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer)); + + for (u32 f = 0; f < kFaceCount; ++f) { + vk::BufferImageCopy region; + region.setBufferOffset(faceBytes * f) + .setBufferRowLength(0) + .setBufferImageHeight(0) + .setImageSubresource({vk::ImageAspectFlagBits::eColor, + 0, f, 1}) + .setImageOffset({0, 0, 0}) + .setImageExtent({faceSize, faceSize, 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, kFaceCount}); + cmd.pipelineBarrier2( + vk::DependencyInfo{}.setImageMemoryBarriers(toShader)); + + cmd.end(); + + // ---- 7. 提交并等待,清理 staging ---- + 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); + + ENGINE_INFO("Environment cubemap: {} ({}x6 {} faces, fp16)", + path, faceSize, kFaceCount); + + return Scope(new Cubemap(device, allocator, image, view, + sampler, imageAlloc, faceSize)); + } + + Cubemap::~Cubemap() + { + const auto& vkDevice = m_Device.GetDevice(); + + vkDevice.destroySampler(m_Sampler); + vkDevice.destroyImageView(m_View); + + // image 资源与显存经 VMA 成对释放 + m_Allocator.Destroy(m_Allocation); + } + +} // namespace Engine diff --git a/Engine/src/Renderer/Passes/ForwardPass.cpp b/Engine/src/Renderer/Passes/ForwardPass.cpp index f82a6e1..bea2844 100644 --- a/Engine/src/Renderer/Passes/ForwardPass.cpp +++ b/Engine/src/Renderer/Passes/ForwardPass.cpp @@ -22,9 +22,10 @@ namespace Engine::Passes std::array{0.1f, 0.1f, 0.1f, 0.0f}); vk::RenderingAttachmentInfo colorAttachments[2]; + // HDR 附件由 Skybox pass 清屏并画背景,这里 Load 在其上合成几何 colorAttachments[0].setImageView(ctx.View(m_HDRColor)) .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) - .setLoadOp(vk::AttachmentLoadOp::eClear) + .setLoadOp(vk::AttachmentLoadOp::eLoad) .setStoreOp(vk::AttachmentStoreOp::eStore) .setClearValue(clearValue); diff --git a/Engine/src/Renderer/Passes/SkyboxPass.cpp b/Engine/src/Renderer/Passes/SkyboxPass.cpp new file mode 100644 index 0000000..e882988 --- /dev/null +++ b/Engine/src/Renderer/Passes/SkyboxPass.cpp @@ -0,0 +1,177 @@ +// +// Created by atdunbg on 2026/9/23. +// + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +namespace Engine::Passes +{ + SkyboxPass::SkyboxPass(Device& device, ShaderManager& shaders, + DescriptorManager& descriptors, FrameSceneData& frame, + RG::TextureHandle hdrColor, const Cubemap* env) + : m_Device(device), m_Descriptors(descriptors), m_Frame(frame), + m_HDRColor(hdrColor), m_Env(env) + { + // Set0 = {samplerCube}(fragment 采样) + std::vector bindings = { + {0, vk::DescriptorType::eCombinedImageSampler, + vk::ShaderStageFlagBits::eFragment}, + }; + m_SetLayout = descriptors.CreateLayout(bindings); + + // push = invVP 64B(skybox.vert/.frag 声明一致,VUID-08878) + m_Layout = descriptors.CreatePipelineLayout( + std::vector{m_SetLayout}, + sizeof(glm::mat4)); + + const std::string shaderDir = "shaders/"; + auto vert = shaders.CreateFromFile( + shaderDir + "skybox.vert.spv", + vk::ShaderStageFlagBits::eVertex, + std::vector{m_SetLayout}, + sizeof(glm::mat4)); + auto frag = shaders.CreateFromFile( + shaderDir + "skybox.frag.spv", + vk::ShaderStageFlagBits::eFragment, + std::vector{m_SetLayout}, + sizeof(glm::mat4)); + + m_Vert = vert.Handle; + m_Frag = frag.Handle; + if (!m_Vert || !m_Frag) { + ENGINE_FATAL("SkyboxPass: failed to create shader objects"); + } + + // per-slot set:cube view 全程稳定,构造期绑定一次即可 + for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { + m_Sets[slot] = descriptors.AllocateDescriptorSet(m_SetLayout); + if (m_Env) { + descriptors.UpdateTexture(m_Sets[slot], 0, + m_Env->GetSampler(), + m_Env->GetView()); + } + } + } + + SkyboxPass::~SkyboxPass() + { + const auto& vkDevice = m_Device.GetDevice(); + + if (m_Vert) vkDevice.destroyShaderEXT(m_Vert); + if (m_Frag) vkDevice.destroyShaderEXT(m_Frag); + vkDevice.destroyPipelineLayout(m_Layout); + vkDevice.destroyDescriptorSetLayout(m_SetLayout); + } + + void SkyboxPass::Register(RG::RenderGraph& rg) + { + rg.AddPass("Skybox", [this](RG::RenderGraphContext& ctx) { + ZoneScopedN("Skybox"); + const vk::CommandBuffer cmd = ctx.cmd; + + vk::ClearValue clear; + clear.color = vk::ClearColorValue( + std::array{0.1f, 0.1f, 0.1f, 1.0f}); + + vk::RenderingAttachmentInfo color; + color.setImageView(ctx.View(m_HDRColor)) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) // 清屏职责在本 pass + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(clear); + + vk::RenderingInfo renderingInfo; + renderingInfo.setRenderArea({{0, 0}, ctx.extent}) + .setLayerCount(1) + .setColorAttachments(color); // 无深度附件 + + cmd.beginRendering(renderingInfo); + + if (m_Env) { + // 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写) + std::array stages = { + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment + }; + std::array shaders = {m_Vert, m_Frag}; + cmd.bindShadersEXT(stages, shaders); + + cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + m_Layout, 0, + m_Sets[ctx.frame.frameSlot], {}); + cmd.pushConstants( + m_Layout, + vk::ShaderStageFlagBits::eVertex | + vk::ShaderStageFlagBits::eFragment, + 0, m_Frame.skyInvVP); + + // ---- 动态状态 ---- + cmd.setRasterizerDiscardEnable(VK_FALSE); + + cmd.setDepthTestEnable(VK_FALSE); + cmd.setDepthWriteEnable(VK_FALSE); + cmd.setDepthBoundsTestEnable(VK_FALSE); + cmd.setStencilTestEnable(VK_FALSE); + cmd.setDepthBiasEnable(VK_FALSE); + + cmd.setPolygonModeEXT(vk::PolygonMode::eFill); + cmd.setCullMode(vk::CullModeFlagBits::eNone); + cmd.setFrontFace(vk::FrontFace::eCounterClockwise); + cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList); + cmd.setPrimitiveRestartEnable(VK_FALSE); + + // 全屏三角:无顶点输入 + cmd.setVertexInputEXT({}, {}); + + cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + vk::SampleMask sampleMask = 0xFFFFFFFF; + cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask); + cmd.setAlphaToCoverageEnableEXT(VK_FALSE); + cmd.setAlphaToOneEnableEXT(VK_FALSE); + + vk::Viewport viewport(0.0f, 0.0f, + static_cast(ctx.extent.width), + static_cast(ctx.extent.height), + 0.0f, 1.0f); + cmd.setViewportWithCount(viewport); + cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent)); + + // 无混合直写(天空不透明) + vk::ColorBlendEquationEXT blendEquation; + blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne) + .setDstColorBlendFactor(vk::BlendFactor::eZero) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + // ArrayProxyNoTemporaries 拒绝临时量:具名局部变量传入 + vk::Bool32 blendEnable = VK_FALSE; + vk::ColorComponentFlags colorWriteMask = + vk::ColorComponentFlagBits::eR | + vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | + vk::ColorComponentFlagBits::eA; + cmd.setColorBlendEnableEXT(0, blendEnable); + cmd.setColorBlendEquationEXT(0, blendEquation); + cmd.setColorWriteMaskEXT(0, colorWriteMask); + + cmd.draw(3, 1, 0, 0); + } + + cmd.endRendering(); + }) + .Write(m_HDRColor, RG::ResourceUse::ColorAttachment); + } + +} // namespace Engine::Passes diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 5223b3b..5d3a10b 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -12,8 +12,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -38,6 +40,7 @@ #include #include +#include #include namespace Engine @@ -71,6 +74,14 @@ namespace Engine m_FullscreenGpu = CreateScope( *m_Device, *m_DescriptorManager, *m_ShaderManager); + // 3.5 环境贴图(IBL 阶段①):HDR 等距柱状 → cube。 + // 文件缺失则跳过(SkyboxPass 自动退化为纯清屏,Forward 不受影响) + 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); + } + // 4. Swapchain(只管呈现;渲染目标统一归 RenderGraph) m_Swapchain = CreateScope(*m_Device, *m_Allocator, m_Window); m_Extent = m_Swapchain->GetExtent(); @@ -228,6 +239,10 @@ namespace Engine FrameSceneData& frame = m_SceneRenderer->Frame(); m_Passes.push_back(CreateScope( *m_SceneGpu, frame, m_ShadowMaps)); + // Skybox = HDRColor 的 first writer(清屏 + 画背景),先于 Forward 注册 + m_Passes.push_back(CreateScope( + *m_Device, *m_ShaderManager, *m_DescriptorManager, frame, + m_HDRColor, m_Environment.get())); m_Passes.push_back(CreateScope( *m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps)); m_Passes.push_back(CreateScope( diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index e3026e2..74d9cc8 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -168,6 +168,11 @@ namespace Engine ubo.ambientColor = scene.GetSettings().ambientColor; + // 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass; + // 反投影远平面点 = 相机相对世界方向,frag normalize 后采样 cube + m_Frame.skyInvVP = + glm::inverse(ubo.projNoJitter * glm::mat4(glm::mat3(ubo.view))); + // ---- 2. 灯光(第一个方向光作为 CSM 阴影光源;点光位置来自 Transform)---- u32 lightCount = 0; glm::vec3 shadowDir{0.0f, -1.0f, 0.0f}; // 无灯兜底:头顶正射 diff --git a/shaders/skybox.frag b/shaders/skybox.frag new file mode 100644 index 0000000..53691ad --- /dev/null +++ b/shaders/skybox.frag @@ -0,0 +1,21 @@ +#version 450 + +// 天空盒像素:NDC → 世界方向 → cube 采样。 +// invVP = inverse(projNoJitter × mat4(mat3(view))):去掉平移的视图, +// 反投影结果即"相机相对"的世界方向,normalize 后直接采样 cube。 + +layout(location = 0) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform samplerCube envMap; + +// push 与 skybox.vert 一致声明(VUID-08878) +layout(push_constant) uniform PushConstants { + mat4 invVP; +} pc; + +void main() { + vec4 target = pc.invVP * vec4(fragUV * 2.0 - 1.0, 1.0, 1.0); + vec3 dir = normalize(target.xyz / target.w); + outColor = vec4(texture(envMap, dir).rgb, 1.0); +} diff --git a/shaders/skybox.vert b/shaders/skybox.vert new file mode 100644 index 0000000..d8a2ff5 --- /dev/null +++ b/shaders/skybox.vert @@ -0,0 +1,21 @@ +#version 450 + +// 天空盒顶点:全屏三角形(无顶点缓冲),UV 交给 fragment 反投影。 +// 方向在 fragment 逐像素计算(invVP 是投影变换,对 NDC 线性插值方向 +// 不精确;UV 插值是仿射的,逐像素重建方向零误差)。 + +layout(location = 0) out vec2 fragUV; + +// push 与 skybox.frag 一致声明(VUID-08878:shader object 接口一致) +layout(push_constant) uniform PushConstants { + mat4 invVP; +} pc; + +void main() { + vec2 p = vec2( + float((gl_VertexIndex << 1) & 2), + float(gl_VertexIndex & 2)); + fragUV = p; + // 远平面:z = 1(Vulkan [0,1] 深度),无深度附件,只关心覆盖屏幕 + gl_Position = vec4(p * 2.0 - 1.0, 1.0, 1.0); +}