feat: IBL 预积分 GPU compute 化 + 环境资产入 Assets 层(动态切换)

CPU 逐像素卷积(venice 4k 阻塞 7.36s)→ 4 个 compute shader 一次性
提交:equirect→cube / irradiance / prefilter×5mip / BRDF LUT。
Cubemap 瘦身为纯资源持有类,生成职责移交 IblGenerator;
环境经 AssetManager 缓存(EnvRef),Renderer::SetEnvironment
动态换绑(RenderPass::OnEnvironmentChanged 广播)。
调试视图 +4:EnvCube / Irradiance / Prefilter / BRDF LUT。

性能前后对比(RTX 5060 Laptop,venice_dawn_1_4k.hdr):
- 改造前 CPU:7.36 s(主线程阻塞)
- 改造后 GPU:HDR decode 127-134ms + 资源 22-35ms + compute 17-19ms
  ≈ 165-190ms(约 40×);Neutral 兜底 ~6ms
- 运行中动态切换环境同量级(ms 级 GPU,decode 占大头)

冒烟:两构建目录 0 警告;运行期 0 validation(含两次环境切换 +
调试视图 9-12 轮播自动化验证)。
This commit is contained in:
2026-09-24 13:23:59 +08:00
parent 705250df76
commit 1d690067bd
30 changed files with 1162 additions and 824 deletions
+3 -739
View File
@@ -1,753 +1,17 @@
//
// Created by atdunbg on 2026/9/23.
//
// Cubemap 实现:HDR 等距柱状 → CPU 预积分四件套 → fp16 上传。
//
// 一次性加载成本(秒级 CPU),换取运行时零积分开销;硬件 Cube 采样
//(filtering/边缘接缝由 driver 处理)。split-sum 预积分(LearnOpenGL/
// UE 拆分方案的离线端):
// irradiance —— 半球 Lambert 积分,漫反射 ambient
// prefilter —— GGX 重要性采样(Hammersley 序列),mip 分级 roughness
// BRDF LUT —— (NdotV, roughness) → (F 缩放, F 偏置),纯数学表
// Cubemap 实现:纯资源持有(构造注入 + 成对析构)。
// 预积分生成见 IblGenerator(compute 链);CPU 积分路径已整体移除,
// 环境切换从秒级阻塞降到毫秒级 GPU 提交。
//
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Core/Log.h>
#include <stb/stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <vector>
namespace Engine
{
namespace
{
constexpr u32 kFaceCount = 6;
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
constexpr u32 kPhiSteps = 32; // 方位角采样数(512 采样/像素)
constexpr u32 kThetaSteps = 16; // 极角采样数
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
constexpr u32 kBrdfLutSamples = 128; // LUT 每像素采样数
// ---- GL/Vulkan cube 面方向表(与 GL spec 立方体选择表互逆)----
// (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位方向
glm::vec3 FaceDir(u32 face, f32 u, f32 v)
{
const f32 s = u * 2.0f - 1.0f;
const f32 t = v * 2.0f - 1.0f;
glm::vec3 dir;
switch (face) {
case 0: dir = { 1.0f, -t, -s }; break; // +X(面片顶行 = +Y)
case 1: dir = {-1.0f, -t, s }; break; // -X
case 2: dir = { s, 1.0f, t }; break; // +Y
case 3: dir = { s,-1.0f, -t }; break; // -Y
case 4: dir = { s, -t, 1.0f }; break; // +Z
default: dir = {-s, -t,-1.0f }; break; // -Z
}
return glm::normalize(dir); // equirect 查表要求单位方向
}
// 等距柱状双线性采样(x 环绕跨经度接缝,y 钳制到极点)。
// 图像行 0 = 天顶(+Y):v = acos(y)/π,与 stbi 行序一致,无需翻转
glm::vec3 SampleEquirect(const f32* img, u32 w, u32 h, const glm::vec3& dir)
{
const f32 u = std::atan2(dir.z, dir.x) * (0.5f / glm::pi<f32>()) + 0.5f;
const f32 v = std::acos(glm::clamp(dir.y, -1.0f, 1.0f)) / glm::pi<f32>();
const f32 fx = u * static_cast<f32>(w) - 0.5f;
const f32 fy = v * static_cast<f32>(h) - 0.5f;
const u32 x0 = static_cast<u32>(std::floor(fx));
const u32 y0 = static_cast<u32>(std::floor(fy));
const f32 tx = fx - std::floor(fx);
const f32 ty = fy - std::floor(fy);
auto fetch = [&](u32 x, u32 y) {
const u32 xc = ((x % w) + w) % w; // 经度环绕
const u32 yc = glm::clamp(y, 0u, h - 1u); // 纬度钳制
const size_t off = (static_cast<size_t>(yc) * w + xc) * 4;
return glm::vec3(img[off + 0], img[off + 1], img[off + 2]);
};
const glm::vec3 c00 = fetch(x0, y0);
const glm::vec3 c10 = fetch(x0 + 1, y0);
const glm::vec3 c01 = fetch(x0, y0 + 1);
const glm::vec3 c11 = fetch(x0 + 1, y0 + 1);
return glm::mix(glm::mix(c00, c10, tx), glm::mix(c01, c11, tx), ty);
}
// 漫反射辐照度半球积分(Lambert):E = ∫ L·cosθ·sinθ dθ dφ。
// 切线基 up×N 构造;均匀网格 ≈2048 采样/像素,对 32² 低频图足够
glm::vec3 ConvolveIrradiance(const f32* img, u32 w, u32 h,
const glm::vec3& N)
{
const glm::vec3 up = glm::abs(N.y) > 0.999f
? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
const glm::vec3 T = glm::normalize(glm::cross(up, N));
const glm::vec3 B = glm::cross(N, T);
const f32 dPhi = 2.0f * glm::pi<f32>() / static_cast<f32>(kPhiSteps);
const f32 dTheta = 0.5f * glm::pi<f32>() / static_cast<f32>(kThetaSteps);
glm::vec3 irradiance(0.0f);
for (u32 p = 0; p < kPhiSteps; ++p) {
const f32 phi = (static_cast<f32>(p) + 0.5f) * dPhi;
for (u32 t = 0; t < kThetaSteps; ++t) {
const f32 theta = (static_cast<f32>(t) + 0.5f) * dTheta;
const f32 sinT = std::sin(theta);
const f32 cosT = std::cos(theta);
const glm::vec3 sampleDir =
T * (sinT * std::cos(phi)) +
B * (sinT * std::sin(phi)) +
N * cosT;
irradiance += SampleEquirect(img, w, h, sampleDir) *
(cosT * sinT);
}
}
return irradiance * (dPhi * dTheta);
}
// ---- GGX 重要性采样(split-sum 镜面端)----
// van der Corput 基底反转(位反转技巧)。
// 注意是整数值转浮点(static_cast),不是位型重解读——
// bit_cast 会把反转后的整数当 IEEE754 解读,序列完全错乱
f32 RadicalInverseVdC(u32 bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return static_cast<f32>(bits) * 2.3283064365386963e-10f;
}
// n = 当次积分总采样数(xi.x = i/n 必须覆盖 [0,1] 全程)
glm::vec2 Hammersley(u32 i, u32 n)
{
return { static_cast<f32>(i) / static_cast<f32>(n),
RadicalInverseVdC(i) };
}
// GGX NDF 按概率密度取半程向量 H(切线空间 → 世界)
glm::vec3 ImportanceSampleGGX(const glm::vec2& xi, const glm::vec3& N,
f32 roughness)
{
const f32 a = roughness * roughness;
const f32 phi = 2.0f * glm::pi<f32>() * xi.x;
const f32 cosTheta =
std::sqrt((1.0f - xi.y) / (1.0f + (a * a - 1.0f) * xi.y));
const f32 sinTheta = std::sqrt(1.0f - cosTheta * cosTheta);
const glm::vec3 H(sinTheta * std::cos(phi),
sinTheta * std::sin(phi), cosTheta);
const glm::vec3 up = glm::abs(N.z) > 0.999f
? glm::vec3(1.0f, 0.0f, 0.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
const glm::vec3 T = glm::normalize(glm::cross(up, N));
const glm::vec3 B = glm::cross(N, T);
return glm::normalize(T * H.x + B * H.y + N * H.z);
}
// pre-filter 卷积:R(N) ≈ Σ L·NdotL / Σ NdotL(GGX 加权镜面均值)
glm::vec3 Prefilter(const f32* img, u32 w, u32 h,
const glm::vec3& N, f32 roughness, u32 samples)
{
const glm::vec3 V = N; // split-sum 约定:V 沿 N
glm::vec3 acc(0.0f);
f32 weight = 0.0f;
for (u32 i = 0; i < samples; ++i) {
const glm::vec3 H = ImportanceSampleGGX(
Hammersley(i, samples), N, roughness);
const glm::vec3 L = glm::normalize(2.0f * glm::dot(V, H) * H - V);
const f32 nDotL = glm::max(glm::dot(N, L), 0.0f);
if (nDotL > 0.0f) {
acc += SampleEquirect(img, w, h, L) * nDotL;
weight += nDotL;
}
}
return weight > 0.0f ? acc / weight : glm::vec3(0.0f);
}
// BRDF LUT 积分:(NdotV, roughness) → (A, B)。
// A/B 是 Fresnel 的缩放/偏置:specular = prefiltered·(F·A + B)
glm::vec2 IntegrateBrdf(f32 nDotV, f32 roughness)
{
// V 在以 N 为 +Z 的切线空间里(积分只依赖这两角度)
const glm::vec3 V(std::sqrt(1.0f - nDotV * nDotV), 0.0f, nDotV);
f32 a = 0.0f, b = 0.0f;
const f32 k = roughness * roughness * 0.5f; // IBL 版 Smith k
for (u32 i = 0; i < kBrdfLutSamples; ++i) {
const glm::vec2 xi = Hammersley(i, kBrdfLutSamples);
const glm::vec3 H = ImportanceSampleGGX(xi, glm::vec3(0, 0, 1),
roughness);
const glm::vec3 L = glm::normalize(
2.0f * glm::dot(V, H) * H - V);
const f32 nDotL = glm::max(L.z, 0.0f);
const f32 nDotH = glm::max(H.z, 0.0f);
const f32 vDotH = glm::max(glm::dot(V, H), 0.0f);
if (nDotL <= 0.0f) continue;
const f32 g = (nDotV / (nDotV * (1.0f - k) + k)) *
(nDotL / (nDotL * (1.0f - k) + k));
// 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),
// 0/0 = NaN 会累进 LUT——分母钳 epsilon
const f32 gVis = g * vDotH /
glm::max(nDotH * nDotV, 1e-6f);
const f32 fc = 1.0f - vDotH;
const f32 f = fc * fc * fc * fc * fc; // (1-vDotH)⁵,乘法链
a += (1.0f - f) * gVis;
b += f * gVis;
}
return { a / static_cast<f32>(kBrdfLutSamples),
b / static_cast<f32>(kBrdfLutSamples) };
}
// IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf)
u16 FloatToHalf(f32 value)
{
const u32 x = std::bit_cast<u32>(value);
const u16 sign = static_cast<u16>((x >> 16) & 0x8000u);
const u32 absVal = x & 0x7fffffffu;
if (absVal > 0x7f800000u) {
return static_cast<u16>(sign | 0x7e00u); // NaN
}
if (absVal >= 0x47800000u) {
return static_cast<u16>(sign | 0x7c00u); // Inf / 溢出饱和
}
if (absVal >= 0x38800000u) {
const u32 bits = absVal - 0x38000000u;
const u32 round = 0x0fffu + ((bits >> 13) & 1u);
return static_cast<u16>(sign | ((bits + round) >> 13));
}
if (absVal < 0x33000000u) {
return sign; // < 2^-25:舍入为 0
}
const u32 mantissa = (absVal & 0x007fffffu) | 0x00800000u;
const u32 shift = 126u - (absVal >> 23u);
const u32 m = (mantissa + (1u << (shift - 1))) >> shift;
return static_cast<u16>(sign | std::min(m, 0x400u));
}
// ---- 上传 ----
vk::ImageView CreateCubeView(Device& device, vk::Image image, u32 mips)
{
return device.GetDevice().createImageView(
vk::ImageViewCreateInfo{}
.setImage(image)
.setViewType(vk::ImageViewType::eCube)
.setFormat(vk::Format::eR16G16B16A16Sfloat)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, mips, 0, kFaceCount}));
}
vk::ImageView CreateLutView(Device& device, vk::Image image)
{
return device.GetDevice().createImageView(
vk::ImageViewCreateInfo{}
.setImage(image)
.setViewType(vk::ImageViewType::e2D)
.setFormat(vk::Format::eR16G16Sfloat)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1}));
}
// cube mip 链数据:[mip0: 6 面][mip1: 6 面]...,每面 faceSize²×8B
struct CubeMipData {
const u16* data; // 6 面连续
u32 faceSize;
};
// 上传 cube(含 mip 链)为 eCubeCompatible image(fp16),
// 返回完整资源单元(image + view + 显存分配)
EnvTexture UploadCube(Device& device, MemoryAllocator& allocator,
const std::vector<CubeMipData>& mips,
u32 viewMipCount)
{
const u32 mipCount = static_cast<u32>(mips.size());
// staging:各 mip 紧凑排布,记偏移
vk::DeviceSize total = 0;
for (const auto& mip : mips) {
total += static_cast<vk::DeviceSize>(mip.faceSize) *
mip.faceSize * 8 * kFaceCount;
}
auto stagingAlloc = allocator.CreateBuffer(
total,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
{
auto* dst = static_cast<u8*>(stagingAlloc.MappedPtr);
vk::DeviceSize off = 0;
for (const auto& mip : mips) {
const vk::DeviceSize bytes =
static_cast<vk::DeviceSize>(mip.faceSize) *
mip.faceSize * 8 * kFaceCount;
std::memcpy(dst + off, mip.data,
static_cast<size_t>(bytes));
off += bytes;
}
}
const u32 baseSize = mips[0].faceSize;
vk::ImageCreateInfo imageInfo;
imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(vk::Format::eR16G16B16A16Sfloat)
.setExtent({baseSize, baseSize, 1})
.setMipLevels(mipCount)
.setArrayLayers(kFaceCount)
.setSamples(vk::SampleCountFlagBits::e1)
.setTiling(vk::ImageTiling::eOptimal)
.setFlags(vk::ImageCreateFlagBits::eCubeCompatible)
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled)
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
auto imageAlloc = allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
const vk::Image image = imageAlloc.Image;
vk::CommandPoolCreateInfo poolInfo;
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
vk::CommandPool pool = device.GetDevice().createCommandPool(poolInfo);
vk::CommandBuffer cmd = device.GetDevice().allocateCommandBuffers(
vk::CommandBufferAllocateInfo{}
.setCommandPool(pool)
.setLevel(vk::CommandBufferLevel::ePrimary)
.setCommandBufferCount(1))[0];
cmd.begin(vk::CommandBufferBeginInfo{
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
// 全 mip × 全层一起转 layout
vk::ImageMemoryBarrier2 toTransfer;
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, mipCount, 0, kFaceCount});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
// 逐 mip 逐面拷贝;buffer 偏移必须按 mips[0..mip) 字节数累积
//(mip 面尺寸递减,用当前 dim 乘 mip 序号会错位 → 读到
// staging 未初始化数据 → fp16 NaN 污染整屏)
vk::DeviceSize offset = 0;
for (u32 mip = 0; mip < mipCount; ++mip) {
const u32 dim = mips[mip].faceSize;
for (u32 f = 0; f < kFaceCount; ++f) {
vk::BufferImageCopy region;
region.setBufferOffset(
offset + static_cast<vk::DeviceSize>(dim) *
dim * 8 * f)
.setBufferRowLength(0)
.setBufferImageHeight(0)
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
mip, f, 1})
.setImageOffset({0, 0, 0})
.setImageExtent({dim, dim, 1});
cmd.copyBufferToImage(stagingAlloc.Buffer, image,
vk::ImageLayout::eTransferDstOptimal,
region);
}
offset += static_cast<vk::DeviceSize>(dim) * dim * 8 *
kFaceCount;
}
vk::ImageMemoryBarrier2 toShader;
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, mipCount, 0, kFaceCount});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
cmd.end();
vk::Fence fence = device.GetDevice().createFence({});
device.GetGraphicsQueue().submit(
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
vk::Result::eSuccess) {
ENGINE_ERROR("Cubemap upload: waitForFences failed");
}
device.GetDevice().destroyFence(fence);
device.GetDevice().destroyCommandPool(pool);
allocator.Destroy(stagingAlloc);
return {imageAlloc.Image,
CreateCubeView(device, imageAlloc.Image, viewMipCount),
imageAlloc};
}
// 上传 BRDF LUT(2D,R16G16Sfloat,8B/px),返回完整资源单元
EnvTexture UploadTexture2D(Device& device,
MemoryAllocator& allocator,
const std::vector<u16>& texels,
u32 size, vk::Format format)
{
const vk::DeviceSize total =
static_cast<vk::DeviceSize>(size) * size * 8;
auto stagingAlloc = allocator.CreateBuffer(
total,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
std::memcpy(stagingAlloc.MappedPtr, texels.data(),
static_cast<size_t>(total));
vk::ImageCreateInfo imageInfo;
imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(format)
.setExtent({size, size, 1})
.setMipLevels(1)
.setArrayLayers(1)
.setSamples(vk::SampleCountFlagBits::e1)
.setTiling(vk::ImageTiling::eOptimal)
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled)
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
auto imageAlloc = allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
const vk::Image image = imageAlloc.Image;
vk::CommandPoolCreateInfo poolInfo;
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
vk::CommandPool pool = device.GetDevice().createCommandPool(poolInfo);
vk::CommandBuffer cmd = device.GetDevice().allocateCommandBuffers(
vk::CommandBufferAllocateInfo{}
.setCommandPool(pool)
.setLevel(vk::CommandBufferLevel::ePrimary)
.setCommandBufferCount(1))[0];
cmd.begin(vk::CommandBufferBeginInfo{
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
vk::ImageMemoryBarrier2 toTransfer;
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
vk::BufferImageCopy region;
region.setBufferOffset(0)
.setBufferRowLength(0)
.setBufferImageHeight(0)
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
0, 0, 1})
.setImageOffset({0, 0, 0})
.setImageExtent({size, size, 1});
cmd.copyBufferToImage(stagingAlloc.Buffer, image,
vk::ImageLayout::eTransferDstOptimal, region);
vk::ImageMemoryBarrier2 toShader;
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1});
cmd.pipelineBarrier2(
vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
cmd.end();
vk::Fence fence = device.GetDevice().createFence({});
device.GetGraphicsQueue().submit(
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
vk::Result::eSuccess) {
ENGINE_ERROR("BRDF LUT upload: waitForFences failed");
}
device.GetDevice().destroyFence(fence);
device.GetDevice().destroyCommandPool(pool);
allocator.Destroy(stagingAlloc);
return {imageAlloc.Image,
CreateLutView(device, imageAlloc.Image), imageAlloc};
}
// fp16 NaN 自检(指数全 1 且尾数非 0):预积分数据上传前的
// 最后一道防线——NaN 进 GPU 会污染整屏输出
bool ContainsNaNHalf(const u16* data, size_t count)
{
for (size_t i = 0; i < count; ++i) {
const u32 abs = data[i] & 0x7FFFu;
if (abs > 0x7C00u) return true;
}
return false;
}
vk::Sampler CreateEnvSampler(Device& device, u32 maxLod)
{
vk::SamplerCreateInfo samplerInfo;
samplerInfo.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge)
.setAnisotropyEnable(VK_FALSE)
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
.setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE)
.setMinLod(0.0f)
.setMaxLod(static_cast<f32>(maxLod));
return device.GetDevice().createSampler(samplerInfo);
}
// BRDF LUT 专用:clamp-to-edge(共享 border 白采样器会在 uv=1.0
// 处混入白色 border,高 roughness 镜面偏大)
vk::Sampler CreateLutSampler(Device& device)
{
return CreateEnvSampler(device, 0);
}
}
Scope<Cubemap> Cubemap::CreateFromHdrEquirect(Device& device,
MemoryAllocator& allocator,
const std::string& path,
u32 faceSize)
{
// ---- 1. CPU 加载 HDR(float RGBA)----
int w = 0, h = 0, channels = 0;
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
if (!pixels) {
ENGINE_ERROR("Cubemap: failed to load HDR: {}", path);
return nullptr;
}
const u32 srcW = static_cast<u32>(w);
const u32 srcH = static_cast<u32>(h);
// ---- 2a. 环境 cube:重采样 6 面 ----
const u32 facePixels = faceSize * faceSize;
std::vector<u16> envFaces(kFaceCount * facePixels * 4);
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = envFaces.data() + f * facePixels * 4;
for (u32 y = 0; y < faceSize; ++y) {
for (u32 x = 0; x < faceSize; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(faceSize);
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(faceSize);
const glm::vec3 rgb =
SampleEquirect(pixels, srcW, srcH, FaceDir(f, u, v));
u16* px = dst + (static_cast<size_t>(y) * faceSize + x) * 4;
px[0] = FloatToHalf(rgb.r);
px[1] = FloatToHalf(rgb.g);
px[2] = FloatToHalf(rgb.b);
px[3] = 0x3C00; // 1.0
}
}
}
// ---- 2b. irradiance cube:半球 Lambert 积分 ----
const u32 irrSize = kIrradianceFaceSize;
const u32 irrPixels = irrSize * irrSize;
std::vector<u16> irrFaces(kFaceCount * irrPixels * 4);
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = irrFaces.data() + f * irrPixels * 4;
for (u32 y = 0; y < irrSize; ++y) {
for (u32 x = 0; x < irrSize; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(irrSize);
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(irrSize);
const glm::vec3 rgb = ConvolveIrradiance(
pixels, srcW, srcH, FaceDir(f, u, v));
u16* px = dst + (static_cast<size_t>(y) * irrSize + x) * 4;
px[0] = FloatToHalf(rgb.r);
px[1] = FloatToHalf(rgb.g);
px[2] = FloatToHalf(rgb.b);
px[3] = 0x3C00;
}
}
}
// ---- 2c. pre-filter cube:GGX 重要性采样 × mip 分级 roughness ----
// 采样数随 mip 递增(高 roughness 分布更宽需要更多样本)。
// 注意:必须一次性分配总容量——逐 mip resize 会 realloc,使
// prefMips 里已记录的 data 指针悬空(上传读到垃圾 = NaN 全黑)
u32 prefTotal = 0;
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const u32 dim = kPrefilterFaceSize >> mip;
prefTotal += kFaceCount * dim * dim * 4;
}
std::vector<u16> prefFaces(prefTotal);
std::vector<CubeMipData> prefMips;
prefMips.reserve(kPrefilterMips);
size_t prefOffset = 0;
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
const u32 dim = kPrefilterFaceSize >> mip;
const f32 roughness =
static_cast<f32>(mip) / static_cast<f32>(kPrefilterMips - 1);
const u32 samples = std::min(32u << mip, 256u);
u16* base = prefFaces.data() + prefOffset;
for (u32 f = 0; f < kFaceCount; ++f) {
u16* dst = base + f * dim * dim * 4;
for (u32 y = 0; y < dim; ++y) {
for (u32 x = 0; x < dim; ++x) {
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(dim);
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(dim);
const glm::vec3 rgb = Prefilter(
pixels, srcW, srcH, FaceDir(f, u, v),
roughness, samples);
u16* px = dst + (static_cast<size_t>(y) * dim + x) * 4;
px[0] = FloatToHalf(rgb.r);
px[1] = FloatToHalf(rgb.g);
px[2] = FloatToHalf(rgb.b);
px[3] = 0x3C00;
}
}
}
prefMips.push_back({base, dim});
prefOffset += kFaceCount * dim * dim * 4;
}
stbi_image_free(pixels); // 源数据消费完毕
// ---- 2d. BRDF LUT:纯数学积分表(与环境内容无关)----
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
for (u32 y = 0; y < kBrdfLutSize; ++y) {
const f32 roughness = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(kBrdfLutSize);
for (u32 x = 0; x < kBrdfLutSize; ++x) {
const f32 nDotV = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(kBrdfLutSize);
const glm::vec2 ab = IntegrateBrdf(nDotV, roughness);
u16* px = lutTexels.data() + (static_cast<size_t>(y) * kBrdfLutSize + x) * 2;
px[0] = FloatToHalf(ab.x);
px[1] = FloatToHalf(ab.y);
}
}
// ---- 3. 上传:env / irradiance / prefilter / LUT(NaN 自检)----
if (ContainsNaNHalf(envFaces.data(), envFaces.size()) ||
ContainsNaNHalf(irrFaces.data(), irrFaces.size()) ||
ContainsNaNHalf(prefFaces.data(), prefFaces.size())) {
ENGINE_ERROR("Cubemap: NaN in pre-integrated data, "
"skipping IBL upload");
return nullptr;
}
if (ContainsNaNHalf(lutTexels.data(), lutTexels.size())) {
ENGINE_ERROR("Cubemap: NaN in BRDF LUT");
return nullptr;
}
EnvTexture env = UploadCube(device, allocator,
{{envFaces.data(), faceSize}}, 1);
EnvTexture irr = UploadCube(device, allocator,
{{irrFaces.data(), irrSize}}, 1);
EnvTexture pref = UploadCube(device, allocator, prefMips,
kPrefilterMips);
EnvTexture lut = UploadTexture2D(device, allocator, lutTexels,
kBrdfLutSize,
vk::Format::eR16G16Sfloat);
envFaces.clear(); envFaces.shrink_to_fit();
irrFaces.clear(); irrFaces.shrink_to_fit();
prefFaces.clear(); prefFaces.shrink_to_fit();
// cube sampler 共享:maxLod 覆盖 pre-filter mip 链(textureLod 显式
// 采样会被 clamp 到 maxLod,必须 ≥ mipCount-1)
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);
vk::Sampler lutSampler = CreateLutSampler(device);
ENGINE_INFO("Environment IBL: {} ({}x6 env, {}x6 irradiance, "
"{}x6 prefilter {} mips, {}² BRDF LUT)",
path, faceSize, irrSize, kPrefilterFaceSize,
kPrefilterMips, kBrdfLutSize);
return Scope<Cubemap>(new Cubemap(device, allocator,
env, irr, pref, lut, sampler, lutSampler));
}
Scope<Cubemap> Cubemap::CreateNeutral(Device& device,
MemoryAllocator& allocator,
const glm::vec3& rgb)
{
// 1×1×6 常数 cube(prefilter 仅 1 mip——1px 图不能建 mip 链,
// 采样越界 lod 会被 view range clamp 到 mip0,效果等同);
// BRDF LUT 仍为真实积分(纯数学,无环境依赖)——描述符恒有效
const u16 px[4] = { FloatToHalf(rgb.r), FloatToHalf(rgb.g),
FloatToHalf(rgb.b), 0x3C00 };
std::vector<u16> faces(kFaceCount * 4, 0);
for (u32 f = 0; f < kFaceCount; ++f) {
std::memcpy(faces.data() + f * 4, px, sizeof(px));
}
std::vector<u16> prefFaces(kFaceCount * 4, 0);
std::vector<CubeMipData> prefMips = {{prefFaces.data(), 1}};
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
for (u32 y = 0; y < kBrdfLutSize; ++y) {
const f32 roughness = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(kBrdfLutSize);
for (u32 x = 0; x < kBrdfLutSize; ++x) {
const f32 nDotV = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(kBrdfLutSize);
const glm::vec2 ab = IntegrateBrdf(nDotV, roughness);
u16* t = lutTexels.data() + (static_cast<size_t>(y) * kBrdfLutSize + x) * 2;
t[0] = FloatToHalf(ab.x);
t[1] = FloatToHalf(ab.y);
}
}
EnvTexture env = UploadCube(device, allocator, {{faces.data(), 1}}, 1);
EnvTexture irr = UploadCube(device, allocator, {{faces.data(), 1}}, 1);
EnvTexture pref = UploadCube(device, allocator, prefMips, 1);
EnvTexture lut = UploadTexture2D(device, allocator, lutTexels,
kBrdfLutSize, vk::Format::eR16G16Sfloat);
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);
vk::Sampler lutSampler = CreateLutSampler(device);
ENGINE_INFO("Neutral IBL environment (constant fallback, real BRDF LUT)");
return Scope<Cubemap>(new Cubemap(device, allocator,
env, irr, pref, lut, sampler, lutSampler));
}
Cubemap::~Cubemap()
{
const auto& vkDevice = m_Device.GetDevice();