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:
@@ -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 <Engine/Core/Types.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
|
||||
class Cubemap {
|
||||
public:
|
||||
// 从等距柱状 HDR 生成(CPU 重采样到 6 面,faceSize 为每面边长)
|
||||
static Scope<Cubemap> 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
|
||||
@@ -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 <Engine/Core/FrameContext.h>
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/SceneTypes.h>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
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<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
|
||||
};
|
||||
|
||||
} // namespace Passes
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_SKYBOXPASS_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<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
|
||||
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
|
||||
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
|
||||
// 环境贴图(IBL 阶段①):HDR → cube,被 SkyboxPass 引用;
|
||||
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
||||
Scope<Cubemap> m_Environment;
|
||||
Scope<Swapchain> m_Swapchain;
|
||||
Scope<SyncManager> m_SyncManager;
|
||||
Scope<CommandManager> m_CommandManager;
|
||||
|
||||
@@ -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<glm::mat4> instanceModels;
|
||||
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
|
||||
|
||||
@@ -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
|
||||
@@ -22,9 +22,10 @@ namespace Engine::Passes
|
||||
std::array<float, 4>{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);
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/SkyboxPass.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <tracy/Tracy.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
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<DescriptorBinding> 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<vk::DescriptorSetLayout>{m_SetLayout},
|
||||
sizeof(glm::mat4));
|
||||
|
||||
const std::string shaderDir = "shaders/";
|
||||
auto vert = shaders.CreateFromFile(
|
||||
shaderDir + "skybox.vert.spv",
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
|
||||
sizeof(glm::mat4));
|
||||
auto frag = shaders.CreateFromFile(
|
||||
shaderDir + "skybox.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
std::vector<vk::DescriptorSetLayout>{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<float, 4>{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<vk::ShaderStageFlagBits, 2> stages = {
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
vk::ShaderStageFlagBits::eFragment
|
||||
};
|
||||
std::array<vk::ShaderEXT, 2> shaders = {m_Vert, m_Frag};
|
||||
cmd.bindShadersEXT(stages, shaders);
|
||||
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Layout, 0,
|
||||
m_Sets[ctx.frame.frameSlot], {});
|
||||
cmd.pushConstants<glm::mat4>(
|
||||
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<float>(ctx.extent.width),
|
||||
static_cast<float>(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
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||
#include <Engine/Renderer/Passes/SkyboxPass.h>
|
||||
#include <Engine/Renderer/Passes/ShadowPass.h>
|
||||
#include <Engine/Renderer/Passes/ForwardPass.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Renderer/Passes/TAAPass.h>
|
||||
#include <Engine/Renderer/Passes/BloomPass.h>
|
||||
#include <Engine/Renderer/Passes/TonemapPass.h>
|
||||
@@ -38,6 +40,7 @@
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
|
||||
namespace Engine
|
||||
@@ -71,6 +74,14 @@ namespace Engine
|
||||
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
|
||||
*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<Swapchain>(*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<Passes::ShadowPass>(
|
||||
*m_SceneGpu, frame, m_ShadowMaps));
|
||||
// Skybox = HDRColor 的 first writer(清屏 + 画背景),先于 Forward 注册
|
||||
m_Passes.push_back(CreateScope<Passes::SkyboxPass>(
|
||||
*m_Device, *m_ShaderManager, *m_DescriptorManager, frame,
|
||||
m_HDRColor, m_Environment.get()));
|
||||
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
|
||||
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps));
|
||||
m_Passes.push_back(CreateScope<Passes::TAAPass>(
|
||||
|
||||
@@ -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}; // 无灯兜底:头顶正射
|
||||
|
||||
Reference in New Issue
Block a user