Sharper Mipmapping using Shader Based Supersampling

Inscrutable Text

Bilinear Filtering (200% Pixel Scale)
Anisotropic 8x Bias -1.0 (200% Pixel Scale)
Anisotropic 8x Bias -1.0 2x2 RGSS (200% Pixel Scale)

MIP Mapping

Multum in Parvo

Extreme Supersampling (200% Pixel Scale)
No mipmaps, Bilinear Filtering (200% Pixel Scale)
Colored Mipmaps, Bilinear Filtering (200% Pixel Scale)

Isotropic Filtering

Bilinear Filtering (200% Pixel Scale)
Trilinear Filtering (200% Pixel Scale)

Anisotropic Filtering

Anisotropic Filtering quality level comparison (200% Pixel Scale)
Anisotropic Filtering 8x (200% Pixel Scale)
“Ground Truth” Supersampling (200% Pixel Scale)
Anisotropic Filtering 8x vs “Ground Truth” (200% Pixel Scale)

Going Sharper-ish

The Modest Proposal

No mipmaps, aka “The Horror” (200% Pixel Scale)

Leveraging Conservative Bias

half4 col = tex2Dbias(_MainTex, float4(i.uv.xy, 0.0, _Bias));
// per pixel screen space partial derivatives
float2 dx = ddx(i.uv);
float2 dy = ddy(i.uv);
// bias scale
float bias = pow(2, _Bias);
half4 col = tex2Dgrad(_MainTex, i.uv.xy, dx * bias, dy * bias);
Anisotropic Filtering 8x Bias -0.5 (200% Pixel Scale)
Anisotropic Filtering 8x Bias -1.0 (200% Pixel Scale)

The Super Solution

Supersampling

// per pixel screen space partial derivatives
float2 dx = ddx(i.uv.xy) * 0.25; // horizontal offset
float2 dy = ddy(i.uv.xy) * 0.25; // vertical offset
// supersampled 2x2 ordered grid
half4 col = 0;
col += tex2Dbias(_MainTex, float4(i.uv.xy + dx + dy, 0.0, _Bias));
col += tex2Dbias(_MainTex, float4(i.uv.xy - dx + dy, 0.0, _Bias));
col += tex2Dbias(_MainTex, float4(i.uv.xy + dx - dy, 0.0, _Bias));
col += tex2Dbias(_MainTex, float4(i.uv.xy - dx - dy, 0.0, _Bias));
col *= 0.25;
Anisotropic Filtering 8x Bias -1.0 2x2 OGSS (200% Pixel Scale)

Multi Sample Anti-Aliasing

from A Quick Overview of MSAA
// per pixel partial derivatives
float2 dx = ddx(i.uv.xy);
float2 dy = ddy(i.uv.xy);
// rotated grid uv offsets
float2 uvOffsets = float2(0.125, 0.375);
float4 offsetUV = float4(0.0, 0.0, 0.0, _Bias);
// supersampled using 2x2 rotated grid
half4 col = 0;
offsetUV.xy = i.uv.xy + uvOffsets.x * dx + uvOffsets.y * dy;
col += tex2Dbias(_MainTex, offsetUV);
offsetUV.xy = i.uv.xy - uvOffsets.x * dx - uvOffsets.y * dy;
col += tex2Dbias(_MainTex, offsetUV);
offsetUV.xy = i.uv.xy + uvOffsets.y * dx - uvOffsets.x * dy;
col += tex2Dbias(_MainTex, offsetUV);
offsetUV.xy = i.uv.xy - uvOffsets.y * dx + uvOffsets.x * dy;
col += tex2Dbias(_MainTex, offsetUV);
col *= 0.25;
Anisotropic Filtering 8x Bias -1.0 2x2 RGSS (200% Pixel Scale)
2x2 RGSS vs “Ground Truth” (200% Pixel Scale)

Closing Thoughts

Bilinear vs RGSS (200% Pixel Scale)

Caveats & Additional Thoughts

Performance and Limitations

Colored Mipmaps

Bilinear (200% Pixel Scale)
Trilinear (200% Pixel Scale)
Anisotropic 8x (200% Pixel Scale)

Giffy

Taking Credit

Signed Distance Fields

MORE SAMPLES

Texture Compression

Gamma Color Space

Kaiser-filtered Mipmaps

Unity’s Kaiser vs Box filtering vs Nvidia Kaiser, Anisotropic 8x (200% Pixel Scale)
Unity’s Kaiser vs Box filtering vs Nvidia Kaiser, RGSS (200% Pixel Scale)
Unity 2019 and earlier Kaiser vs Unity 2020.1 Kaiser vs legacy Nvidia Kaiser, Anisotropic 8x (200% Pixel Scale)
Unity 2019 and earlier Kaiser vs Unity 2020.1 Kaiser vs legacy Nvidia Kaiser, RGSS (200% Pixel Scale)
Unity 2019 vs Unity 2020.1 vs Nvidia 2020.1.3 vs Nvidia legacy Kaiser filters, Anisotropic 8x (200% Pixel Scale)
Ground Truth vs legacy Nvidia Kaiser vs new Nvidia Kaiser, RGSS (200% Pixel Scale)

Blurring at 1:1 Scaling

// per pixel partial derivatives
float2 dx = ddx(i.uv);
float2 dy = ddy(i.uv);
// manually calculate the per axis mip level, clamp to 0 to 1
// and use that to scale down the derivatives
dx *= saturate(
0.5 * log2(dot(dx * textureRes, dx * textureRes))
);
dy *= saturate(
0.5 * log2(dot(dy * textureRes, dy * textureRes))
);
// rotated grid uv offsets
float2 uvOffsets = float2(0.125, 0.375);
float4 offsetUV = float4(0.0, 0.0, 0.0, _Bias);
// supersampled using 2x2 rotated grid
half4 col = 0;
offsetUV.xy = i.uv.xy + uvOffsets.x * dx + uvOffsets.y * dy;
col += tex2Dbias(_MainTex, offsetUV);
offsetUV.xy = i.uv.xy - uvOffsets.x * dx - uvOffsets.y * dy;
col += tex2Dbias(_MainTex, offsetUV);
offsetUV.xy = i.uv.xy + uvOffsets.y * dx - uvOffsets.x * dy;
col += tex2Dbias(_MainTex, offsetUV);
offsetUV.xy = i.uv.xy - uvOffsets.y * dx + uvOffsets.x * dy;
col += tex2Dbias(_MainTex, offsetUV);
col *= 0.25;

Even Sharper

Max Anisotropic Filtering Quality

Unity’s Shader Graph

Temporal Jittering

Quad Antialiasing

Ground Truth

Notes:

--

--

Tech Artist & Graphics Programmer lately focused on Unity VR game dev. https://ko-fi.com/bgolus

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store