Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anti-aliasing): add anti-aliasing by FXAA #1385

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A proper Nix package which can be used to install Cubos and Tesseratos (#1327, **RiscadoA**).
- Added the option to use Shadow Normal Offset Bias algorithm (#1308, **@GalaxyCrush**)
- UI text element using MSDF for text rendering (#1300, **@mkuritsu**).
- Added anti-aliasing using FXAA technique (#1334, **@kuukitenshi**).

### Changed

Expand Down
1 change: 1 addition & 0 deletions engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ set(CUBOS_ENGINE_SOURCE
"src/render/voxels/palette.cpp"
"src/render/tone_mapping/plugin.cpp"
"src/render/tone_mapping/tone_mapping.cpp"
"src/render/tone_mapping/fxaa.cpp"
"src/render/lights/plugin.cpp"
"src/render/lights/environment.cpp"
"src/render/lights/directional.cpp"
Expand Down
163 changes: 162 additions & 1 deletion engine/assets/render/tone_mapping.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,172 @@ uniform sampler2D hdrTexture;
uniform float gamma;
uniform float exposure;

uniform uvec2 screenSize;
uniform bool fxaaEnabled;

layout(std140) uniform FxaaConfig
{
float edgeThresholdMin;
float edgeThresholdMax;
float subpixelQuality;
int iterations;
};

layout(location = 0) out vec4 color;

// Convert RGB to luma using the formula: L = 0.299 * R + 0.587 * G + 0.114 * B
float rgb2luma(vec3 rgb){
return sqrt(dot(rgb, vec3(0.299, 0.587, 0.114)));
}

float quality(int i) {
return (i < 5) ? 1.0 : 1.5 + (i - 5) * 0.5; //increase progressively the quality
}

vec3 fxaa(sampler2D screenTexture, vec2 fragUv, vec2 inverseScreenSize){

vec3 colorCenter = texture(screenTexture, fragUv).rgb;
float lumaCenter = rgb2luma(colorCenter);

// direct neighbours of the current fragment
float lumaDown = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(0,-1)).rgb);
float lumaUp = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(0,1)).rgb);
float lumaLeft = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(-1,0)).rgb);
float lumaRight = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(1,0)).rgb);

float lumaMin = min(lumaCenter,min(min(lumaDown,lumaUp),min(lumaLeft,lumaRight)));
float lumaMax = max(lumaCenter,max(max(lumaDown,lumaUp),max(lumaLeft,lumaRight)));
float lumaRange = lumaMax - lumaMin;

// when luma variation is lower that a threshold (or if we are in a dark area), we aren't on an edge, so don't do AA
if(lumaRange < max(edgeThresholdMin,lumaMax*edgeThresholdMax)){
return colorCenter;
}

// corners of the current fragment
float lumaDownLeft = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(-1,-1)).rgb);
float lumaUpRight = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(1,1)).rgb);
float lumaUpLeft = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(-1,1)).rgb);
float lumaDownRight = rgb2luma(textureOffset(screenTexture, fragUv, ivec2(1,-1)).rgb);

float lumaDownUp = lumaDown + lumaUp;
float lumaLeftRight = lumaLeft + lumaRight;
float lumaLeftCorners = lumaDownLeft + lumaUpLeft;
float lumaDownCorners = lumaDownLeft + lumaDownRight;
float lumaRightCorners = lumaDownRight + lumaUpRight;
float lumaUpCorners = lumaUpRight + lumaUpLeft;

// gradient for horizontal and vertical axis
float edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + abs(-2.0 * lumaCenter + lumaDownUp ) * 2.0 + abs(-2.0 * lumaRight + lumaRightCorners);
float edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + abs(-2.0 * lumaDown + lumaDownCorners);
bool isLocalEdgeHorizontal = (edgeHorizontal >= edgeVertical);

// pick the 2 neighboring texels lumas in the opposite direction to the local edge
float luma1 = isLocalEdgeHorizontal ? lumaDown : lumaLeft;
float luma2 = isLocalEdgeHorizontal ? lumaUp : lumaRight;

float gradient1 = luma1 - lumaCenter;
float gradient2 = luma2 - lumaCenter;

bool isGradient1Steepest = abs(gradient1) >= abs(gradient2); // steepness direction
float gradientScaled = 0.25*max(abs(gradient1),abs(gradient2));
float stepLength = isLocalEdgeHorizontal ? inverseScreenSize.y : inverseScreenSize.x; // step size (1 pixel) according to the edge direction

