mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
661 lines
23 KiB
C++
661 lines
23 KiB
C++
#include "VulkanPostProcessing.h"
|
|
|
|
#include <array>
|
|
|
|
#include "gr_vulkan.h"
|
|
#include "VulkanBarrier.h"
|
|
#include "VulkanRenderer.h"
|
|
#include "VulkanBuffer.h"
|
|
#include "VulkanTexture.h"
|
|
#include "VulkanPipeline.h"
|
|
#include "VulkanDescriptorManager.h"
|
|
#include "graphics/util/uniform_structs.h"
|
|
#include "graphics/grinternal.h"
|
|
#include "graphics/matrix.h"
|
|
#include "graphics/2d.h"
|
|
#include "lighting/lighting_profiles.h"
|
|
#include "lighting/lighting.h"
|
|
#include "math/vecmat.h"
|
|
#include "render/3d.h"
|
|
#include "tracing/tracing.h"
|
|
#include "nebula/neb.h"
|
|
#include "nebula/volumetrics.h"
|
|
#include "mission/missionparse.h"
|
|
|
|
extern float Sun_spot;
|
|
extern int Game_subspace_effect;
|
|
extern SCP_vector<light> Lights;
|
|
extern int Num_lights;
|
|
|
|
|
|
namespace graphics::vulkan {
|
|
|
|
|
|
// ========== Fog / Volumetric Nebula ==========
|
|
|
|
void VulkanFog::init(PostProcessContext& ctx, const RenderTarget& sceneColor,
|
|
const RenderTarget& sceneDepth, const RenderTarget& sceneDepthCopy,
|
|
const VulkanDeferredGBuffer& gbuffer)
|
|
{
|
|
m_ctx = &ctx;
|
|
m_sceneColor = &sceneColor;
|
|
m_sceneDepth = &sceneDepth;
|
|
m_sceneDepthCopy = &sceneDepthCopy;
|
|
m_gbuffer = &gbuffer;
|
|
}
|
|
|
|
void VulkanFog::copySceneDepth(vk::CommandBuffer cmd)
|
|
{
|
|
// Copies scene depth → depth copy texture so the fog shaders can sample it.
|
|
// Scene depth is in eDepthStencilAttachmentOptimal (from the ended scene render pass).
|
|
copyImageToImage(cmd,
|
|
m_sceneDepth->image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal,
|
|
m_sceneDepthCopy->image, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal,
|
|
m_ctx->sceneExtent,
|
|
imageAspectFromFormat(m_ctx->depthFormat));
|
|
}
|
|
|
|
bool VulkanFog::initFogPass()
|
|
{
|
|
if (m_fogInitialized) {
|
|
return true;
|
|
}
|
|
|
|
// Create fog render pass: 1 RGBA16F color attachment, loadOp=eDontCare (writing every pixel),
|
|
// initialLayout/finalLayout = eColorAttachmentOptimal (scene color stays as render target)
|
|
{
|
|
vk::AttachmentDescription att;
|
|
att.format = HDR_COLOR_FORMAT;
|
|
att.samples = vk::SampleCountFlagBits::e1;
|
|
att.loadOp = vk::AttachmentLoadOp::eDontCare;
|
|
att.storeOp = vk::AttachmentStoreOp::eStore;
|
|
att.stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
|
|
att.stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
|
|
att.initialLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
att.finalLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
|
|
vk::AttachmentReference colorRef;
|
|
colorRef.attachment = 0;
|
|
colorRef.layout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
|
|
vk::SubpassDescription subpass;
|
|
subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
|
|
subpass.colorAttachmentCount = 1;
|
|
subpass.pColorAttachments = &colorRef;
|
|
|
|
vk::SubpassDependency dep;
|
|
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
dep.dstSubpass = 0;
|
|
dep.srcStageMask = vk::PipelineStageFlagBits::eFragmentShader
|
|
| vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
dep.dstStageMask = vk::PipelineStageFlagBits::eFragmentShader
|
|
| vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
|
dep.srcAccessMask = vk::AccessFlagBits::eShaderRead
|
|
| vk::AccessFlagBits::eColorAttachmentWrite;
|
|
dep.dstAccessMask = vk::AccessFlagBits::eShaderRead
|
|
| vk::AccessFlagBits::eColorAttachmentWrite;
|
|
|
|
vk::RenderPassCreateInfo rpInfo;
|
|
rpInfo.attachmentCount = 1;
|
|
rpInfo.pAttachments = &att;
|
|
rpInfo.subpassCount = 1;
|
|
rpInfo.pSubpasses = &subpass;
|
|
rpInfo.dependencyCount = 1;
|
|
rpInfo.pDependencies = &dep;
|
|
|
|
try {
|
|
m_fogRenderPass = m_ctx->device.createRenderPass(rpInfo);
|
|
} catch (const vk::SystemError& e) {
|
|
nprintf(("vulkan", "VulkanPostProcessor: Failed to create fog render pass: %s\n", e.what()));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Create fog framebuffer (scene color as attachment)
|
|
if (!createFogFramebuffer()) {
|
|
return false;
|
|
}
|
|
|
|
m_fogInitialized = true;
|
|
nprintf(("vulkan", "VulkanPostProcessor: Fog pass initialized\n"));
|
|
return true;
|
|
}
|
|
|
|
bool VulkanFog::createFogFramebuffer()
|
|
{
|
|
vk::FramebufferCreateInfo fbInfo;
|
|
fbInfo.renderPass = m_fogRenderPass;
|
|
fbInfo.attachmentCount = 1;
|
|
fbInfo.pAttachments = &m_sceneColor->view;
|
|
fbInfo.width = m_ctx->sceneExtent.width;
|
|
fbInfo.height = m_ctx->sceneExtent.height;
|
|
fbInfo.layers = 1;
|
|
|
|
try {
|
|
m_fogFramebuffer = m_ctx->device.createFramebuffer(fbInfo);
|
|
} catch (const vk::SystemError& e) {
|
|
nprintf(("vulkan", "VulkanPostProcessor: Failed to create fog framebuffer: %s\n", e.what()));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void VulkanFog::destroyEmissiveMipmapped()
|
|
{
|
|
if (m_emissiveMipmappedFullView) {
|
|
m_ctx->device.destroyImageView(m_emissiveMipmappedFullView);
|
|
m_emissiveMipmappedFullView = nullptr;
|
|
}
|
|
m_ctx->destroyTarget(m_emissiveMipmapped);
|
|
m_emissiveMipLevels = 0;
|
|
m_emissiveMipmappedInitialized = false;
|
|
}
|
|
|
|
bool VulkanFog::onResize()
|
|
{
|
|
if (!m_ctx || !m_ctx->device) {
|
|
return true;
|
|
}
|
|
|
|
// The emissive mip chain is extent-sized; drop it and let renderVolumetric()
|
|
// lazily rebuild it at the new extent.
|
|
destroyEmissiveMipmapped();
|
|
|
|
if (!m_fogInitialized) {
|
|
// Fog pass doesn't exist yet; lazy init picks up the new views.
|
|
return true;
|
|
}
|
|
|
|
// Recreate the framebuffer: it attaches the scene color view, which the
|
|
// post-processor resize just recreated. The render pass is kept.
|
|
if (m_fogFramebuffer) {
|
|
m_ctx->device.destroyFramebuffer(m_fogFramebuffer);
|
|
m_fogFramebuffer = nullptr;
|
|
}
|
|
return createFogFramebuffer();
|
|
}
|
|
|
|
void VulkanFog::shutdown()
|
|
{
|
|
if (!m_ctx || !m_ctx->device) {
|
|
return;
|
|
}
|
|
|
|
destroyEmissiveMipmapped();
|
|
|
|
if (m_fogFramebuffer) {
|
|
m_ctx->device.destroyFramebuffer(m_fogFramebuffer);
|
|
m_fogFramebuffer = nullptr;
|
|
}
|
|
if (m_fogRenderPass) {
|
|
m_ctx->device.destroyRenderPass(m_fogRenderPass);
|
|
m_fogRenderPass = nullptr;
|
|
}
|
|
m_fogInitialized = false;
|
|
}
|
|
|
|
void VulkanFog::renderScene(vk::CommandBuffer cmd)
|
|
{
|
|
GR_DEBUG_SCOPE("Scene Fog");
|
|
|
|
if (!m_fogInitialized) {
|
|
if (!initFogPass()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto* pipelineMgr = getPipelineManager();
|
|
auto* descriptorMgr = getDescriptorManager();
|
|
auto* bufferMgr = getBufferManager();
|
|
auto* texMgr = getTextureManager();
|
|
|
|
if (!pipelineMgr || !descriptorMgr || !bufferMgr || !texMgr) {
|
|
return;
|
|
}
|
|
|
|
// Copy scene depth for fog sampling
|
|
copySceneDepth(cmd);
|
|
|
|
// Transition scene color: eShaderReadOnlyOptimal -> eColorAttachmentOptimal
|
|
{
|
|
ImageBarrier2 barrier;
|
|
barrier.image = m_sceneColor->image;
|
|
barrier.levelCount = 1;
|
|
barrier.layerCount = 1;
|
|
barrier.oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
|
barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
|
barrier.srcAccess = {};
|
|
barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput;
|
|
barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
|
|
|
|
cmdImageBarrier(cmd, barrier);
|
|
}
|
|
|
|
// Fill fog UBO
|
|
graphics::generic_data::fog_data fogData;
|
|
{
|
|
float fog_near, fog_density;
|
|
neb2_get_adjusted_fog_values(&fog_near, &fog_density);
|
|
unsigned char r, g, b;
|
|
neb2_get_fog_color(&r, &g, &b);
|
|
|
|
fogData.fog_start = fog_near;
|
|
fogData.fog_density = fog_density;
|
|
fogData.fog_color.xyz.x = r / 255.f;
|
|
fogData.fog_color.xyz.y = g / 255.f;
|
|
fogData.fog_color.xyz.z = b / 255.f;
|
|
fogData.zNear = Min_draw_distance;
|
|
fogData.zFar = Max_draw_distance;
|
|
fogData.clip_dist = Neb2_fog_clip_distance;
|
|
fogData.clip_inf_dist = Neb2_fog_skybox_clip_distance;
|
|
}
|
|
|
|
// Hand-rolled fullscreen pass rather than PostProcessContext::drawFullscreenTriangle:
|
|
// the scene-fog shader samples the scene-depth copy at Material binding 4
|
|
// (DepthMap), which the shared helper doesn't bind — it only writes a single
|
|
// color texture into TextureArray[0]. The per-frame scratch-ring UBO plumbing is
|
|
// shared; only the custom Material-set binds stay local.
|
|
PipelineConfig config;
|
|
config.shaderType = SDR_TYPE_SCENE_FOG;
|
|
config.vertexLayoutHash = 0;
|
|
config.primitiveType = PRIM_TYPE_TRIS;
|
|
config.depthMode = ZBUFFER_TYPE_NONE;
|
|
config.blendMode = ALPHA_BLEND_NONE;
|
|
config.cullEnabled = false;
|
|
config.depthWriteEnabled = false;
|
|
config.renderPass = m_fogRenderPass;
|
|
|
|
vertex_layout emptyLayout;
|
|
vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout);
|
|
if (!pipeline) {
|
|
return;
|
|
}
|
|
|
|
vk::PipelineLayout pipelineLayout = pipelineMgr->getPipelineLayout();
|
|
|
|
// Begin render pass
|
|
vk::RenderPassBeginInfo rpBegin;
|
|
rpBegin.renderPass = m_fogRenderPass;
|
|
rpBegin.framebuffer = m_fogFramebuffer;
|
|
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
|
|
rpBegin.renderArea.extent = m_ctx->sceneExtent;
|
|
|
|
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
|
|
cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
|
|
|
|
vk::Viewport viewport;
|
|
viewport.x = 0.0f;
|
|
viewport.y = 0.0f;
|
|
viewport.width = static_cast<float>(m_ctx->sceneExtent.width);
|
|
viewport.height = static_cast<float>(m_ctx->sceneExtent.height);
|
|
viewport.minDepth = 0.0f;
|
|
viewport.maxDepth = 1.0f;
|
|
cmd.setViewport(0, viewport);
|
|
|
|
vk::Rect2D scissor;
|
|
scissor.offset = vk::Offset2D(0, 0);
|
|
scissor.extent = m_ctx->sceneExtent;
|
|
cmd.setScissor(0, scissor);
|
|
|
|
DescriptorWriter writer;
|
|
writer.reset(m_ctx->device, descriptorMgr->getFallbacks());
|
|
|
|
// Set 1: Material
|
|
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
|
Assert(materialSet);
|
|
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
|
{
|
|
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
|
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
|
texArrayInfos[0] = {m_ctx->linearSampler, m_gbuffer->compositeView(), vk::ImageLayout::eShaderReadOnlyOptimal};
|
|
writer.setImageArray(MaterialBinding::TextureArray, texArrayInfos);
|
|
}
|
|
writer.setImage(MaterialBinding::DepthMap, {m_ctx->linearSampler, m_sceneDepthCopy->view, vk::ImageLayout::eShaderReadOnlyOptimal});
|
|
|
|
// Set 2: PerDraw — fog UBO (from the per-frame scratch ring)
|
|
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
|
Assert(perDrawSet);
|
|
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
|
{
|
|
vk::DeviceSize slotOffset =
|
|
m_ctx->scratchRing.alloc(descriptorMgr->getCurrentFrame(), &fogData, sizeof(fogData));
|
|
writer.setBuffer(PerDrawBinding::GenericData,
|
|
{m_ctx->scratchRing.buffer(), slotOffset, m_ctx->scratchRing.slotSize()});
|
|
}
|
|
writer.flush();
|
|
|
|
// Bind descriptor sets and draw
|
|
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
|
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
|
|
|
cmd.draw(3, 1, 0, 0);
|
|
cmd.endRenderPass();
|
|
|
|
// Scene color is now in eColorAttachmentOptimal (fog render pass finalLayout)
|
|
}
|
|
|
|
void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
|
|
{
|
|
GR_DEBUG_SCOPE("Volumetric Nebulae");
|
|
TRACE_SCOPE(tracing::Volumetrics);
|
|
|
|
if (!m_fogInitialized) {
|
|
if (!initFogPass()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
auto* pipelineMgr = getPipelineManager();
|
|
auto* descriptorMgr = getDescriptorManager();
|
|
auto* bufferMgr = getBufferManager();
|
|
auto* texMgr = getTextureManager();
|
|
|
|
if (!pipelineMgr || !descriptorMgr || !bufferMgr || !texMgr) {
|
|
return;
|
|
}
|
|
|
|
const volumetric_nebula& neb = *The_mission.volumetrics;
|
|
Assertion(neb.isVolumeBitmapValid(), "Volumetric nebula was not properly initialized!");
|
|
|
|
// Get 3D texture handles. The volume textures may not be resident yet, so
|
|
// upload them on demand (same pattern as the main texture binds in VulkanDraw).
|
|
int volHandle = neb.getVolumeBitmapHandle();
|
|
auto* volSlot = texMgr->getTextureSlot(volHandle);
|
|
if (!volSlot || !volSlot->imageView) {
|
|
bitmap* bmp = bm_lock(volHandle, 32, BMP_TEX_XPARENT);
|
|
if (bmp) {
|
|
texMgr->bm_data(volHandle, bmp, bm_is_compressed(volHandle));
|
|
bm_unlock(volHandle);
|
|
volSlot = texMgr->getTextureSlot(volHandle);
|
|
}
|
|
}
|
|
if (!volSlot || !volSlot->imageView) {
|
|
nprintf(("vulkan", "VulkanFog::renderVolumetric: Volume texture not available\n"));
|
|
return;
|
|
}
|
|
|
|
bool noiseActive = neb.getNoiseActive();
|
|
tcache_slot_vulkan* noiseSlot = nullptr;
|
|
if (noiseActive) {
|
|
int noiseHandle = neb.getNoiseVolumeBitmapHandle();
|
|
noiseSlot = texMgr->getTextureSlot(noiseHandle);
|
|
if (!noiseSlot || !noiseSlot->imageView) {
|
|
bitmap* bmp = bm_lock(noiseHandle, 32, BMP_TEX_XPARENT);
|
|
if (bmp) {
|
|
texMgr->bm_data(noiseHandle, bmp, bm_is_compressed(noiseHandle));
|
|
bm_unlock(noiseHandle);
|
|
noiseSlot = texMgr->getTextureSlot(noiseHandle);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare mipmapped emissive copy for LOD sampling
|
|
if (!m_emissiveMipmappedInitialized) {
|
|
m_emissiveMipLevels = 1;
|
|
uint32_t dim = std::max(m_ctx->sceneExtent.width, m_ctx->sceneExtent.height);
|
|
while (dim > 1) {
|
|
dim >>= 1;
|
|
m_emissiveMipLevels++;
|
|
}
|
|
|
|
vk::ImageCreateInfo imgInfo;
|
|
imgInfo.imageType = vk::ImageType::e2D;
|
|
imgInfo.format = HDR_COLOR_FORMAT;
|
|
imgInfo.extent = vk::Extent3D(m_ctx->sceneExtent.width, m_ctx->sceneExtent.height, 1);
|
|
imgInfo.mipLevels = m_emissiveMipLevels;
|
|
imgInfo.arrayLayers = 1;
|
|
imgInfo.samples = vk::SampleCountFlagBits::e1;
|
|
imgInfo.tiling = vk::ImageTiling::eOptimal;
|
|
imgInfo.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst
|
|
| vk::ImageUsageFlagBits::eSampled;
|
|
imgInfo.sharingMode = vk::SharingMode::eExclusive;
|
|
imgInfo.initialLayout = vk::ImageLayout::eUndefined;
|
|
|
|
try {
|
|
m_emissiveMipmapped.image = m_ctx->device.createImage(imgInfo);
|
|
} catch (const vk::SystemError& e) {
|
|
nprintf(("vulkan", "VulkanPostProcessor: Failed to create mipmapped emissive: %s\n", e.what()));
|
|
return;
|
|
}
|
|
|
|
Verification(m_ctx->memoryManager->allocateImageMemory(m_emissiveMipmapped.image, MemoryUsage::GpuOnly,
|
|
m_emissiveMipmapped.allocation, MemoryPurpose::RenderTarget),
|
|
"Failed to allocate memory for mipmapped emissive image");
|
|
|
|
// Create full-mip-chain view for LOD sampling
|
|
vk::ImageViewCreateInfo viewInfo;
|
|
viewInfo.image = m_emissiveMipmapped.image;
|
|
viewInfo.viewType = vk::ImageViewType::e2D;
|
|
viewInfo.format = HDR_COLOR_FORMAT;
|
|
viewInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
|
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
|
viewInfo.subresourceRange.levelCount = m_emissiveMipLevels;
|
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
|
viewInfo.subresourceRange.layerCount = 1;
|
|
|
|
try {
|
|
m_emissiveMipmappedFullView = m_ctx->device.createImageView(viewInfo);
|
|
} catch (const vk::SystemError& e) {
|
|
nprintf(("vulkan", "VulkanPostProcessor: Failed to create mipmapped emissive view: %s\n", e.what()));
|
|
return;
|
|
}
|
|
|
|
m_emissiveMipmapped.format = HDR_COLOR_FORMAT;
|
|
m_emissiveMipmapped.width = m_ctx->sceneExtent.width;
|
|
m_emissiveMipmapped.height = m_ctx->sceneExtent.height;
|
|
m_emissiveMipmappedInitialized = true;
|
|
}
|
|
|
|
// Copy G-buffer emissive (mip 0) to mipmapped emissive, then generate mips.
|
|
// dstMipLevels transitions ALL mip levels to eTransferDstOptimal in the pre-barrier.
|
|
// Skip dst post-barrier (stays in eTransferDstOptimal for generateMipmaps).
|
|
copyImageToImage(cmd,
|
|
m_gbuffer->emissiveImage(), vk::ImageLayout::eShaderReadOnlyOptimal, vk::ImageLayout::eShaderReadOnlyOptimal,
|
|
m_emissiveMipmapped.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal,
|
|
m_ctx->sceneExtent,
|
|
vk::ImageAspectFlagBits::eColor,
|
|
m_emissiveMipLevels);
|
|
|
|
// Generate mipmaps via blit chain (expects dst in eTransferDstOptimal).
|
|
// After return, all mips are in eShaderReadOnlyOptimal.
|
|
PostProcessContext::generateMipmaps(cmd, m_emissiveMipmapped.image, m_ctx->sceneExtent.width, m_ctx->sceneExtent.height, m_emissiveMipLevels);
|
|
|
|
// Copy scene depth (if not already done by renderSceneFog)
|
|
// copySceneDepth is safe to call multiple times — but it re-transitions the depth buffer.
|
|
// The fog pass already called it if scene fog ran. For standalone volumetric, we need it.
|
|
copySceneDepth(cmd);
|
|
|
|
// Transition scene color → eColorAttachmentOptimal for the fog render pass.
|
|
// oldLayout=eUndefined is safe: render pass has loadOp=eDontCare (overwrites every pixel).
|
|
// Scene color may be in eShaderReadOnlyOptimal (volumetric-only) or
|
|
// eColorAttachmentOptimal (after scene fog + copySceneColorToComposite).
|
|
{
|
|
ImageBarrier2 barrier;
|
|
barrier.image = m_sceneColor->image;
|
|
barrier.levelCount = 1;
|
|
barrier.layerCount = 1;
|
|
barrier.oldLayout = vk::ImageLayout::eUndefined;
|
|
barrier.newLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
|
barrier.srcAccess = {};
|
|
barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput;
|
|
barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
|
|
|
|
cmdImageBarrier(cmd, barrier);
|
|
}
|
|
|
|
// volumetric_fog_data is the largest fullscreen-pass UBO; it dictates the
|
|
// scratch ring's slot size. The static_assert below guards against silent
|
|
// overflow (memcpy spilling into the neighboring slot, with the shader's
|
|
// bound range ending before the aspect/fov/noise fields) if the struct
|
|
// ever grows past the slot size.
|
|
static_assert(sizeof(graphics::generic_data::volumetric_fog_data) <= PostProcessContext::SCRATCH_UBO_SLOT_SIZE,
|
|
"volumetric_fog_data no longer fits a scratch UBO slot - grow SCRATCH_UBO_SLOT_SIZE");
|
|
|
|
// Fill volumetric fog UBO
|
|
graphics::generic_data::volumetric_fog_data volData;
|
|
{
|
|
// The projection/view matrices are already set at this point; setting them
|
|
// again here trips an assert, so just read the current matrices.
|
|
vm_inverse_matrix4(&volData.p_inv, &gr_projection_matrix);
|
|
vm_inverse_matrix4(&volData.v_inv, &gr_view_matrix);
|
|
|
|
volData.zNear = Min_draw_distance;
|
|
volData.zFar = Max_draw_distance;
|
|
volData.cameraPos = Eye_position;
|
|
|
|
// Find first directional light for global light direction/color
|
|
vec3d global_light_dir = ZERO_VECTOR;
|
|
vec3d global_light_diffuse = ZERO_VECTOR;
|
|
for (const auto& l : Lights) {
|
|
if (l.type == Light_Type::Directional) {
|
|
global_light_dir = l.vec;
|
|
global_light_diffuse.xyz.x = l.r * l.intensity;
|
|
global_light_diffuse.xyz.y = l.g * l.intensity;
|
|
global_light_diffuse.xyz.z = l.b * l.intensity;
|
|
break;
|
|
}
|
|
}
|
|
|
|
volData.globalLightDirection = global_light_dir;
|
|
volData.globalLightDiffuse = global_light_diffuse;
|
|
volData.nebPos = neb.getPos();
|
|
volData.nebSize = neb.getSize();
|
|
volData.stepsize = neb.getStepsize();
|
|
volData.opacitydistance = neb.getOpacityDistance();
|
|
volData.alphalimit = neb.getAlphaLim();
|
|
auto nebColor = neb.getNebulaColor();
|
|
volData.nebColor[0] = std::get<0>(nebColor);
|
|
volData.nebColor[1] = std::get<1>(nebColor);
|
|
volData.nebColor[2] = std::get<2>(nebColor);
|
|
volData.udfScale = neb.getUDFScale();
|
|
volData.emissiveSpreadFactor = neb.getEmissiveSpread();
|
|
volData.emissiveIntensity = neb.getEmissiveIntensity();
|
|
volData.emissiveFalloff = neb.getEmissiveFalloff();
|
|
volData.henyeyGreensteinCoeff = neb.getHenyeyGreensteinCoeff();
|
|
volData.directionalLightSampleSteps = neb.getGlobalLightSteps();
|
|
volData.directionalLightStepSize = neb.getGlobalLightStepsize();
|
|
auto noiseColor = neb.getNoiseColor();
|
|
volData.noiseColor[0] = std::get<0>(noiseColor);
|
|
volData.noiseColor[1] = std::get<1>(noiseColor);
|
|
volData.noiseColor[2] = std::get<2>(noiseColor);
|
|
auto noiseScale = neb.getNoiseColorScale();
|
|
volData.noiseColorScale1 = std::get<0>(noiseScale);
|
|
volData.noiseColorScale2 = std::get<1>(noiseScale);
|
|
volData.noiseColorIntensity = neb.getNoiseColorIntensity();
|
|
volData.aspect = gr_screen.clip_aspect;
|
|
volData.fov = g3_get_hfov(Proj_fov);
|
|
}
|
|
|
|
// Compute shader flags for volumetric fog variants
|
|
unsigned int volFogFlags = 0;
|
|
if (neb.getEdgeSmoothing()) {
|
|
volFogFlags |= SDR_FLAG_VOLUMETRICS_DO_EDGE_SMOOTHING;
|
|
}
|
|
if (noiseActive) {
|
|
volFogFlags |= SDR_FLAG_VOLUMETRICS_NOISE;
|
|
}
|
|
|
|
// Hand-rolled fullscreen pass rather than PostProcessContext::drawFullscreenTriangle:
|
|
// the volumetric shader binds five Material-set textures the shared helper
|
|
// can't express — DepthMap (binding 4), a sampler3D volume texture (binding 5,
|
|
// reusing the SceneColor slot) and a sampler3D noise texture (binding 6, reusing
|
|
// DistortionMap), whose fallbacks are 3D not 2D. drawFullscreenTriangle only writes
|
|
// TextureArray[0]. The per-frame scratch-ring UBO plumbing is still shared; only the
|
|
// custom Material-set binds stay local.
|
|
|
|
PipelineConfig config;
|
|
config.shaderType = SDR_TYPE_VOLUMETRIC_FOG;
|
|
config.shaderFlags = volFogFlags;
|
|
config.vertexLayoutHash = 0;
|
|
config.primitiveType = PRIM_TYPE_TRIS;
|
|
config.depthMode = ZBUFFER_TYPE_NONE;
|
|
config.blendMode = ALPHA_BLEND_NONE;
|
|
config.cullEnabled = false;
|
|
config.depthWriteEnabled = false;
|
|
config.renderPass = m_fogRenderPass;
|
|
|
|
vertex_layout emptyLayout;
|
|
vk::Pipeline pipeline = pipelineMgr->getPipeline(config, emptyLayout);
|
|
if (!pipeline) {
|
|
return;
|
|
}
|
|
|
|
vk::PipelineLayout pipelineLayout = pipelineMgr->getPipelineLayout();
|
|
|
|
// Begin render pass
|
|
vk::RenderPassBeginInfo rpBegin;
|
|
rpBegin.renderPass = m_fogRenderPass;
|
|
rpBegin.framebuffer = m_fogFramebuffer;
|
|
rpBegin.renderArea.offset = vk::Offset2D(0, 0);
|
|
rpBegin.renderArea.extent = m_ctx->sceneExtent;
|
|
|
|
cmd.beginRenderPass(rpBegin, vk::SubpassContents::eInline);
|
|
cmd.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
|
|
|
|
vk::Viewport viewport;
|
|
viewport.x = 0.0f;
|
|
viewport.y = 0.0f;
|
|
viewport.width = static_cast<float>(m_ctx->sceneExtent.width);
|
|
viewport.height = static_cast<float>(m_ctx->sceneExtent.height);
|
|
viewport.minDepth = 0.0f;
|
|
viewport.maxDepth = 1.0f;
|
|
cmd.setViewport(0, viewport);
|
|
|
|
vk::Rect2D scissor;
|
|
scissor.offset = vk::Offset2D(0, 0);
|
|
scissor.extent = m_ctx->sceneExtent;
|
|
cmd.setScissor(0, scissor);
|
|
|
|
DescriptorWriter writer;
|
|
writer.reset(m_ctx->device, descriptorMgr->getFallbacks());
|
|
|
|
// Set 1: Material
|
|
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
|
|
Assert(materialSet);
|
|
writer.writeSet(DescriptorSetIndex::Material, materialSet);
|
|
{
|
|
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
|
|
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
|
|
texArrayInfos[0] = {m_ctx->linearSampler, m_gbuffer->compositeView(), vk::ImageLayout::eShaderReadOnlyOptimal};
|
|
texArrayInfos[1] = {m_ctx->mipmapSampler, m_emissiveMipmappedFullView, vk::ImageLayout::eShaderReadOnlyOptimal};
|
|
writer.setImageArray(MaterialBinding::TextureArray, texArrayInfos);
|
|
}
|
|
writer.setImage(MaterialBinding::DepthMap, {m_ctx->linearSampler, m_sceneDepthCopy->view, vk::ImageLayout::eShaderReadOnlyOptimal});
|
|
// Binding 5: 3D volume texture (reuses SceneColor slot)
|
|
writer.setImage(MaterialBinding::SceneColor, {m_ctx->linearSampler, volSlot->imageView, vk::ImageLayout::eShaderReadOnlyOptimal});
|
|
// Binding 6: 3D noise texture (or fallback 3D)
|
|
{
|
|
auto noiseInfo = descriptorMgr->getFallbacks().texture3D;
|
|
noiseInfo.sampler = m_ctx->linearSampler;
|
|
if (noiseSlot && noiseSlot->imageView) {
|
|
noiseInfo.imageView = noiseSlot->imageView;
|
|
}
|
|
writer.setImage(MaterialBinding::DistortionMap, noiseInfo);
|
|
}
|
|
|
|
// Set 2: PerDraw — volumetric fog UBO (from the per-frame scratch ring)
|
|
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
|
|
Assert(perDrawSet);
|
|
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
|
|
{
|
|
vk::DeviceSize slotOffset =
|
|
m_ctx->scratchRing.alloc(descriptorMgr->getCurrentFrame(), &volData, sizeof(volData));
|
|
writer.setBuffer(PerDrawBinding::GenericData,
|
|
{m_ctx->scratchRing.buffer(), slotOffset, m_ctx->scratchRing.slotSize()});
|
|
}
|
|
writer.flush();
|
|
|
|
// Bind descriptor sets and draw
|
|
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
|
|
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
|
|
|
|
cmd.draw(3, 1, 0, 0);
|
|
cmd.endRenderPass();
|
|
|
|
// Scene color is now in eColorAttachmentOptimal (fog render pass finalLayout)
|
|
}
|
|
|
|
} // namespace graphics::vulkan
|