From d5ae159f73c22e3ea472ea7673ab1b847feb6ce8 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 2 Dec 2025 20:12:48 +0900 Subject: [PATCH] ADD: SSR with RT --- Readme.md | 2 +- shaders/ssr.frag | 73 ++++++------- shaders/ssr_rt.frag | 209 ++++++++++++++++++++++++++++++++++++++ src/core/context.h | 3 + src/core/engine.cpp | 8 +- src/core/engine_ui.cpp | 28 +++++ src/render/passes/ssr.cpp | 45 ++++++-- src/scene/vk_scene.cpp | 5 +- 8 files changed, 314 insertions(+), 59 deletions(-) create mode 100644 shaders/ssr_rt.frag diff --git a/Readme.md b/Readme.md index e8c9f1a..ae2e6d0 100644 --- a/Readme.md +++ b/Readme.md @@ -1,5 +1,5 @@ # CopernicusEngine -Multi-purpose Vulkan render engine specialized for physics simulation and solar system visualization +Multipurpose Vulkan render engine specialized for physics simulation and solar system visualization ## Introduction Work-In-Progress Vulkan render engine diff --git a/shaders/ssr.frag b/shaders/ssr.frag index 743de35..6b556fd 100644 --- a/shaders/ssr.frag +++ b/shaders/ssr.frag @@ -6,8 +6,6 @@ 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; @@ -16,21 +14,25 @@ layout(set = 1, binding = 3) uniform sampler2D albedoTex; vec3 getCameraWorldPosition() { - mat4 invView = inverse(sceneData.view); - return vec3(invView[3]); + mat3 rotT = mat3(sceneData.view); // R^T + mat3 rot = transpose(rotT); // R + vec3 T = sceneData.view[3].xyz; // -R^T * C + return -rot * T; // C = -R * T } 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; + return vec3(0.0, 0.0, -1.0); + + float invW = 1.0 / clip.w; + vec3 ndc = clip.xyz * invW; + 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) + ndc.y < -1.0 || ndc.y > 1.0 || + ndc.z < 0.0 || ndc.z > 1.0) { return vec3(0.0, 0.0, -1.0); } @@ -53,27 +55,21 @@ void main() vec3 worldPos = posSample.xyz; vec4 normSample = texture(normalTex, inUV); - vec3 N = normalize(normSample.xyz); + 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); + vec4 albSample = texture(albedoTex, inUV); + float metallic = clamp(albSample.a, 0.0, 1.0); vec3 camPos = getCameraWorldPosition(); - vec3 V = normalize(camPos - worldPos); - vec3 R = reflect(-V, N); + vec3 V = normalize(camPos - worldPos); + vec3 R = reflect(-V, N); - float gloss = 1.0 - roughness; - float reflectivity = gloss * mix(0.04, 1.0, metallic); + float gloss = 1.0 - roughness; + float F0 = mix(0.04, 1.0, metallic); + float reflectivity = gloss * F0; - if (reflectivity <= 0.05) - { - outColor = vec4(baseColor, 1.0); - return; - } - - if (dot(R, V) <= 0.0) + if (reflectivity <= 0.05 || dot(R, V) <= 0.0) { outColor = vec4(baseColor, 1.0); return; @@ -84,17 +80,15 @@ void main() 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); + 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) + for (int i = 0; i < maxSteps && t <= MAX_DISTANCE; ++i, t += STEP_LENGTH) { - if (t > MAX_DISTANCE) break; - vec3 samplePos = worldPos + R * t; + vec3 proj = projectToScreen(samplePos); if (proj.z < 0.0) { @@ -105,28 +99,23 @@ void main() 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; + float depthDiff = depthRay - depthScene; if (depthRay > 0.0 && depthScene > 0.0 && depthDiff > 0.0 && depthDiff < THICKNESS) { - hit = true; + hit = true; hitUV = uv; break; } - - t += STEP_LENGTH; } vec3 result = baseColor; @@ -135,16 +124,12 @@ void main() 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); + float weight = clamp(F * ssrVisibility, 0.0, 1.0); result = mix(baseColor, reflColor, weight); } outColor = vec4(result, 1.0); } - diff --git a/shaders/ssr_rt.frag b/shaders/ssr_rt.frag new file mode 100644 index 0000000..5386781 --- /dev/null +++ b/shaders/ssr_rt.frag @@ -0,0 +1,209 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_ray_query : require + +#include "input_structures.glsl" + +layout(location = 0) in vec2 inUV; +layout(location = 0) out vec4 outColor; + +// Set 0: SceneData UBO (binding=0, declared in input_structures.glsl) +// + optional TLAS for ray queries (binding=1). +layout(set = 0, binding = 1) uniform accelerationStructureEXT topLevelAS; + +// 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() +{ + mat3 rotT = mat3(sceneData.view); // R^T + mat3 rot = transpose(rotT); // R + vec3 T = sceneData.view[3].xyz; // -R^T * C + return -rot * T; // C = -R * T +} + +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); + } + + float invW = 1.0 / clip.w; + vec3 ndc = clip.xyz * invW; + + 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); + 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 F0 = mix(0.04, 1.0, metallic); + float reflectivity = gloss * F0; + + if (reflectivity <= 0.05 || dot(R, V) <= 0.0) + { + outColor = vec4(baseColor, 1.0); + return; + } + + // Reflection mode (encoded in rtOptions.w): + // 0 = SSR only, 1 = SSR + RT fallback, 2 = RT only + uint reflMode = sceneData.rtOptions.w; + bool useSSR = (reflMode == 0u || reflMode == 1u); + bool useRT = (reflMode >= 1u); // hybrid or RT-only + + vec3 result = baseColor; + + // ------------------------------------------------------------------------- + // 1) Screen-space reflections (SSR) via depth ray-march (optional). + // ------------------------------------------------------------------------- + bool ssrHit = false; + vec2 ssrUV = vec2(0.0); + + if (useSSR) + { + const int MAX_STEPS_SSR = 64; + const float STEP_LENGTH_SSR = 0.5; // world units per step + const float MAX_DISTANCE_SSR = 50.0; // clamp ray length + const float THICKNESS_SSR = 3.0; // world-space thickness tolerance + + int maxSteps = int(mix(8.0, float(MAX_STEPS_SSR), reflectivity)); + float t = STEP_LENGTH_SSR; + for (int i = 0; i < maxSteps && t <= MAX_DISTANCE_SSR; ++i, t += STEP_LENGTH_SSR) + { + 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) + { + continue; + } + + 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_SSR) + { + ssrHit = true; + ssrUV = uv; + break; + } + } + } + + // If SSR hits and we are not in RT-only mode, use it as the primary reflection. + if (ssrHit && reflMode != 2u) + { + vec3 reflColor = texture(hdrColor, ssrUV).rgb; + + float NoV = clamp(dot(N, V), 0.0, 1.0); + float F = F0 + (1.0 - F0) * pow(1.0 - NoV, 5.0); // Schlick + float ssrVisibility = gloss; + + float weight = clamp(F * ssrVisibility, 0.0, 1.0); + result = mix(baseColor, reflColor, weight); + outColor = vec4(result, 1.0); + return; + } + + // If RT is disabled for reflections, we are done. + if (!useRT) + { + outColor = vec4(result, 1.0); + return; + } + + // ------------------------------------------------------------------------- + // 2) Ray-traced reflections using TLAS and ray queries (1 ray / pixel). + // ------------------------------------------------------------------------- + const float RT_TMIN = 0.05; + const float RT_TMAX = 50.0; + const float RT_ORIGIN_BIAS = 0.05; + + vec3 origin = worldPos + N * RT_ORIGIN_BIAS; + + rayQueryEXT rq; + rayQueryInitializeEXT( + rq, + topLevelAS, + gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT, + 0xFF, + origin, + RT_TMIN, + R, + RT_TMAX + ); + + while (rayQueryProceedEXT(rq)) { } + bool rtHit = (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT); + + if (rtHit) + { + float tHit = rayQueryGetIntersectionTEXT(rq, true); + vec3 hitPos = origin + R * tHit; + + vec3 proj = projectToScreen(hitPos); + if (proj.z >= 0.0) + { + vec2 hitUV = proj.xy; + vec3 reflColor = texture(hdrColor, hitUV).rgb; + + float NoV = clamp(dot(N, V), 0.0, 1.0); + float F = F0 + (1.0 - F0) * pow(1.0 - NoV, 5.0); // Schlick + float rtVisibility = gloss; + + float weight = clamp(F * rtVisibility, 0.0, 1.0); + result = mix(baseColor, reflColor, weight); + } + } + + outColor = vec4(result, 1.0); +} diff --git a/src/core/context.h b/src/core/context.h index ee898a7..2c40656 100644 --- a/src/core/context.h +++ b/src/core/context.h @@ -76,6 +76,9 @@ public: // Runtime settings visible to passes/shaders ShadowSettings shadowSettings{}; bool enableSSR = false; // optional screen-space reflections toggle + // Reflection mode for SSR/RT reflections; encoded into sceneData.rtOptions.w + // 0 = SSR only, 1 = SSR + RT fallback, 2 = RT only + uint32_t reflectionMode = 0; // Ray tracing manager (optional, nullptr if unsupported) RayTracingManager* ray = nullptr; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 7b3519d..47324f3 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -569,8 +569,12 @@ void VulkanEngine::draw() //now that we are sure that the commands finished executing, we can safely reset the command buffer to begin recording again. VK_CHECK(vkResetCommandBuffer(get_current_frame()._mainCommandBuffer, 0)); - // Build or update TLAS for current frame now that the previous frame is idle - if (_rayManager && _context->shadowSettings.mode != 0u) + // Build or update TLAS for current frame now that the previous frame is idle. + // TLAS is used for hybrid/full RT shadows and RT-assisted SSR reflections. + // For reflections, only build TLAS when RT is actually enabled (reflectionMode != 0). + if (_rayManager && + (_context->shadowSettings.mode != 0u || + (_context->enableSSR && _context->reflectionMode != 0u))) { _rayManager->buildTLASFromDrawContext(_context->getMainDrawContext(), get_current_frame()._deletionQueue); } diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index c6696bc..8fa292c 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -602,6 +602,34 @@ namespace static void ui_postfx(VulkanEngine *eng) { if (!eng) return; + if (!eng->_context) return; + + EngineContext *ctx = eng->_context.get(); + + ImGui::TextUnformatted("Reflections"); + bool ssrEnabled = ctx->enableSSR; + if (ImGui::Checkbox("Enable Screen-Space Reflections", &ssrEnabled)) + { + ctx->enableSSR = ssrEnabled; + } + + int reflMode = static_cast(ctx->reflectionMode); + ImGui::TextUnformatted("Reflection Mode"); + ImGui::RadioButton("SSR only", &reflMode, 0); + ImGui::SameLine(); + ImGui::RadioButton("SSR + RT fallback", &reflMode, 1); + ImGui::SameLine(); + ImGui::RadioButton("RT only", &reflMode, 2); + + const bool rq = eng->_deviceManager->supportsRayQuery(); + const bool as = eng->_deviceManager->supportsAccelerationStructure(); + if (!(rq && as) && reflMode != 0) + { + reflMode = 0; // guard for unsupported HW + } + ctx->reflectionMode = static_cast(reflMode); + + ImGui::Separator(); if (auto *tm = eng->_renderPassManager ? eng->_renderPassManager->getPass() : nullptr) { float exp = tm->exposure(); diff --git a/src/render/passes/ssr.cpp b/src/render/passes/ssr.cpp index ebcb6b2..382626a 100644 --- a/src/render/passes/ssr.cpp +++ b/src/render/passes/ssr.cpp @@ -1,5 +1,6 @@ #include "ssr.h" +#include "raytracing.h" #include "core/frame/resources.h" #include "core/descriptor/manager.h" #include "core/descriptor/descriptors.h" @@ -38,16 +39,15 @@ void SSRPass::init(EngineContext *context) 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) + // Graphics pipelines: fullscreen triangle, no depth, HDR color attachment. + GraphicsPipelineCreateInfo baseInfo{}; + baseInfo.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); + baseInfo.setLayouts = { + _context->getDescriptorLayouts()->gpuSceneDataLayout(), // set = 0 (sceneData UBO + optional TLAS) _inputSetLayout // set = 1 (HDR + GBuffer) }; - info.configure = [this](PipelineBuilder &b) + baseInfo.configure = [this](PipelineBuilder &b) { b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); b.set_polygon_mode(VK_POLYGON_MODE_FILL); @@ -61,7 +61,15 @@ void SSRPass::init(EngineContext *context) } }; - _context->pipelines->createGraphicsPipeline("ssr", info); + // Non-RT variant (pure screen-space reflections). + GraphicsPipelineCreateInfo infoNoRT = baseInfo; + infoNoRT.fragmentShaderPath = _context->getAssets()->shaderPath("ssr.frag.spv"); + _context->pipelines->createGraphicsPipeline("ssr.nort", infoNoRT); + + // RT-assisted variant (SSR + ray-query fallback using TLAS). + GraphicsPipelineCreateInfo infoRT = baseInfo; + infoRT.fragmentShaderPath = _context->getAssets()->shaderPath("ssr_rt.frag.spv"); + _context->pipelines->createGraphicsPipeline("ssr.rt", infoRT); } void SSRPass::cleanup() @@ -148,10 +156,21 @@ void SSRPass::draw_ssr(VkCommandBuffer cmd, return; } - // Fetch (or refresh) pipeline for hot-reload support. - if (!pipelineManager->getGraphics("ssr", _pipeline, _pipelineLayout)) + // Choose RT variant only if TLAS is valid; otherwise fall back to non-RT. + const bool haveRTFeatures = deviceManager->supportsAccelerationStructure(); + const VkAccelerationStructureKHR tlas = (ctxLocal->ray ? ctxLocal->ray->tlas() : VK_NULL_HANDLE); + const VkDeviceAddress tlasAddr = (ctxLocal->ray ? ctxLocal->ray->tlasAddress() : 0); + const bool useRT = haveRTFeatures && (tlas != VK_NULL_HANDLE) && (tlasAddr != 0); + + const char *pipeName = useRT ? "ssr.rt" : "ssr.nort"; + if (!pipelineManager->getGraphics(pipeName, _pipeline, _pipelineLayout)) { - return; + // Try the other variant as a fallback. + const char *fallback = useRT ? "ssr.nort" : "ssr.rt"; + if (!pipelineManager->getGraphics(fallback, _pipeline, _pipelineLayout)) + { + return; + } } // Scene UBO (set=0, binding=0) – mirror LightingPass behavior. @@ -175,6 +194,10 @@ void SSRPass::draw_ssr(VkCommandBuffer cmd, { DescriptorWriter writer; writer.write_buffer(0, sceneBuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + if (useRT) + { + writer.write_acceleration_structure(1, tlas); + } writer.update_set(deviceManager->device(), globalSet); } diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 4595c79..2f70cf3 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -347,7 +347,10 @@ void SceneManager::update_scene() { const auto &ss = _context->shadowSettings; const uint32_t rtEnabled = (ss.mode != 0) ? 1u : 0u; - sceneData.rtOptions = glm::uvec4(rtEnabled, ss.hybridRayCascadesMask, ss.mode, 0u); + const uint32_t reflMode = _context->reflectionMode; + // rtOptions.x = RT shadows enabled, y = cascade mask, z = shadow mode, w = reflection mode (SSR/RT) + sceneData.rtOptions = glm::uvec4(rtEnabled, ss.hybridRayCascadesMask, ss.mode, reflMode); + // rtParams.x = N·L threshold for hybrid shadows; remaining components reserved sceneData.rtParams = glm::vec4(ss.hybridRayNoLThreshold, 0.0f, 0.0f, 0.0f); }