From 64528f2c4a59a787b07a299723164582e2756a4e Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Mon, 1 Dec 2025 19:35:38 +0900 Subject: [PATCH] ADD: SSR --- Readme.md | 4 +- shaders/ssr.frag | 150 +++++++++++++++++++++++ src/CMakeLists.txt | 2 + src/core/assets/ibl_manager.cpp | 24 +++- src/core/context.h | 1 + src/core/engine.cpp | 39 +++++- src/core/engine.h | 1 + src/render/passes/ssr.cpp | 208 ++++++++++++++++++++++++++++++++ src/render/passes/ssr.h | 49 ++++++++ src/render/renderpass.cpp | 6 + 10 files changed, 479 insertions(+), 5 deletions(-) create mode 100644 shaders/ssr.frag create mode 100644 src/render/passes/ssr.cpp create mode 100644 src/render/passes/ssr.h diff --git a/Readme.md b/Readme.md index 401d179..e8c9f1a 100644 --- a/Readme.md +++ b/Readme.md @@ -11,10 +11,10 @@ Current structure: - Supports texture compression(BCn, non glTF standard), LRU reload - Object clicking, generation. - Multi light system +- SSR Work-In-Progress -- [ ] TAA -- [ ] SSR +- [ ] AA - [ ] bloom - [ ] Planet Rendering diff --git a/shaders/ssr.frag b/shaders/ssr.frag new file mode 100644 index 0000000..743de35 --- /dev/null +++ b/shaders/ssr.frag @@ -0,0 +1,150 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require + +#include "input_structures.glsl" + +layout(location = 0) in vec2 inUV; +layout(location = 0) out vec4 outColor; + +// Set 0: scene data (see input_structures.glsl) + +// Set 1: SSR inputs +layout(set = 1, binding = 0) uniform sampler2D hdrColor; +layout(set = 1, binding = 1) uniform sampler2D posTex; +layout(set = 1, binding = 2) uniform sampler2D normalTex; +layout(set = 1, binding = 3) uniform sampler2D albedoTex; + +vec3 getCameraWorldPosition() +{ + mat4 invView = inverse(sceneData.view); + return vec3(invView[3]); +} + +vec3 projectToScreen(vec3 worldPos) +{ + vec4 clip = sceneData.viewproj * vec4(worldPos, 1.0); + if (clip.w <= 0.0) + { + return vec3(0.0, 0.0, -1.0); + } + vec3 ndc = clip.xyz / clip.w; + if (ndc.x < -1.0 || ndc.x > 1.0 || + ndc.y < -1.0 || ndc.y > 1.0 || + ndc.z < 0.0 || ndc.z > 1.0) + { + return vec3(0.0, 0.0, -1.0); + } + + vec2 uv = ndc.xy * 0.5 + 0.5; + return vec3(uv, ndc.z); +} + +void main() +{ + vec3 baseColor = texture(hdrColor, inUV).rgb; + + vec4 posSample = texture(posTex, inUV); + if (posSample.w == 0.0) + { + outColor = vec4(baseColor, 1.0); + return; + } + + vec3 worldPos = posSample.xyz; + + vec4 normSample = texture(normalTex, inUV); + vec3 N = normalize(normSample.xyz); + float roughness = clamp(normSample.w, 0.04, 1.0); + + vec4 albSample = texture(albedoTex, inUV); + vec3 albedo = albSample.rgb; + float metallic = clamp(albSample.a, 0.0, 1.0); + + vec3 camPos = getCameraWorldPosition(); + vec3 V = normalize(camPos - worldPos); + vec3 R = reflect(-V, N); + + float gloss = 1.0 - roughness; + float reflectivity = gloss * mix(0.04, 1.0, metallic); + + if (reflectivity <= 0.05) + { + outColor = vec4(baseColor, 1.0); + return; + } + + if (dot(R, V) <= 0.0) + { + outColor = vec4(baseColor, 1.0); + return; + } + + const int MAX_STEPS = 64; + const float STEP_LENGTH = 0.5; // world units per step + const float MAX_DISTANCE = 50.0; // clamp ray length + const float THICKNESS = 3.0; // world-space thickness tolerance + + int maxSteps = int(mix(8.0, float(MAX_STEPS), reflectivity)); + + bool hit = false; + vec2 hitUV = vec2(0.0); + + float t = STEP_LENGTH; + for (int i = 0; i < maxSteps; ++i) + { + if (t > MAX_DISTANCE) break; + + vec3 samplePos = worldPos + R * t; + vec3 proj = projectToScreen(samplePos); + if (proj.z < 0.0) + { + break; + } + + vec2 uv = proj.xy; + vec4 scenePosSample = texture(posTex, uv); + if (scenePosSample.w == 0.0) + { + t += STEP_LENGTH; + continue; + } + + // Compare distances along view direction as a simple intersection test. + vec3 viewSample = (sceneData.view * vec4(samplePos, 1.0)).xyz; + vec3 viewScene = (sceneData.view * vec4(scenePosSample.xyz, 1.0)).xyz; + + float depthRay = -viewSample.z; + float depthScene = -viewScene.z; + + float depthDiff = depthRay - depthScene; + + if (depthRay > 0.0 && depthScene > 0.0 && + depthDiff > 0.0 && depthDiff < THICKNESS) + { + hit = true; + hitUV = uv; + break; + } + + t += STEP_LENGTH; + } + + vec3 result = baseColor; + if (hit) + { + vec3 reflColor = texture(hdrColor, hitUV).rgb; + + float NoV = clamp(dot(N, V), 0.0, 1.0); + float F0 = mix(0.04, 1.0, metallic); + float F = F0 + (1.0 - F0) * pow(1.0 - NoV, 5.0); // Schlick + float gloss = 1.0 - roughness; + + float ssrVisibility = gloss; + float weight = clamp(F * ssrVisibility, 0.0, 1.0); + + result = mix(baseColor, reflColor, weight); + } + + outColor = vec4(result, 1.0); +} + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6bbac85..3e9a3e1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -68,6 +68,8 @@ add_executable (vulkan_engine render/passes/lighting.cpp render/passes/shadow.h render/passes/shadow.cpp + render/passes/ssr.h + render/passes/ssr.cpp render/passes/transparent.h render/passes/transparent.cpp render/passes/imgui_pass.h diff --git a/src/core/assets/ibl_manager.cpp b/src/core/assets/ibl_manager.cpp index 1965139..9ee952a 100644 --- a/src/core/assets/ibl_manager.cpp +++ b/src/core/assets/ibl_manager.cpp @@ -237,7 +237,29 @@ bool IBLManager::load(const IBLPaths &paths) _diff = _spec; } - // If background is still missing but specular is valid, reuse the specular environment. + if (!paths.background2D.empty()) + { + ktxutil::Ktx2D bg{}; + if (ktxutil::load_ktx2_2d(paths.background2D.c_str(), bg)) + { + std::vector lv; + lv.reserve(bg.mipLevels); + for (uint32_t mip = 0; mip < bg.mipLevels; ++mip) + { + const auto &r = bg.copies[mip]; + lv.push_back(ResourceManager::MipLevelCopy{ + .offset = r.bufferOffset, + .length = 0, + .width = r.imageExtent.width, + .height = r.imageExtent.height, + }); + } + _background = rm->create_image_compressed( + bg.bytes.data(), bg.bytes.size(), bg.fmt, lv, + VK_IMAGE_USAGE_SAMPLED_BIT); + } + } + if (_background.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE) { _background = _spec; diff --git a/src/core/context.h b/src/core/context.h index dad4511..ee898a7 100644 --- a/src/core/context.h +++ b/src/core/context.h @@ -75,6 +75,7 @@ public: // Runtime settings visible to passes/shaders ShadowSettings shadowSettings{}; + bool enableSSR = false; // optional screen-space reflections toggle // Ray tracing manager (optional, nullptr if unsupported) RayTracingManager* ray = nullptr; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index d340aa3..7b3519d 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -268,6 +268,8 @@ void VulkanEngine::init() _resourceManager->set_deferred_uploads(true); + _context->enableSSR = true; + //everything went fine _isInitialized = true; } @@ -716,16 +718,49 @@ void VulkanEngine::draw() lighting->register_graph(_renderGraph.get(), hDraw, hGBufferPosition, hGBufferNormal, hGBufferAlbedo, std::span(hShadowCascades.data(), hShadowCascades.size())); } + + // Optional Screen Space Reflections pass: consumes HDR draw + G-Buffer and + // produces an SSR-augmented HDR image. Controlled by EngineContext::enableSSR. + RGImageHandle hSSR{}; + SSRPass *ssr = _renderPassManager->getPass(); + const bool ssrEnabled = (_context && _context->enableSSR && ssr != nullptr); + if (ssrEnabled) + { + RGImageDesc ssrDesc{}; + ssrDesc.name = "hdr.ssr"; + ssrDesc.format = _swapchainManager->drawImage().imageFormat; + ssrDesc.extent = _drawExtent; + ssrDesc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT + | VK_IMAGE_USAGE_SAMPLED_BIT + | VK_IMAGE_USAGE_STORAGE_BIT; + hSSR = _renderGraph->create_image(ssrDesc); + + ssr->register_graph(_renderGraph.get(), + hDraw, + hGBufferPosition, + hGBufferNormal, + hGBufferAlbedo, + hSSR); + } + if (auto *transparent = _renderPassManager->getPass()) { - transparent->register_graph(_renderGraph.get(), hDraw, hDepth); + // Transparent objects draw on top of either the SSR output or the raw HDR draw. + RGImageHandle hdrTarget = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw; + transparent->register_graph(_renderGraph.get(), hdrTarget, hDepth); } imguiPass = _renderPassManager->getImGuiPass(); // Optional Tonemap pass: sample HDR draw -> LDR intermediate if (auto *tonemap = _renderPassManager->getPass()) { - finalColor = tonemap->register_graph(_renderGraph.get(), hDraw); + RGImageHandle hdrInput = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw; + finalColor = tonemap->register_graph(_renderGraph.get(), hdrInput); + } + else + { + // If tonemapping is disabled, present whichever HDR buffer we ended up with. + finalColor = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw; } } diff --git a/src/core/engine.h b/src/core/engine.h index d27f0a9..4539ac7 100644 --- a/src/core/engine.h +++ b/src/core/engine.h @@ -26,6 +26,7 @@ #include "frame/resources.h" #include "descriptor/manager.h" #include "pipeline/sampler.h" +#include "render/passes/ssr.h" #include "core/context.h" #include "core/pipeline/manager.h" #include "core/assets/manager.h" diff --git a/src/render/passes/ssr.cpp b/src/render/passes/ssr.cpp new file mode 100644 index 0000000..ebcb6b2 --- /dev/null +++ b/src/render/passes/ssr.cpp @@ -0,0 +1,208 @@ +#include "ssr.h" + +#include "core/frame/resources.h" +#include "core/descriptor/manager.h" +#include "core/descriptor/descriptors.h" +#include "core/device/device.h" +#include "core/device/resource.h" +#include "core/device/swapchain.h" +#include "core/context.h" +#include "core/pipeline/manager.h" +#include "core/assets/manager.h" +#include "core/pipeline/sampler.h" + +#include "render/graph/graph.h" +#include "render/pipelines.h" + +void SSRPass::init(EngineContext *context) +{ + _context = context; + if (!_context || !_context->getDevice() || !_context->getDescriptorLayouts() || !_context->pipelines) + { + return; + } + + VkDevice device = _context->getDevice()->device(); + + // Set 1 layout: HDR + G-Buffer inputs (all sampled images). + { + DescriptorLayoutBuilder builder; + builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // hdrColor + builder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // posTex + builder.add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // normalTex + builder.add_binding(3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); // albedoTex + _inputSetLayout = builder.build( + device, + VK_SHADER_STAGE_FRAGMENT_BIT, + nullptr, + VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + } + + // Graphics pipeline: fullscreen triangle, no depth, HDR color attachment. + GraphicsPipelineCreateInfo info{}; + info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); + info.fragmentShaderPath = _context->getAssets()->shaderPath("ssr.frag.spv"); + info.setLayouts = { + _context->getDescriptorLayouts()->gpuSceneDataLayout(), // set = 0 (sceneData UBO) + _inputSetLayout // set = 1 (HDR + GBuffer) + }; + + info.configure = [this](PipelineBuilder &b) + { + b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + b.set_polygon_mode(VK_POLYGON_MODE_FILL); + b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE); + b.set_multisampling_none(); + b.disable_depthtest(); + b.disable_blending(); + if (_context && _context->getSwapchain()) + { + b.set_color_attachment_format(_context->getSwapchain()->drawImage().imageFormat); + } + }; + + _context->pipelines->createGraphicsPipeline("ssr", info); +} + +void SSRPass::cleanup() +{ + if (_context && _context->getDevice() && _inputSetLayout) + { + vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _inputSetLayout, nullptr); + _inputSetLayout = VK_NULL_HANDLE; + } + + _deletionQueue.flush(); +} + +void SSRPass::execute(VkCommandBuffer) +{ + // Executed via render graph; nothing to do here. +} + +void SSRPass::register_graph(RenderGraph *graph, + RGImageHandle hdrInput, + RGImageHandle gbufPos, + RGImageHandle gbufNorm, + RGImageHandle gbufAlbedo, + RGImageHandle hdrOutput) +{ + if (!graph || !hdrInput.valid() || !hdrOutput.valid()) + { + return; + } + + graph->add_pass( + "SSR", + RGPassType::Graphics, + [hdrInput, gbufPos, gbufNorm, gbufAlbedo, hdrOutput](RGPassBuilder &builder, EngineContext *) + { + // Read current HDR lighting + G-Buffer; write to an HDR output. + builder.read(hdrInput, RGImageUsage::SampledFragment); + if (gbufPos.valid()) + { + builder.read(gbufPos, RGImageUsage::SampledFragment); + } + if (gbufNorm.valid()) + { + builder.read(gbufNorm, RGImageUsage::SampledFragment); + } + if (gbufAlbedo.valid()) + { + builder.read(gbufAlbedo, RGImageUsage::SampledFragment); + } + builder.write_color(hdrOutput, false /*load existing contents*/); + }, + [this, hdrInput, gbufPos, gbufNorm, gbufAlbedo](VkCommandBuffer cmd, + const RGPassResources &res, + EngineContext *ctx) + { + draw_ssr(cmd, ctx, res, hdrInput, gbufPos, gbufNorm, gbufAlbedo); + }); +} + +void SSRPass::draw_ssr(VkCommandBuffer cmd, + EngineContext *context, + const RGPassResources &resources, + RGImageHandle hdrInput, + RGImageHandle gbufPos, + RGImageHandle gbufNorm, + RGImageHandle gbufAlbedo) +{ + EngineContext *ctxLocal = context ? context : _context; + if (!ctxLocal || !ctxLocal->currentFrame) return; + + ResourceManager *resourceManager = ctxLocal->getResources(); + DeviceManager *deviceManager = ctxLocal->getDevice(); + DescriptorManager *descriptorLayouts = ctxLocal->getDescriptorLayouts(); + PipelineManager *pipelineManager = ctxLocal->pipelines; + if (!resourceManager || !deviceManager || !descriptorLayouts || !pipelineManager) return; + + VkImageView hdrView = resources.image_view(hdrInput); + VkImageView posView = resources.image_view(gbufPos); + VkImageView normView = resources.image_view(gbufNorm); + VkImageView albedoView = resources.image_view(gbufAlbedo); + if (hdrView == VK_NULL_HANDLE || posView == VK_NULL_HANDLE || + normView == VK_NULL_HANDLE || albedoView == VK_NULL_HANDLE) + { + return; + } + + // Fetch (or refresh) pipeline for hot-reload support. + if (!pipelineManager->getGraphics("ssr", _pipeline, _pipelineLayout)) + { + return; + } + + // Scene UBO (set=0, binding=0) – mirror LightingPass behavior. + AllocatedBuffer sceneBuf = resourceManager->create_buffer( + sizeof(GPUSceneData), + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, sceneBuf]() + { + resourceManager->destroy_buffer(sceneBuf); + }); + + VmaAllocationInfo allocInfo{}; + vmaGetAllocationInfo(deviceManager->allocator(), sceneBuf.allocation, &allocInfo); + auto *sceneUniformData = static_cast(allocInfo.pMappedData); + *sceneUniformData = ctxLocal->getSceneData(); + vmaFlushAllocation(deviceManager->allocator(), sceneBuf.allocation, 0, sizeof(GPUSceneData)); + + VkDescriptorSet globalSet = ctxLocal->currentFrame->_frameDescriptors.allocate( + deviceManager->device(), descriptorLayouts->gpuSceneDataLayout()); + { + DescriptorWriter writer; + writer.write_buffer(0, sceneBuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + writer.update_set(deviceManager->device(), globalSet); + } + + // Input set (set=1): HDR color + G-Buffer textures. + VkDescriptorSet inputSet = ctxLocal->currentFrame->_frameDescriptors.allocate( + deviceManager->device(), _inputSetLayout); + { + DescriptorWriter writer; + writer.write_image(0, hdrView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.write_image(1, posView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.write_image(2, normView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.write_image(3, albedoView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.update_set(deviceManager->device(), inputSet); + } + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 0, 1, &globalSet, 0, nullptr); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 1, 1, &inputSet, 0, nullptr); + + VkExtent2D extent = ctxLocal->getDrawExtent(); + VkViewport vp{0.f, 0.f, (float)extent.width, (float)extent.height, 0.f, 1.f}; + VkRect2D sc{{0, 0}, extent}; + vkCmdSetViewport(cmd, 0, 1, &vp); + vkCmdSetScissor(cmd, 0, 1, &sc); + + vkCmdDraw(cmd, 3, 1, 0, 0); +} diff --git a/src/render/passes/ssr.h b/src/render/passes/ssr.h new file mode 100644 index 0000000..a7a4c94 --- /dev/null +++ b/src/render/passes/ssr.h @@ -0,0 +1,49 @@ +#pragma once + +#include "render/renderpass.h" +#include "render/graph/types.h" + +class RenderGraph; +class RGPassResources; + +// Screen Space Reflections (SSR) pass. +// In v1 this is a lightweight stub wired into the RenderGraph by the engine. +// The full pipeline and shader implementation is added in later plan steps. +class SSRPass : public IRenderPass +{ +public: + void init(EngineContext *context) override; + void cleanup() override; + void execute(VkCommandBuffer cmd) override; + + const char *getName() const override { return "SSR"; } + + // Register SSR in the render graph. + // hdrInput : HDR color buffer produced by deferred lighting. + // gbufPos : G-Buffer world-space position (RGBA32F). + // gbufNorm : G-Buffer world-space normal + roughness. + // gbufAlbedo : G-Buffer albedo + metallic. + // hdrOutput : HDR color buffer that will carry lighting + SSR. + void register_graph(RenderGraph *graph, + RGImageHandle hdrInput, + RGImageHandle gbufPos, + RGImageHandle gbufNorm, + RGImageHandle gbufAlbedo, + RGImageHandle hdrOutput); + +private: + EngineContext *_context = nullptr; + VkDescriptorSetLayout _inputSetLayout = VK_NULL_HANDLE; // set=1: HDR + GBuffer inputs + VkPipeline _pipeline = VK_NULL_HANDLE; + VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE; + + void draw_ssr(VkCommandBuffer cmd, + EngineContext *context, + const class RGPassResources &resources, + RGImageHandle hdrInput, + RGImageHandle gbufPos, + RGImageHandle gbufNorm, + RGImageHandle gbufAlbedo); + + DeletionQueue _deletionQueue; +}; diff --git a/src/render/renderpass.cpp b/src/render/renderpass.cpp index 40bbe5c..7aeaf58 100644 --- a/src/render/renderpass.cpp +++ b/src/render/renderpass.cpp @@ -4,6 +4,7 @@ #include "passes/geometry.h" #include "passes/imgui_pass.h" #include "passes/lighting.h" +#include "passes/ssr.h" #include "passes/transparent.h" #include "passes/tonemap.h" #include "passes/shadow.h" @@ -29,6 +30,11 @@ void RenderPassManager::init(EngineContext *context) lightingPass->init(context); addPass(std::move(lightingPass)); + // Screen Space Reflections pass (wired between lighting and transparent) + auto ssrPass = std::make_unique(); + ssrPass->init(context); + addPass(std::move(ssrPass)); + auto transparentPass = std::make_unique(); transparentPass->init(context); addPass(std::move(transparentPass));