feat: 环境背景管线(IBL 阶段①)——HDR 等距柱状转 cube + Skybox pass

- 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)))
This commit is contained in:
2026-09-23 18:37:01 +08:00
parent 89bf4256e8
commit 4b5990e6c8
11 changed files with 662 additions and 1 deletions
+293
View File
@@ -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 <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;
// 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<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);
}
// IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf)。
// HDR 环境值域内常规路径足够,次正规/NaN 路径只为数学完备
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) {
// 常规数:指数重偏(127→15),尾数右移 13 位并舍入到最近偶数
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
}
// 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<u16>(sign | std::min(m, 0x400u));
}
}
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);
// ---- 2. 重采样 6 面 + fp16(RGBA16F = 每像素 8B)----
const u32 facePixels = faceSize * faceSize;
std::vector<u16> 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<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
}
}
}
stbi_image_free(pixels);
// ---- 3. staging(HOST_VISIBLE 持久映射,逐面偏移拷入)----
const vk::DeviceSize faceBytes =
static_cast<vk::DeviceSize>(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<size_t>(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<Cubemap>(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