From 9269afaa187b8eb63daf1a7cad65b6e5f6f1d4da Mon Sep 17 00:00:00 2001 From: atdunbg Date: Wed, 23 Sep 2026 19:04:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20IBL=20=E9=98=B6=E6=AE=B5=E2=91=A2?= =?UTF-8?q?=E2=80=94=E2=80=94pre-filtered=20cubemap=20+=20BRDF=20LUT?= =?UTF-8?q?=EF=BC=8Csplit-sum=20=E9=95=9C=E9=9D=A2=E9=A1=B9=E6=8E=A5?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pre-filter:GGX 重要性采样(Hammersley + van der Corput 位反转), 128²×5 mip 分级 roughness,采样数 32< CreateFromHdrEquirect(Device& device, MemoryAllocator& allocator, const std::string& path, u32 faceSize); - // 中性兜底(无 HDR 资产时):env/irradiance 均为常数色。 - // 保证引用侧描述符恒有效(validation),渲染退化为常数环境 + // 中性兜底(无 HDR 资产时):cube 均为常数色;BRDF LUT 仍为 + // 真实积分(纯数学)。保证引用侧描述符恒有效(validation) static Scope CreateNeutral(Device& device, MemoryAllocator& allocator, const glm::vec3& rgb); @@ -51,23 +49,43 @@ namespace Engine { vk::Image Get() const { return m_EnvImage; } vk::ImageView GetView() const { return m_EnvView; } - // ---- 漫反射辐照度 cube(PBR ambient)---- + // ---- 漫反射辐照度 cube(PBR ambient diffuse 项)---- vk::ImageView GetIrradianceView() const { return m_IrrView; } + // ---- pre-filtered cube(PBR ambient specular 项,按 mip 分 roughness)---- + vk::ImageView GetPrefilteredView() const { return m_PrefView; } + u32 GetPrefilteredMipCount() const { return m_PrefMipLevels; } + + // ---- BRDF LUT(envBRDF 查表,vec2(NdotV, roughness) → F 缩放/偏置)---- + vk::ImageView GetBrdfLutView() const { return m_LutView; } + // LUT 专用采样器(clamp-to-edge):共享的 border 白采样器会在 + // uv=1.0 处混入 50% 白色 border,导致高 roughness 镜面偏大 + vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; } + vk::Sampler GetSampler() const { return m_Sampler; } u32 GetFaceSize() const { return m_FaceSize; } private: Cubemap(Device& device, MemoryAllocator& allocator, vk::Image envImage, vk::ImageView envView, - vk::Image irrImage, vk::ImageView irrView, vk::Sampler sampler, + vk::Image irrImage, vk::ImageView irrView, + vk::Image prefImage, vk::ImageView prefView, u32 prefMips, + vk::Image lutImage, vk::ImageView lutView, + vk::Sampler sampler, vk::Sampler lutSampler, MemoryAllocator::Allocation envAlloc, - MemoryAllocator::Allocation irrAlloc, u32 faceSize) + MemoryAllocator::Allocation irrAlloc, + MemoryAllocator::Allocation prefAlloc, + MemoryAllocator::Allocation lutAlloc, u32 faceSize) : m_Device(device), m_Allocator(allocator), m_EnvImage(envImage), m_EnvView(envView), m_IrrImage(irrImage), m_IrrView(irrView), - m_Sampler(sampler), m_EnvAllocation(envAlloc), - m_IrrAllocation(irrAlloc), m_FaceSize(faceSize) {} + m_PrefImage(prefImage), m_PrefView(prefView), + m_PrefMipLevels(prefMips), + m_LutImage(lutImage), m_LutView(lutView), + m_Sampler(sampler), m_LutSampler(lutSampler), + m_EnvAllocation(envAlloc), m_IrrAllocation(irrAlloc), + m_PrefAllocation(prefAlloc), m_LutAllocation(lutAlloc), + m_FaceSize(faceSize) {} Device& m_Device; MemoryAllocator& m_Allocator; @@ -76,9 +94,18 @@ namespace Engine { vk::ImageView m_EnvView; vk::Image m_IrrImage; vk::ImageView m_IrrView; + vk::Image m_PrefImage; + vk::ImageView m_PrefView; + u32 m_PrefMipLevels; + vk::Image m_LutImage; + vk::ImageView m_LutView; vk::Sampler m_Sampler; + vk::Sampler m_LutSampler; + MemoryAllocator::Allocation m_EnvAllocation; MemoryAllocator::Allocation m_IrrAllocation; + MemoryAllocator::Allocation m_PrefAllocation; + MemoryAllocator::Allocation m_LutAllocation; u32 m_FaceSize; }; diff --git a/Engine/src/Renderer/Cubemap.cpp b/Engine/src/Renderer/Cubemap.cpp index 7069225..6a3f3cc 100644 --- a/Engine/src/Renderer/Cubemap.cpp +++ b/Engine/src/Renderer/Cubemap.cpp @@ -1,14 +1,14 @@ // // Created by atdunbg on 2026/9/23. // -// Cubemap 实现:HDR 等距柱状 → CPU 重采样 6 面 + irradiance 卷积 → fp16 cube。 +// Cubemap 实现:HDR 等距柱状 → CPU 预积分四件套 → fp16 上传。 // -// 一次性加载成本(亚秒级 CPU),换取运行时零转换开销; -// 硬件 Cube 采样(filtering/边缘接缝由 driver 处理)。 -// -// irradiance 卷积(LearnOpenGL split-sum 的离线端):对每个法线方向 -// 做半球 Lambert 积分 E = ∫ L·cosθ·sinθ dθ dφ,切线基由 up×N 构造。 -// 32² 面足够——漫反射辐照度本来就是超低频信号。 +// 一次性加载成本(秒级 CPU),换取运行时零积分开销;硬件 Cube 采样 +//(filtering/边缘接缝由 driver 处理)。split-sum 预积分(LearnOpenGL/ +// UE 拆分方案的离线端): +// irradiance —— 半球 Lambert 积分,漫反射 ambient +// prefilter —— GGX 重要性采样(Hammersley 序列),mip 分级 roughness +// BRDF LUT —— (NdotV, roughness) → (F 缩放, F 偏置),纯数学表 // #include @@ -33,12 +33,15 @@ namespace Engine { constexpr u32 kFaceCount = 6; constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频) - constexpr u32 kPhiSteps = 64; // 方位角采样数 - constexpr u32 kThetaSteps = 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] → 单位方向。 - // Vulkan 面序与 GL 一致:+X -X +Y -Y +Z -Z + // ---- 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; @@ -85,8 +88,7 @@ namespace Engine } // 漫反射辐照度半球积分(Lambert):E = ∫ L·cosθ·sinθ dθ dφ。 - // 切线基 up×N 构造;φSteps×θSteps 均匀网格 ≈ 2048 采样/像素, - // 对 32² 低频图绰绰有余 + // 切线基 up×N 构造;均匀网格 ≈2048 采样/像素,对 32² 低频图足够 glm::vec3 ConvolveIrradiance(const f32* img, u32 w, u32 h, const glm::vec3& N) { @@ -105,7 +107,6 @@ namespace Engine 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)) + @@ -117,8 +118,102 @@ namespace Engine return irradiance * (dPhi * dTheta); } - // IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf)。 - // HDR 环境值域内常规路径足够,次正规/NaN 路径只为数学完备 + // ---- GGX 重要性采样(split-sum 镜面端)---- + + // van der Corput 基底反转(位反转技巧) + 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 std::bit_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)); + const f32 gVis = g * vDotH / (nDotH * nDotV); + 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); @@ -132,7 +227,6 @@ namespace Engine 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)); @@ -140,38 +234,59 @@ namespace Engine 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)); } - // 上传 6 面 fp16 数据为 cube image(UNDEFINED → TransferDst → - // ShaderRead;一次性命令缓冲)。view 由调用方创建 + // ---- 上传 ---- + + // cube mip 链数据:[mip0: 6 面][mip1: 6 面]...,每面 faceSize²×8B + struct CubeMipData { + const u16* data; // 6 面连续 + u32 faceSize; + }; + + // 上传 cube(含 mip 链)为 eCubeCompatible image(fp16) MemoryAllocator::Allocation UploadCube(Device& device, MemoryAllocator& allocator, - const std::vector& faces, - u32 faceSize, vk::Image& outImage) + const std::vector& mips, + vk::Image& outImage) { - const vk::DeviceSize faceBytes = - static_cast(faceSize) * faceSize * 8; - const vk::DeviceSize total = faceBytes * kFaceCount; + 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); - std::memcpy(stagingAlloc.MappedPtr, faces.data(), - static_cast(total)); + { + 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({faceSize, faceSize, 1}) - .setMipLevels(1) + .setExtent({baseSize, baseSize, 1}) + .setMipLevels(mipCount) .setArrayLayers(kFaceCount) .setSamples(vk::SampleCountFlagBits::e1) .setTiling(vk::ImageTiling::eOptimal) @@ -190,6 +305,119 @@ namespace Engine .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(outImage) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, + 0, mipCount, 0, kFaceCount}); + cmd.pipelineBarrier2( + vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer)); + + 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( + (static_cast(dim) * dim * 8 * + kFaceCount) * mip) + .setBufferRowLength(0) + .setBufferImageHeight(0) + .setImageSubresource({vk::ImageAspectFlagBits::eColor, + mip, f, 1}) + .setImageOffset({0, 0, 0}) + .setImageExtent({dim, dim, 1}); + cmd.copyBufferToImage(stagingAlloc.Buffer, outImage, + 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(outImage) + .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; + } + + // 上传 BRDF LUT(2D,R16G16Sfloat,8B/px) + MemoryAllocator::Allocation UploadTexture2D(Device& device, + MemoryAllocator& allocator, + const std::vector& texels, + u32 size, + vk::Format format, + vk::Image& outImage) + { + 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); + outImage = 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) @@ -208,23 +436,20 @@ namespace Engine .setNewLayout(vk::ImageLayout::eTransferDstOptimal) .setImage(outImage) .setSubresourceRange({vk::ImageAspectFlagBits::eColor, - 0, 1, 0, kFaceCount}); + 0, 1, 0, 1}); 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, outImage, - vk::ImageLayout::eTransferDstOptimal, - region); - } + 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, outImage, + vk::ImageLayout::eTransferDstOptimal, region); vk::ImageMemoryBarrier2 toShader; toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer) @@ -235,7 +460,7 @@ namespace Engine .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) .setImage(outImage) .setSubresourceRange({vk::ImageAspectFlagBits::eColor, - 0, 1, 0, kFaceCount}); + 0, 1, 0, 1}); cmd.pipelineBarrier2( vk::DependencyInfo{}.setImageMemoryBarriers(toShader)); @@ -246,7 +471,7 @@ namespace Engine vk::SubmitInfo{}.setCommandBuffers(cmd), fence); if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) { - ENGINE_ERROR("Cubemap upload: waitForFences failed"); + ENGINE_ERROR("BRDF LUT upload: waitForFences failed"); } device.GetDevice().destroyFence(fence); @@ -256,7 +481,7 @@ namespace Engine return imageAlloc; } - vk::ImageView CreateCubeView(Device& device, vk::Image image) + vk::ImageView CreateCubeView(Device& device, vk::Image image, u32 mips) { return device.GetDevice().createImageView( vk::ImageViewCreateInfo{} @@ -264,7 +489,43 @@ namespace Engine .setViewType(vk::ImageViewType::eCube) .setFormat(vk::Format::eR16G16B16A16Sfloat) .setSubresourceRange({vk::ImageAspectFlagBits::eColor, - 0, 1, 0, kFaceCount})); + 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})); + } + + 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); } } @@ -283,7 +544,7 @@ namespace Engine const u32 srcW = static_cast(w); const u32 srcH = static_cast(h); - // ---- 2. 重采样 6 面 + irradiance 卷积 + fp16 ---- + // ---- 2a. 环境 cube:重采样 6 面 ---- const u32 facePixels = faceSize * faceSize; std::vector envFaces(kFaceCount * facePixels * 4); for (u32 f = 0; f < kFaceCount; ++f) { @@ -303,17 +564,19 @@ namespace Engine } } - const u32 irrPixels = kIrradianceFaceSize * kIrradianceFaceSize; + // ---- 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 < kIrradianceFaceSize; ++y) { - for (u32 x = 0; x < kIrradianceFaceSize; ++x) { - const f32 u = (static_cast(x) + 0.5f) / static_cast(kIrradianceFaceSize); - const f32 v = (static_cast(y) + 0.5f) / static_cast(kIrradianceFaceSize); + 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) * kIrradianceFaceSize + x) * 4; + u16* px = dst + (static_cast(y) * irrSize + x) * 4; px[0] = FloatToHalf(rgb.r); px[1] = FloatToHalf(rgb.g); px[2] = FloatToHalf(rgb.b); @@ -321,47 +584,99 @@ namespace Engine } } } - stbi_image_free(pixels); // 源数据消费完毕(卷积/重采样都在构造期) - // ---- 3. 上传双 cube + 共享采样器 ---- - vk::Image envImage, irrImage; - auto envAlloc = UploadCube(device, allocator, envFaces, faceSize, envImage); - auto irrAlloc = UploadCube(device, allocator, irrFaces, - kIrradianceFaceSize, irrImage); + // ---- 2c. pre-filter cube:GGX 重要性采样 × mip 分级 roughness ---- + // 采样数随 mip 递增(高 roughness 分布更宽需要更多样本) + std::vector prefFaces; + std::vector prefMips; + prefMips.reserve(kPrefilterMips); + 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); + + const size_t offset = prefFaces.size(); + prefFaces.resize(offset + kFaceCount * dim * dim * 4); + for (u32 f = 0; f < kFaceCount; ++f) { + u16* dst = prefFaces.data() + offset + 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({prefFaces.data() + offset, dim}); + } + + 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 ---- + vk::Image envImage, irrImage, prefImage, lutImage; + auto envAlloc = UploadCube(device, allocator, + {{envFaces.data(), faceSize}}, envImage); + auto irrAlloc = UploadCube(device, allocator, + {{irrFaces.data(), irrSize}}, irrImage); + auto prefAlloc = UploadCube(device, allocator, prefMips, prefImage); + auto lutAlloc = UploadTexture2D(device, allocator, lutTexels, + kBrdfLutSize, + vk::Format::eR16G16Sfloat, lutImage); + envFaces.clear(); envFaces.shrink_to_fit(); irrFaces.clear(); irrFaces.shrink_to_fit(); + prefFaces.clear(); prefFaces.shrink_to_fit(); - vk::ImageView envView = CreateCubeView(device, envImage); - vk::ImageView irrView = CreateCubeView(device, irrImage); + vk::ImageView envView = CreateCubeView(device, envImage, 1); + vk::ImageView irrView = CreateCubeView(device, irrImage, 1); + vk::ImageView prefView = CreateCubeView(device, prefImage, + kPrefilterMips); + vk::ImageView lutView = CreateLutView(device, lutImage); - 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); + // 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 cubemap: {} ({}x6 env + {}x6 irradiance, fp16)", - path, faceSize, kIrradianceFaceSize); + 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, envImage, envView, - irrImage, irrView, sampler, - envAlloc, irrAlloc, faceSize)); + return Scope(new Cubemap(device, allocator, + envImage, envView, irrImage, irrView, + prefImage, prefView, kPrefilterMips, + lutImage, lutView, sampler, lutSampler, + envAlloc, irrAlloc, prefAlloc, lutAlloc, faceSize)); } Scope Cubemap::CreateNeutral(Device& device, MemoryAllocator& allocator, const glm::vec3& rgb) { - // 1×1×6 常数 cube:env 与 irradiance 同值,描述符恒有效 + // 1×1×6 常数 cube(prefilter 各 mip 同值);BRDF LUT 仍为真实 + // 积分(纯数学,无环境依赖)——描述符恒有效 const u16 px[4] = { FloatToHalf(rgb.r), FloatToHalf(rgb.g), FloatToHalf(rgb.b), 0x3C00 }; std::vector faces(kFaceCount * 4, 0); @@ -369,32 +684,51 @@ namespace Engine std::memcpy(faces.data() + f * 4, px, sizeof(px)); } - vk::Image envImage, irrImage; - auto envAlloc = UploadCube(device, allocator, faces, 1, envImage); - auto irrAlloc = UploadCube(device, allocator, faces, 1, irrImage); + std::vector prefFaces(kPrefilterMips * kFaceCount * 4, 0); + std::vector prefMips; + prefMips.reserve(kPrefilterMips); + for (u32 mip = 0; mip < kPrefilterMips; ++mip) { + const size_t offset = mip * kFaceCount * 4; + std::memcpy(prefFaces.data() + offset, faces.data(), + kFaceCount * 4 * sizeof(u16)); + prefMips.push_back({prefFaces.data() + offset, 1}); + } - vk::ImageView envView = CreateCubeView(device, envImage); - vk::ImageView irrView = CreateCubeView(device, irrImage); + 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); + } + } - vk::SamplerCreateInfo samplerInfo; - samplerInfo.setMagFilter(vk::Filter::eNearest) - .setMinFilter(vk::Filter::eNearest) - .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); + vk::Image envImage, irrImage, prefImage, lutImage; + auto envAlloc = UploadCube(device, allocator, {{faces.data(), 1}}, envImage); + auto irrAlloc = UploadCube(device, allocator, {{faces.data(), 1}}, irrImage); + auto prefAlloc = UploadCube(device, allocator, prefMips, prefImage); + auto lutAlloc = UploadTexture2D(device, allocator, lutTexels, + kBrdfLutSize, + vk::Format::eR16G16Sfloat, lutImage); - ENGINE_INFO("Neutral cubemap created (constant fallback)"); - return Scope(new Cubemap(device, allocator, envImage, envView, - irrImage, irrView, sampler, - envAlloc, irrAlloc, 1)); + vk::ImageView envView = CreateCubeView(device, envImage, 1); + vk::ImageView irrView = CreateCubeView(device, irrImage, 1); + vk::ImageView prefView = CreateCubeView(device, prefImage, + kPrefilterMips); + vk::ImageView lutView = CreateLutView(device, lutImage); + + 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, + envImage, envView, irrImage, irrView, + prefImage, prefView, kPrefilterMips, + lutImage, lutView, sampler, lutSampler, + envAlloc, irrAlloc, prefAlloc, lutAlloc, 1)); } Cubemap::~Cubemap() @@ -402,12 +736,17 @@ namespace Engine const auto& vkDevice = m_Device.GetDevice(); vkDevice.destroySampler(m_Sampler); + vkDevice.destroySampler(m_LutSampler); vkDevice.destroyImageView(m_EnvView); vkDevice.destroyImageView(m_IrrView); + vkDevice.destroyImageView(m_PrefView); + vkDevice.destroyImageView(m_LutView); // image 资源与显存经 VMA 成对释放 m_Allocator.Destroy(m_EnvAllocation); m_Allocator.Destroy(m_IrrAllocation); + m_Allocator.Destroy(m_PrefAllocation); + m_Allocator.Destroy(m_LutAllocation); } } // namespace Engine diff --git a/Engine/src/Renderer/Passes/SceneGpu.cpp b/Engine/src/Renderer/Passes/SceneGpu.cpp index 4dc1253..44b35a5 100644 --- a/Engine/src/Renderer/Passes/SceneGpu.cpp +++ b/Engine/src/Renderer/Passes/SceneGpu.cpp @@ -46,17 +46,19 @@ namespace Engine::Passes vk::ShaderStageFlagBits::eFragment}); textureSetLayout = descriptors.CreateLayout(texBindings); - // Set 2: CSM 级联阴影图(fragment 采样,binding = 级联序号) - // + 漫反射辐照度 cube(IBL 阶段②,binding 3) + // Set 2: CSM 级联阴影图(0/1/2)+ IBL 环境三件套(3 irradiance + // / 4 pre-filtered / 5 BRDF LUT),均 fragment 采样 std::vector shadowBindings; for (u32 c = 0; c < kMaxCascades; ++c) { shadowBindings.push_back( {c, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment}); } - shadowBindings.push_back( - {3, vk::DescriptorType::eCombinedImageSampler, - vk::ShaderStageFlagBits::eFragment}); + for (const u32 b : {3u, 4u, 5u}) { + shadowBindings.push_back( + {b, vk::DescriptorType::eCombinedImageSampler, + vk::ShaderStageFlagBits::eFragment}); + } shadowSetLayout = descriptors.CreateLayout(shadowBindings); // Forward pipeline layout:三个 set;无 push(GPUDriven 经 SSBO) @@ -164,10 +166,15 @@ namespace Engine::Passes void SceneGpu::BindEnvironment(const Cubemap& env) { - // irradiance view 全程稳定(不随 RG 重编译变化),绑一次即可 + // IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可 for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) { m_Descriptors.UpdateTexture(shadowSets[slot], 3, shadowSampler, env.GetIrradianceView()); + m_Descriptors.UpdateTexture(shadowSets[slot], 4, shadowSampler, + env.GetPrefilteredView()); + m_Descriptors.UpdateTexture(shadowSets[slot], 5, + env.GetBrdfLutSampler(), + env.GetBrdfLutView()); } } diff --git a/shaders/simple_shader.frag b/shaders/simple_shader.frag index 1490258..2c242e4 100644 --- a/shaders/simple_shader.frag +++ b/shaders/simple_shader.frag @@ -46,6 +46,8 @@ layout(set = 2, binding = 0) uniform sampler2D shadowMap0; // CSM 级联 0( layout(set = 2, binding = 1) uniform sampler2D shadowMap1; // 级联 1 layout(set = 2, binding = 2) uniform sampler2D shadowMap2; // 级联 2(最远) layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②) +layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③) +layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias) // 第 0 盏方向光(KeyLight)的 CSM 阴影因子:0.3 影中 / 1.0 受光。 // 级联按视图深度选择;采样器均 clamp-to-border 白色: @@ -166,14 +168,25 @@ void main() { Lo += (kD * albedo / PI + specular) * radiance * NdotL * shade; } - // 环境光:漫反射辐照度(IBL 阶段②,split-sum 的 diffuse 项)。 - // N 采样预卷积的 irradiance cube(半球 Lambert 积分已在加载期完成); - // kD 用粗糙度感知 Fresnel + (1-metallic)(金属无漫反射)。 - // ambientColor = 用户可调强度/色调乘子。镜面项待阶段③ BRDF LUT + // 环境光:IBL split-sum(阶段②③)。 + // 漫反射:N 采样 irradiance cube(半球 Lambert 预卷积) + // 镜面:R 采样 pre-filtered cube(mip = roughness 分级) + // × BRDF LUT 查表得 F 缩放/偏置 + // ambientColor = 用户可调强度/色调乘子 vec3 F_amb = FresnelSchlickRoughness(NdotV, F0, roughness); vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic); + vec3 irradiance = texture(irradianceMap, N).rgb; - vec3 ambient = kD_amb * irradiance * albedo * ubo.ambientColor.rgb; + vec3 diffuse = kD_amb * irradiance * albedo; + + vec3 R = reflect(-V, N); + const float kMaxPrefilterMip = 4.0; // 与 Cubemap::kPrefilterMips - 1 同步 + vec3 prefilteredColor = textureLod(prefilteredMap, R, + roughness * kMaxPrefilterMip).rgb; + vec2 envBRDF = texture(brdfLut, vec2(NdotV, roughness)).rg; + vec3 specular = prefilteredColor * (F_amb * envBRDF.x + envBRDF.y); + + vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb; vec3 color = ambient + Lo + emissive;