diff --git a/Readme.md b/Readme.md index ae2e6d0..e79ff64 100644 --- a/Readme.md +++ b/Readme.md @@ -1,21 +1,24 @@ # CopernicusEngine Multipurpose Vulkan render engine specialized for physics simulation and solar system visualization +![](vk.png) + ## Introduction Work-In-Progress Vulkan render engine Current structure: - Flexible render graph system with multiple render passes, Hot reloading - Deferred rendering - PBR, cascaded shadows, normal mapping (MikkTSpace tangents optional) -- GLTF loading and rendering, primitive creation and rendering. +- GLTF loading and rendering, primitive creation and rendering - Supports texture compression(BCn, non glTF standard), LRU reload -- Object clicking, generation. +- Runtime object clicking, generation, movement - Multi light system - SSR +- FXAA +- Bloom Work-In-Progress -- [ ] AA -- [ ] bloom +- [ ] Floating origin with double precision coordinate system - [ ] Planet Rendering ## Build prequsites diff --git a/shaders/fxaa.frag b/shaders/fxaa.frag new file mode 100644 index 0000000..33250c8 --- /dev/null +++ b/shaders/fxaa.frag @@ -0,0 +1,52 @@ +#version 450 + +layout(location = 0) in vec2 inUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D uColor; + +layout(push_constant) uniform Push +{ + float inverse_width; + float inverse_height; + float edge_threshold; + float edge_threshold_min; +} pc; + +float luma(vec3 c) +{ + return dot(c, vec3(0.299, 0.587, 0.114)); +} + +void main() +{ + vec2 texel = vec2(pc.inverse_width, pc.inverse_height); + + vec3 cM = texture(uColor, inUV).rgb; + vec3 cN = texture(uColor, inUV + vec2(0.0, texel.y)).rgb; + vec3 cS = texture(uColor, inUV + vec2(0.0, -texel.y)).rgb; + vec3 cE = texture(uColor, inUV + vec2( texel.x, 0.0)).rgb; + vec3 cW = texture(uColor, inUV + vec2(-texel.x, 0.0)).rgb; + + float lM = luma(cM); + float lN = luma(cN); + float lS = luma(cS); + float lE = luma(cE); + float lW = luma(cW); + + float lMin = min(lM, min(min(lN, lS), min(lE, lW))); + float lMax = max(lM, max(max(lN, lS), max(lE, lW))); + float lRange = lMax - lMin; + + float threshold = max(pc.edge_threshold_min, pc.edge_threshold * lMax); + if (lRange < threshold) + { + outColor = vec4(cM, 1.0); + return; + } + + // Simple 5-tap cross blur when we detect an edge. + vec3 avg = (cM + cN + cS + cE + cW) * 0.2; + outColor = vec4(avg, 1.0); +} + diff --git a/shaders/tonemap.frag b/shaders/tonemap.frag index 59c3d70..a788fd8 100644 --- a/shaders/tonemap.frag +++ b/shaders/tonemap.frag @@ -9,6 +9,9 @@ layout(push_constant) uniform Push { float exposure; int mode; + int bloomEnabled; + float bloomThreshold; + float bloomIntensity; } pc; vec3 reinhard(vec3 x) @@ -32,6 +35,34 @@ void main() { vec3 hdr = texture(uHdr, inUV).rgb; + // Simple bloom in HDR space: gather bright neighbors and add a small blurred contribution. + if (pc.bloomEnabled != 0) + { + vec2 texel = 1.0 / vec2(textureSize(uHdr, 0)); + vec3 bloom = vec3(0.0); + int radius = 2; + int count = 0; + for (int x = -radius; x <= radius; ++x) + { + for (int y = -radius; y <= radius; ++y) + { + vec2 offset = vec2(x, y) * texel; + vec3 c = texture(uHdr, clamp(inUV + offset, vec2(0.0), vec2(1.0))).rgb; + float bright = max(max(c.r, c.g), c.b) - pc.bloomThreshold; + if (bright > 0.0) + { + bloom += c * bright; + count++; + } + } + } + if (count > 0) + { + bloom /= float(count); + } + hdr += pc.bloomIntensity * bloom; + } + // Simple exposure float exposure = max(pc.exposure, 0.0001); vec3 mapped = hdr * exposure; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0dfc1d4..45861e0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -70,6 +70,8 @@ add_executable (vulkan_engine render/passes/lighting.cpp render/passes/shadow.h render/passes/shadow.cpp + render/passes/fxaa.h + render/passes/fxaa.cpp render/passes/ssr.h render/passes/ssr.cpp render/passes/transparent.h diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 59157f5..c50a389 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -42,6 +42,7 @@ #include "render/passes/imgui_pass.h" #include "render/passes/lighting.h" #include "render/passes/transparent.h" +#include "render/passes/fxaa.h" #include "render/passes/tonemap.h" #include "render/passes/shadow.h" #include "device/resource.h" @@ -848,6 +849,12 @@ void VulkanEngine::draw() { RGImageHandle hdrInput = (ssrEnabled && hSSR.valid()) ? hSSR : hDraw; finalColor = tonemap->register_graph(_renderGraph.get(), hdrInput); + + // Optional FXAA pass: runs on LDR tonemapped output. + if (auto *fxaa = _renderPassManager->getPass()) + { + finalColor = fxaa->register_graph(_renderGraph.get(), finalColor); + } } else { diff --git a/src/core/engine_ui.cpp b/src/core/engine_ui.cpp index 10f68c3..ec72605 100644 --- a/src/core/engine_ui.cpp +++ b/src/core/engine_ui.cpp @@ -12,6 +12,7 @@ #include "render/primitives.h" #include "vk_mem_alloc.h" #include "render/passes/tonemap.h" +#include "render/passes/fxaa.h" #include "render/passes/background.h" #include #include @@ -147,6 +148,30 @@ namespace "5x5 spheres: metallic across columns, roughness across rows.\nExtra: chrome + glass."); ImGui::Separator(); + + // Post-processing: FXAA + if (auto *fx = eng->_renderPassManager ? eng->_renderPassManager->getPass() : nullptr) + { + bool fxaaEnabled = fx->enabled(); + if (ImGui::Checkbox("FXAA", &fxaaEnabled)) + { + fx->set_enabled(fxaaEnabled); + } + float edgeTh = fx->edge_threshold(); + if (ImGui::SliderFloat("FXAA Edge Threshold", &edgeTh, 0.01f, 0.5f)) + { + fx->set_edge_threshold(edgeTh); + } + float edgeThMin = fx->edge_threshold_min(); + if (ImGui::SliderFloat("FXAA Edge Threshold Min", &edgeThMin, 0.0f, 0.1f)) + { + fx->set_edge_threshold_min(edgeThMin); + } + } + else + { + ImGui::TextUnformatted("FXAA pass not available"); + } ImGui::TextUnformatted("IBL Volumes (reflection probes)"); if (!eng->_iblManager) @@ -740,6 +765,23 @@ namespace mode = 1; tm->setMode(mode); } + + // Bloom controls + bool bloomEnabled = tm->bloomEnabled(); + if (ImGui::Checkbox("Bloom", &bloomEnabled)) + { + tm->setBloomEnabled(bloomEnabled); + } + float bloomThreshold = tm->bloomThreshold(); + if (ImGui::SliderFloat("Bloom Threshold", &bloomThreshold, 0.0f, 5.0f)) + { + tm->setBloomThreshold(bloomThreshold); + } + float bloomIntensity = tm->bloomIntensity(); + if (ImGui::SliderFloat("Bloom Intensity", &bloomIntensity, 0.0f, 2.0f)) + { + tm->setBloomIntensity(bloomIntensity); + } } else { diff --git a/src/render/passes/fxaa.cpp b/src/render/passes/fxaa.cpp new file mode 100644 index 0000000..833febc --- /dev/null +++ b/src/render/passes/fxaa.cpp @@ -0,0 +1,140 @@ +#include "fxaa.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/frame/resources.h" + +void FxaaPass::init(EngineContext *context) +{ + _context = context; + if (!_context || !_context->getDevice() || !_context->getDescriptorLayouts() || !_context->pipelines) + { + return; + } + + _inputSetLayout = _context->getDescriptorLayouts()->singleImageLayout(); + + GraphicsPipelineCreateInfo info{}; + info.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); + info.fragmentShaderPath = _context->getAssets()->shaderPath("fxaa.frag.spv"); + info.setLayouts = { _inputSetLayout }; + + VkPushConstantRange pcr{}; + pcr.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pcr.offset = 0; + pcr.size = sizeof(FxaaPush); + info.pushConstants = { pcr }; + + 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(); + b.set_color_attachment_format(VK_FORMAT_R8G8B8A8_UNORM); + }; + + _context->pipelines->createGraphicsPipeline("fxaa", info); +} + +void FxaaPass::cleanup() +{ + _deletionQueue.flush(); +} + +void FxaaPass::execute(VkCommandBuffer) +{ + // Executed via render graph. +} + +RGImageHandle FxaaPass::register_graph(RenderGraph *graph, RGImageHandle ldrInput) +{ + if (!graph || !ldrInput.valid() || !_context) + { + return {}; + } + + // If disabled, simply bypass and return the input image. + if (!_enabled) + { + return ldrInput; + } + + RGImageDesc desc{}; + desc.name = "ldr.fxaa"; + desc.format = VK_FORMAT_R8G8B8A8_UNORM; + desc.extent = _context->getDrawExtent(); + desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT + | VK_IMAGE_USAGE_SAMPLED_BIT + | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + RGImageHandle aaOutput = graph->create_image(desc); + + graph->add_pass( + "FXAA", + RGPassType::Graphics, + [ldrInput, aaOutput](RGPassBuilder &builder, EngineContext *) { + builder.read(ldrInput, RGImageUsage::SampledFragment); + builder.write_color(aaOutput, true /*clear*/); + }, + [this, ldrInput](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) { + draw_fxaa(cmd, ctx, res, ldrInput); + }); + + return aaOutput; +} + +void FxaaPass::draw_fxaa(VkCommandBuffer cmd, EngineContext *ctx, const RGPassResources &res, + RGImageHandle ldrInput) +{ + if (!ctx || !ctx->currentFrame) return; + DeviceManager *deviceManager = ctx->getDevice(); + DescriptorManager *descriptorLayouts = ctx->getDescriptorLayouts(); + PipelineManager *pipelineManager = ctx->pipelines; + if (!deviceManager || !descriptorLayouts || !pipelineManager) return; + + VkImageView srcView = res.image_view(ldrInput); + if (srcView == VK_NULL_HANDLE) return; + + VkDevice device = deviceManager->device(); + + VkDescriptorSet set = ctx->currentFrame->_frameDescriptors.allocate(device, _inputSetLayout); + DescriptorWriter writer; + writer.write_image(0, srcView, ctx->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + writer.update_set(device, set); + + VkPipeline pipeline{}; + VkPipelineLayout layout{}; + if (!pipelineManager->getGraphics("fxaa", pipeline, layout)) + { + return; + } + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &set, 0, nullptr); + + VkExtent2D extent = ctx->getDrawExtent(); + + FxaaPush push{}; + push.inverse_width = extent.width > 0 ? 1.0f / static_cast(extent.width) : 0.0f; + push.inverse_height = extent.height > 0 ? 1.0f / static_cast(extent.height) : 0.0f; + push.edge_threshold = _edge_threshold; + push.edge_threshold_min = _edge_threshold_min; + vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(FxaaPush), &push); + + VkViewport vp{0.f, 0.f, static_cast(extent.width), static_cast(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/fxaa.h b/src/render/passes/fxaa.h new file mode 100644 index 0000000..edbf07b --- /dev/null +++ b/src/render/passes/fxaa.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +class EngineContext; +class RenderGraph; +class RGPassResources; + +// Simple post-process anti-aliasing pass (FXAA-like). +// Operates on the LDR tonemapped image and outputs a smoothed LDR image. +class FxaaPass final : public IRenderPass +{ +public: + void init(EngineContext *context) override; + void cleanup() override; + void execute(VkCommandBuffer) override; // Not used directly; executed via render graph + const char *getName() const override { return "FXAA"; } + + // Register pass in the render graph. Returns the AA output image handle. + RGImageHandle register_graph(RenderGraph *graph, RGImageHandle ldrInput); + + // Runtime parameters + void set_enabled(bool e) { _enabled = e; } + bool enabled() const { return _enabled; } + void set_edge_threshold(float v) { _edge_threshold = v; } + float edge_threshold() const { return _edge_threshold; } + void set_edge_threshold_min(float v) { _edge_threshold_min = v; } + float edge_threshold_min() const { return _edge_threshold_min; } + +private: + struct FxaaPush + { + float inverse_width; + float inverse_height; + float edge_threshold; + float edge_threshold_min; + }; + + void draw_fxaa(VkCommandBuffer cmd, EngineContext *ctx, const RGPassResources &res, + RGImageHandle ldrInput); + + EngineContext *_context = nullptr; + + VkPipeline _pipeline = VK_NULL_HANDLE; + VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout _inputSetLayout = VK_NULL_HANDLE; + + // Tunables for edge detection; chosen to be conservative by default. + bool _enabled = true; + float _edge_threshold = 0.125f; + float _edge_threshold_min = 0.0312f; + + DeletionQueue _deletionQueue; +}; diff --git a/src/render/passes/tonemap.cpp b/src/render/passes/tonemap.cpp index 919e983..88dc806 100644 --- a/src/render/passes/tonemap.cpp +++ b/src/render/passes/tonemap.cpp @@ -17,6 +17,9 @@ struct TonemapPush { float exposure; int mode; + int bloomEnabled; + float bloomThreshold; + float bloomIntensity; }; void TonemapPass::init(EngineContext *context) @@ -72,7 +75,9 @@ RGImageHandle TonemapPass::register_graph(RenderGraph *graph, RGImageHandle hdrI desc.name = "ldr.tonemap"; desc.format = VK_FORMAT_R8G8B8A8_UNORM; desc.extent = _context->getDrawExtent(); - desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT + | VK_IMAGE_USAGE_SAMPLED_BIT + | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; RGImageHandle ldr = graph->create_image(desc); graph->add_pass( @@ -109,7 +114,12 @@ void TonemapPass::draw_tonemap(VkCommandBuffer cmd, EngineContext *ctx, const RG vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 0, 1, &set, 0, nullptr); - TonemapPush push{_exposure, _mode}; + TonemapPush push{}; + push.exposure = _exposure; + push.mode = _mode; + push.bloomEnabled = _bloomEnabled ? 1 : 0; + push.bloomThreshold = _bloomThreshold; + push.bloomIntensity = _bloomIntensity; vkCmdPushConstants(cmd, _pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(TonemapPush), &push); VkExtent2D extent = ctx->getDrawExtent(); diff --git a/src/render/passes/tonemap.h b/src/render/passes/tonemap.h index 4cb63a5..2380426 100644 --- a/src/render/passes/tonemap.h +++ b/src/render/passes/tonemap.h @@ -25,6 +25,13 @@ public: void setMode(int m) { _mode = m; } int mode() const { return _mode; } + void setBloomEnabled(bool b) { _bloomEnabled = b; } + bool bloomEnabled() const { return _bloomEnabled; } + void setBloomThreshold(float t) { _bloomThreshold = t; } + float bloomThreshold() const { return _bloomThreshold; } + void setBloomIntensity(float i) { _bloomIntensity = i; } + float bloomIntensity() const { return _bloomIntensity; } + private: void draw_tonemap(VkCommandBuffer cmd, EngineContext *ctx, const RGPassResources &res, RGImageHandle hdrInput); @@ -38,6 +45,10 @@ private: float _exposure = 1.0f; int _mode = 1; // default to ACES + bool _bloomEnabled = true; + float _bloomThreshold = 1.0f; + float _bloomIntensity = 0.7f; + DeletionQueue _deletionQueue; }; diff --git a/src/render/renderpass.cpp b/src/render/renderpass.cpp index 7aeaf58..b99b81c 100644 --- a/src/render/renderpass.cpp +++ b/src/render/renderpass.cpp @@ -5,6 +5,7 @@ #include "passes/imgui_pass.h" #include "passes/lighting.h" #include "passes/ssr.h" +#include "passes/fxaa.h" #include "passes/transparent.h" #include "passes/tonemap.h" #include "passes/shadow.h" @@ -35,6 +36,11 @@ void RenderPassManager::init(EngineContext *context) ssrPass->init(context); addPass(std::move(ssrPass)); + // Post-process AA (FXAA-like) after tonemapping. + auto fxaaPass = std::make_unique(); + fxaaPass->init(context); + addPass(std::move(fxaaPass)); + auto transparentPass = std::make_unique(); transparentPass->init(context); addPass(std::move(transparentPass)); diff --git a/vk.png b/vk.png new file mode 100644 index 0000000..69f3470 Binary files /dev/null and b/vk.png differ