FIX: FXAA, bloom
This commit is contained in:
11
Readme.md
11
Readme.md
@@ -1,21 +1,24 @@
|
||||
# CopernicusEngine
|
||||
Multipurpose Vulkan render engine specialized for physics simulation and solar system visualization
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
52
shaders/fxaa.frag
Normal file
52
shaders/fxaa.frag
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<FxaaPass>())
|
||||
{
|
||||
finalColor = fxaa->register_graph(_renderGraph.get(), finalColor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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 <glm/gtx/euler_angles.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
@@ -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<FxaaPass>() : 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
|
||||
{
|
||||
|
||||
140
src/render/passes/fxaa.cpp
Normal file
140
src/render/passes/fxaa.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "fxaa.h"
|
||||
|
||||
#include <core/context.h>
|
||||
#include <core/descriptor/descriptors.h>
|
||||
#include <core/descriptor/manager.h>
|
||||
#include <core/pipeline/manager.h>
|
||||
#include <core/assets/manager.h>
|
||||
#include <core/device/device.h>
|
||||
#include <core/device/resource.h>
|
||||
#include <core/pipeline/sampler.h>
|
||||
#include <render/graph/graph.h>
|
||||
#include <render/graph/resources.h>
|
||||
|
||||
#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<float>(extent.width) : 0.0f;
|
||||
push.inverse_height = extent.height > 0 ? 1.0f / static_cast<float>(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<float>(extent.width), static_cast<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);
|
||||
}
|
||||
56
src/render/passes/fxaa.h
Normal file
56
src/render/passes/fxaa.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/types.h>
|
||||
#include <render/renderpass.h>
|
||||
#include <render/graph/types.h>
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>();
|
||||
fxaaPass->init(context);
|
||||
addPass(std::move(fxaaPass));
|
||||
|
||||
auto transparentPass = std::make_unique<TransparentPass>();
|
||||
transparentPass->init(context);
|
||||
addPass(std::move(transparentPass));
|
||||
|
||||
Reference in New Issue
Block a user