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 23, 2024
1 parent ce15432 commit f6a9328
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 3 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
152 changes: 151 additions & 1 deletion engine/assets/render/tone_mapping.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,162 @@ in vec2 fragUv;
uniform sampler2D hdrTexture;
uniform float gamma;
uniform float exposure;
uniform uvec2 screenSize;

const float EDGE_THRESHOLD_MIN = 0.0312;
const float EDGE_THRESHOLD_MAX = 0.125;
const float SUBPIXEL_QUALITY = 0.75;
const int ITERATIONS = 12;

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(EDGE_THRESHOLD_MIN,lumaMax*EDGE_THRESHOLD_MAX)){
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 * SUBPIXEL_QUALITY;
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 = fxaa(hdrTexture, fragUv, inverseScreenSize);
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
mapped = pow(mapped, vec3(1.0 / gamma));
color = vec4(mapped, 1.0);
Expand Down
6 changes: 5 additions & 1 deletion engine/src/render/tone_mapping/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace
ShaderBindingPoint gammaBP;
ShaderBindingPoint exposureBP;
ShaderBindingPoint hdrBP;
ShaderBindingPoint screenSize;
VertexArray screenQuad;

State(RenderDevice& renderDevice, const ShaderPipeline& pipeline)
Expand All @@ -37,7 +38,9 @@ namespace
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");
screenSize = pipeline->getBindingPoint("screenSize");
CUBOS_ASSERT(hdrBP && gammaBP && exposureBP && screenSize,

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

View check run for this annotation

Codecov / codecov/patch

engine/src/render/tone_mapping/plugin.cpp#L41-L42

Added lines #L41 - L42 were not covered by tests
"hdrTexture, gamma, exposure and screeSize binding points must exist");

generateScreenQuad(renderDevice, pipeline, screenQuad);
}
Expand Down Expand Up @@ -89,6 +92,7 @@ void cubos::engine::toneMappingPlugin(Cubos& cubos)
state.hdrBP->bind(hdr.frontTexture);
state.gammaBP->setConstant(toneMapping.gamma);
state.exposureBP->setConstant(toneMapping.exposure);
state.screenSize->setConstant(window->framebufferSize());

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L95 was not covered by tests
rd.setVertexArray(state.screenQuad);
rd.drawTriangles(0, 6);

Expand Down

0 comments on commit f6a9328

Please sign in to comment.