float lumaLocalAverage = 0.0;
if(isGradient1Steepest){
stepLength = - stepLength;
lumaLocalAverage = 0.5*(luma1 + lumaCenter);
} else {
lumaLocalAverage = 0.5*(luma2 + lumaCenter);
}
vec2 currentUv = fragUv;
if(isLocalEdgeHorizontal){
currentUv.y += stepLength * 0.5; // shift UV by half a pixel in the edge direction
} else {
currentUv.x += stepLength * 0.5;
}

vec2 offset = isLocalEdgeHorizontal ? vec2(inverseScreenSize.x, 0.0) : vec2(0.0, inverseScreenSize.y);
vec2 uv1 = currentUv - offset; // UV to explore sides of the edge
vec2 uv2 = currentUv + offset;

float lumaEnd1 = rgb2luma(texture(screenTexture,uv1).rgb);
float lumaEnd2 = rgb2luma(texture(screenTexture,uv2).rgb);
lumaEnd1 -= lumaLocalAverage;
lumaEnd2 -= lumaLocalAverage;

bool reachedEdge1 = abs(lumaEnd1) >= gradientScaled;
bool reachedEdge2 = abs(lumaEnd2) >= gradientScaled;
bool reachedBoth = reachedEdge1 && reachedEdge2;
if(!reachedEdge1){
uv1 -= offset;
}
if(!reachedEdge2){
uv2 += offset;
}
if(!reachedBoth){
for(int i = 2; i < iterations; i++){ // explores until reach both sides
if(!reachedEdge1){
lumaEnd1 = rgb2luma(texture(screenTexture, uv1).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
}
if(!reachedEdge2){
lumaEnd2 = rgb2luma(texture(screenTexture, uv2).rgb);
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
}
reachedEdge1 = abs(lumaEnd1) >= gradientScaled;
reachedEdge2 = abs(lumaEnd2) >= gradientScaled;
reachedBoth = reachedEdge1 && reachedEdge2;
if(!reachedEdge1){
uv1 -= offset * quality(i);
}
if(!reachedEdge2){
uv2 += offset * quality(i);
}
if(reachedBoth){
break;
}
}
}

float distanceToEdge1 = isLocalEdgeHorizontal ? (fragUv.x - uv1.x) : (fragUv.y - uv1.y);
float distanceToEdge2 = isLocalEdgeHorizontal ? (uv2.x - fragUv.x) : (uv2.y - fragUv.y);
bool isDirection1Closer = distanceToEdge1 < distanceToEdge2;
float distanceFinal = min(distanceToEdge1, distanceToEdge2);
float edgeLength = (distanceToEdge1 + distanceToEdge2);
float pixelOffset = - distanceFinal / edgeLength + 0.5; // UV offset

bool isLumaCenterSmaller = lumaCenter < lumaLocalAverage;
// if the luma center is smaller, the delta at each end should be positive (same variation) in the direction of the closer side of the edge
bool correctVariation = ((isDirection1Closer ? lumaEnd1 : lumaEnd2) < 0.0) != isLumaCenterSmaller;
float finalOffset = correctVariation ? pixelOffset : 0.0;

// sub-pixel shifting for thin lines, for this cases AA is computed over a 3x3 neighborhood
float lumaAverage = (1.0/12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
float subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter)/lumaRange,0.0,1.0);
float subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
float subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * subpixelQuality;
finalOffset = max(finalOffset,subPixelOffsetFinal);

vec2 finalUv = fragUv;
if(isLocalEdgeHorizontal){
finalUv.y += finalOffset * stepLength;
} else {
finalUv.x += finalOffset * stepLength;
}
return texture(screenTexture,finalUv).rgb;
}

