diff --git a/CHANGELOG.md b/CHANGELOG.md index db71f50370..afc2b3058a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 0bb41db8f3..8745ab695c 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -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" diff --git a/engine/assets/render/tone_mapping.fs b/engine/assets/render/tone_mapping.fs index 84f83d2f9f..847fe13a69 100644 --- a/engine/assets/render/tone_mapping.fs +++ b/engine/assets/render/tone_mapping.fs @@ -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); diff --git a/engine/include/cubos/engine/render/defaults/target.hpp b/engine/include/cubos/engine/render/defaults/target.hpp index f60b5f66f3..e45531ca85 100644 --- a/engine/include/cubos/engine/render/defaults/target.hpp +++ b/engine/include/cubos/engine/render/defaults/target.hpp @@ -19,8 +19,10 @@ #include #include #include +#include #include + namespace cubos::core::memory { CUBOS_ENGINE_EXTERN template class CUBOS_ENGINE_API Opt; @@ -59,6 +61,9 @@ namespace cubos::engine /// @brief Tone Mapping component. ToneMapping toneMapping{}; + /// @brief FXAA component. + FXAA fxaa{}; + /// @brief Deferred Shading component. DeferredShading deferredShading{}; diff --git a/engine/include/cubos/engine/render/tone_mapping/fxaa.hpp b/engine/include/cubos/engine/render/tone_mapping/fxaa.hpp new file mode 100644 index 0000000000..5fd100716d --- /dev/null +++ b/engine/include/cubos/engine/render/tone_mapping/fxaa.hpp @@ -0,0 +1,31 @@ +/// @file +/// @brief Component @ref cubos::engine::FXAA. +/// @ingroup render-tone-mapping-plugin + +#pragma once + +#include + +#include + +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 diff --git a/engine/src/render/defaults/plugin.cpp b/engine/src/render/defaults/plugin.cpp index 18edd360ff..58fb614b7e 100644 --- a/engine/src/render/defaults/plugin.cpp +++ b/engine/src/render/defaults/plugin.cpp @@ -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) diff --git a/engine/src/render/tone_mapping/fxaa.cpp b/engine/src/render/tone_mapping/fxaa.cpp new file mode 100644 index 0000000000..7a2fc9ec8f --- /dev/null +++ b/engine/src/render/tone_mapping/fxaa.cpp @@ -0,0 +1,14 @@ +#include +#include + +#include + +CUBOS_REFLECT_IMPL(cubos::engine::FXAA) +{ + return core::ecs::TypeBuilder("cubos::engine::FXAA") + .withField("edgeThresholdMin", &FXAA::edgeThresholdMin) + .withField("edgeThresholdMax", &FXAA::edgeThresholdMax) + .withField("subpixelQuality", &FXAA::subpixelQuality) + .withField("iterations", &FXAA::iterations) + .build(); +} diff --git a/engine/src/render/tone_mapping/plugin.cpp b/engine/src/render/tone_mapping/plugin.cpp index 0fc5bf2b68..8c469cff6a 100644 --- a/engine/src/render/tone_mapping/plugin.cpp +++ b/engine/src/render/tone_mapping/plugin.cpp @@ -7,13 +7,16 @@ #include #include #include +#include #include #include +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; @@ -21,6 +24,14 @@ CUBOS_DEFINE_TAG(cubos::engine::toneMappingTag); namespace { + struct FxaaConfig + { + float edgeThresholdMin; + float edgeThresholdMax; + float subpixelQuality; + int iterations; + }; + struct State { CUBOS_ANONYMOUS_REFLECT(State); @@ -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 && fxaaEnabledBP, + "hdrTexture, gamma, exposure, screenSize, fxaaConfig and fxaaEnabled binding points must exist"); generateScreenQuad(renderDevice, pipeline, screenQuad); + fxaaConfigCB = renderDevice.createConstantBuffer(sizeof(FxaaConfig), nullptr, Usage::Dynamic); } }; } // namespace @@ -60,6 +82,7 @@ void cubos::engine::toneMappingPlugin(Cubos& cubos) cubos.uninitResource(); cubos.component(); + cubos.component(); cubos.startupSystem("setup Tone Mapping") .tagged(assetsTag) @@ -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 query) { + .call([](const State& state, const Window& window, + Query> 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(target.size.x), static_cast(target.size.y)); rd.setRasterState(nullptr); @@ -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(static_cast(fxaa.contains())); rd.setVertexArray(state.screenQuad); rd.drawTriangles(0, 6);