[GLSL] Simple, fast bicubic filtering shader function

Hello.

I ended up implementing a very simple bicubic texture filtering shader and thought I might as well share it here.

Screenshot comparison of bilinear and bicubic filtering (mouse over to switch to bicubic)

Code


vec4 cubic(float v){
    vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v;
    vec4 s = n * n * n;
    float x = s.x;
    float y = s.y - 4.0 * s.x;
    float z = s.z - 4.0 * s.y + 6.0 * s.x;
    float w = 6.0 - x - y - z;
    return vec4(x, y, z, w) * (1.0/6.0);
}

vec4 textureBicubic(sampler2D sampler, vec2 texCoords){

	vec2 texSize = textureSize(tex, 0);
	vec2 invTexSize = 1.0 / texSize;
	
	texCoords = texCoords * texSize - 0.5;

	
    vec2 fxy = fract(texCoords);
    texCoords -= fxy;

    vec4 xcubic = cubic(fxy.x);
    vec4 ycubic = cubic(fxy.y);

    vec4 c = texCoords.xxyy + vec2(-0.5, +1.5).xyxy;
    
    vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw);
    vec4 offset = c + vec4(xcubic.yw, ycubic.yw) / s;
    
    offset *= invTexSize.xxyy;
    
    vec4 sample0 = texture(sampler, offset.xz);
    vec4 sample1 = texture(sampler, offset.yz);
    vec4 sample2 = texture(sampler, offset.xw);
    vec4 sample3 = texture(sampler, offset.yw);

    float sx = s.x / (s.x + s.y);
    float sy = s.z / (s.z + s.w);

    return mix(
    	mix(sample3, sample2, sx), mix(sample1, sample0, sx)
    , sy);
}

Usage
1.
Simply add the above code into your GLSL shader source code just before your main() function. Alternatively, you can add [icode]#version 130[/icode] above it and put it in its own file. Then you attach both that shader and your shader program that contains main() to your shader program. To be able to use the function in your other shader, you need to define it by adding [icode]vec4 textureBicubic(sampler2D texture, vec2 texCoords);[/icode]. Note that the code uses textureSize() to retrieve the size of the texture for convenience, which requires GLSL version 130 (OGL 3.0). It’s possible to replace this with a uniform variable through which you manually pass in the texture’s size which would make the shader work on OGL2 hardware as well.

In your shader, simply replace your calls to [icode]texture(mySampler, myTexCoords)[/icode] with [icode]textureBicubic(mySampler, myTexCoords)[/icode]. Yep, it’s that simple.

Make sure your texture has bilinear filtering enabled (GL_LINEAR or the mipmapped versions of it)! The shader abuses bilinearly filtered samples to improve performance (4 taps instead of 16).

Notes
Because the shader uses texture() calls instead of texelFetch(), the shader obeys any and all texture parameters you set. The shader works with texture coordinate wrapping and clamping as it should and even works with mipmaps, trilinear filtering and even anisotropic filtering, although when using mipmaps the texture size will not be correct when the texture is minified, which if I understand the code correct means that the shader essentially falls back to bilinear filtering, which hopefully should be a bit sharper when you have a large resolution texture.

Technical details
Bicubic filtering is done by basing the result on the 4x4 area of texels around the sample point. This should require 16 texture samples and then some rather complex blending math between them, which would have a noticeable performance cost. This shader is inspired by this post and relies on the fact that bilinear filtering gives us a weighted average of texels in a 2x2 area. By sampling 4 bilinear samples with carefully modified offsets and how we combine these 4 samples, we can get the correct weights for all 16 texels we need. The result is an extremely fast bicubic filtering function that only requires 4 texture samples.

In case anyone’s wondering, I have a bicubic shader which also does manual (linear) filtering between mipmap levels as well to 100% correctly filter mipmaps as well.

Ooo thanks for the share!