Skip to content

Commit

Permalink
feat(anti-aliasing): add anti-aliasing by FXAA
Browse files Browse the repository at this point in the history
  • Loading branch information
kuukitenshi committed Nov 28, 2024
1 parent ce15432 commit 43b6e5f
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Audio asset (#230, **@Dageus**, **@diogomsmiranda**).
- Compatibility with CMake find_package (#1326, **@RiscadoA**).
- 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**)
- Added the option to use Shadow Normal Offset Bias algorithm (#1308, **@GalaxyCrush**).
- 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 @@ -146,6 +146,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 @@ void cubos::engine::renderDefaultsPlugin(Cubos& cubos)
.add(entity, defaults.gBufferRasterizer)
.add(entity, defaults.ssao)
.add(entity, defaults.toneMapping)
.add(entity, defaults.fxaa)
.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)
{
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();
}
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 @@ namespace
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(
hdrBP && gammaBP && exposureBP && screenSizeBP && fxaaConfigBP,
"hdrTexture, gamma, exposure, screenSize, fxaaConfig and fxaaEnabled binding points must exist");
generateScreenQuad(renderDevice, pipeline, screenQuad);
fxaaConfigCB = renderDevice.createConstantBuffer(sizeof(FxaaConfig), nullptr, Usage::Dynamic);
}
};
} // namespace
Expand All @@ -60,6 +82,7 @@ void cubos::engine::toneMappingPlugin(Cubos& cubos)
cubos.uninitResource<State>();

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

cubos.startupSystem("setup Tone Mapping")
.tagged(assetsTag)
Expand All @@ -74,11 +97,21 @@ void cubos::engine::toneMappingPlugin(Cubos& cubos)
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,
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));
}
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 @@ void cubos::engine::toneMappingPlugin(Cubos& cubos)
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(fxaa.contains());
rd.setVertexArray(state.screenQuad);
rd.drawTriangles(0, 6);

Expand Down

0 comments on commit 43b6e5f

Please sign in to comment.