void main()
{
vec3 hdrColor = texture(hdrTexture, fragUv).rgb;
vec2 inverseScreenSize = vec2(1.0 / screenSize.x, 1.0 / screenSize.y);
vec3 hdrColor;
if(fxaaEnabled){
hdrColor = fxaa(hdrTexture, fragUv, inverseScreenSize);
}
else {
hdrColor = texture(hdrTexture, fragUv).rgb;
}
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
mapped = pow(mapped, vec3(1.0 / gamma));
color = vec4(mapped, 1.0);
Expand Down
5 changes: 5 additions & 0 deletions engine/include/cubos/engine/render/defaults/target.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
#include <cubos/engine/render/split_screen/split_screen.hpp>
#include <cubos/engine/render/ssao/ssao.hpp>
#include <cubos/engine/render/target/target.hpp>
#include <cubos/engine/render/tone_mapping/fxaa.hpp>
#include <cubos/engine/render/tone_mapping/tone_mapping.hpp>


namespace cubos::core::memory
{
CUBOS_ENGINE_EXTERN template class CUBOS_ENGINE_API Opt<cubos::engine::SplitScreen>;
Expand Down Expand Up @@ -59,6 +61,9 @@ namespace cubos::engine
/// @brief Tone Mapping component.
ToneMapping toneMapping{};

/// @brief FXAA component.
FXAA fxaa{};

/// @brief Deferred Shading component.
DeferredShading deferredShading{};

Expand Down
31 changes: 31 additions & 0 deletions engine/include/cubos/engine/render/tone_mapping/fxaa.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// @file
/// @brief Component @ref cubos::engine::FXAA.
/// @ingroup render-tone-mapping-plugin

#pragma once

#include <cubos/core/reflection/reflect.hpp>

#include <cubos/engine/api.hpp>

namespace cubos::engine
{
/// @brief Component which stores the FXAA configuration for a render target.
/// @ingroup render-tone-mapping-plugin
struct CUBOS_ENGINE_API FXAA
{
CUBOS_REFLECT;

/// @brief Edge threshold's min value.
float edgeThresholdMin = 0.0312F;

/// @brief Edge threshold's max value.
float edgeThresholdMax = 0.125F;

/// @brief Subpixel quality value.
float subpixelQuality = 0.75F;

/// @brief Edges exploration iteration value.
int iterations = 12;
};
} // namespace cubos::engine
1 change: 1 addition & 0 deletions engine/src/render/defaults/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
.add(entity, defaults.gBufferRasterizer)
.add(entity, defaults.ssao)
.add(entity, defaults.toneMapping)
.add(entity, defaults.fxaa)

Check warning on line 79 in engine/src/render/defaults/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/defaults/plugin.cpp#L79

Added line #L79 was not covered by tests
.add(entity, defaults.deferredShading);

if (defaults.splitScreen)
Expand Down
14 changes: 14 additions & 0 deletions engine/src/render/tone_mapping/fxaa.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include <cubos/core/ecs/reflection.hpp>
#include <cubos/core/reflection/external/primitives.hpp>

#include <cubos/engine/render/tone_mapping/fxaa.hpp>

CUBOS_REFLECT_IMPL(cubos::engine::FXAA)

Check warning on line 6 in engine/src/render/tone_mapping/fxaa.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/fxaa.cpp#L6

Added line #L6 was not covered by tests
{
return core::ecs::TypeBuilder<FXAA>("cubos::engine::FXAA")
.withField("edgeThresholdMin", &FXAA::edgeThresholdMin)
.withField("edgeThresholdMax", &FXAA::edgeThresholdMax)
.withField("subpixelQuality", &FXAA::subpixelQuality)
.withField("iterations", &FXAA::iterations)
.build();

Check warning on line 13 in engine/src/render/tone_mapping/fxaa.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/fxaa.cpp#L8-L13

Added lines #L8 - L13 were not covered by tests
}
44 changes: 40 additions & 4 deletions engine/src/render/tone_mapping/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@
#include <cubos/engine/render/shader/plugin.hpp>
#include <cubos/engine/render/target/plugin.hpp>
#include <cubos/engine/render/target/target.hpp>
#include <cubos/engine/render/tone_mapping/fxaa.hpp>
#include <cubos/engine/render/tone_mapping/plugin.hpp>
#include <cubos/engine/window/plugin.hpp>

using cubos::core::gl::ConstantBuffer;
using cubos::core::gl::generateScreenQuad;
using cubos::core::gl::RenderDevice;
using cubos::core::gl::ShaderBindingPoint;
using cubos::core::gl::ShaderPipeline;
using cubos::core::gl::Usage;
using cubos::core::gl::VertexArray;
using cubos::core::io::Window;

CUBOS_DEFINE_TAG(cubos::engine::toneMappingTag);

namespace
{
struct FxaaConfig
{
float edgeThresholdMin;
float edgeThresholdMax;
float subpixelQuality;
int iterations;
};

struct State
{
CUBOS_ANONYMOUS_REFLECT(State);
Expand All @@ -29,17 +40,28 @@
ShaderBindingPoint gammaBP;
ShaderBindingPoint exposureBP;
ShaderBindingPoint hdrBP;
ShaderBindingPoint screenSizeBP;
ShaderBindingPoint fxaaConfigBP;
ShaderBindingPoint fxaaEnabledBP;

VertexArray screenQuad;

ConstantBuffer fxaaConfigCB;

State(RenderDevice& renderDevice, const ShaderPipeline& pipeline)
: pipeline(pipeline)
{
hdrBP = pipeline->getBindingPoint("hdrTexture");
gammaBP = pipeline->getBindingPoint("gamma");
exposureBP = pipeline->getBindingPoint("exposure");
CUBOS_ASSERT(hdrBP && gammaBP && exposureBP, "hdrTexture, gamma and exposure binding points must exist");

screenSizeBP = pipeline->getBindingPoint("screenSize");
fxaaConfigBP = pipeline->getBindingPoint("FxaaConfig");
fxaaEnabledBP = pipeline->getBindingPoint("fxaaEnabled");
CUBOS_ASSERT(

Check warning on line 60 in engine/src/render/tone_mapping/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L57-L60

Added lines #L57 - L60 were not covered by tests
hdrBP && gammaBP && exposureBP && screenSizeBP && fxaaConfigBP && fxaaEnabledBP,
"hdrTexture, gamma, exposure, screenSize, fxaaConfig and fxaaEnabled binding points must exist");
generateScreenQuad(renderDevice, pipeline, screenQuad);
fxaaConfigCB = renderDevice.createConstantBuffer(sizeof(FxaaConfig), nullptr, Usage::Dynamic);

Check warning on line 64 in engine/src/render/tone_mapping/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L64

Added line #L64 was not covered by tests
}
};
} // namespace
Expand All @@ -60,6 +82,7 @@
cubos.uninitResource<State>();

cubos.component<ToneMapping>();
cubos.component<FXAA>();

Check warning on line 85 in engine/src/render/tone_mapping/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L85

Added line #L85 was not covered by tests

cubos.startupSystem("setup Tone Mapping")
.tagged(assetsTag)
Expand All @@ -74,11 +97,21 @@
cubos.system("apply Tone Mapping to the HDR texture")
.tagged(drawToRenderTargetTag)
.tagged(toneMappingTag)
.call([](const State& state, const Window& window, Query<RenderTarget&, const HDR&, const ToneMapping&> query) {
.call([](const State& state, const Window& window,

Check warning on line 100 in engine/src/render/tone_mapping/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L100

Added line #L100 was not covered by tests
Query<RenderTarget&, const HDR&, const ToneMapping&, Opt<const FXAA&>> query) {
auto& rd = window->renderDevice();

for (auto [target, hdr, toneMapping] : query)
for (auto [target, hdr, toneMapping, fxaa] : query)
{
if (fxaa.contains())
{
FxaaConfig fxaaConfig{};
fxaaConfig.edgeThresholdMin = fxaa->edgeThresholdMin;
fxaaConfig.edgeThresholdMax = fxaa->edgeThresholdMax;
fxaaConfig.subpixelQuality = fxaa->subpixelQuality;
fxaaConfig.iterations = fxaa->iterations;
state.fxaaConfigCB->fill(&fxaaConfig, sizeof(FxaaConfig));

Check warning on line 113 in engine/src/render/tone_mapping/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L108-L113

Added lines #L108 - L113 were not covered by tests
}
kuukitenshi marked this conversation as resolved.
Show resolved Hide resolved
rd.setFramebuffer(target.framebuffer);
rd.setViewport(0, 0, static_cast<int>(target.size.x), static_cast<int>(target.size.y));
rd.setRasterState(nullptr);
Expand All @@ -89,6 +122,9 @@
state.hdrBP->bind(hdr.frontTexture);
state.gammaBP->setConstant(toneMapping.gamma);
state.exposureBP->setConstant(toneMapping.exposure);
state.screenSizeBP->setConstant(hdr.size);
state.fxaaConfigBP->bind(state.fxaaConfigCB);
state.fxaaEnabledBP->setConstant(static_cast<int>(fxaa.contains()));

Check warning on line 127 in engine/src/render/tone_mapping/plugin.cpp

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L125-L127

Added lines #L125 - L127 were not covered by tests
rd.setVertexArray(state.screenQuad);
rd.drawTriangles(0, 6);

Expand Down
Loading