Scaling images with custom filter via compute shaders

This small example shows how to generate mipmaps of an image with compute shaders. The advantage of this is, that you can use custom filters (like min/max filter or whatever).

It expects the image to resize to be named “source.png” in the same folder. You can change this easily, though.
It is important that the source texture has a sized format, e.g. FRgba8. Because of ImageLoadStore, only RGBA textures, but not RGB textures are supported.

main.py:

from panda3d.core import NodePath, Texture, Shader, ShaderAttrib
from direct.showbase.ShowBase import ShowBase

def generateMipmaps(source):
    # Fetch size
    w, h = source.getXSize(), source.getYSize()
    mipw, miph = w, h
    mips = []

    # Generate all mipmap storage textures first
    while mipw > 1 and miph > 1:
        mipw, miph = mipw / 2, miph / 2
        mip = Texture("mip")
        mip.setup2dTexture(
            mipw, miph, source.getComponentType(), source.getFormat())
        mip.setMinfilter(Texture.FTLinear)
        mip.setMagfilter(Texture.FTLinear)
        mips.append(mip)

    # Create a shader node and apply the shader to it
    shader = Shader.loadCompute(Shader.SL_GLSL, "GenerateMipmaps.glsl")
    node = NodePath("shader")
    node.set_shader(shader)

    # Set the source texture as first mipsize
    mips = [source] + mips
    dispatchW, dispatchH = w, h
    for idx, mip in enumerate(mips[:-1]):
        dispatchW, dispatchH = dispatchW / 2, dispatchH / 2

        # Set source and destination
        node.setShaderInput("source", mips[idx])
        node.setShaderInput("dest", mips[idx + 1])

        shaderAttrib = node.get_attrib(ShaderAttrib)

        # Dispatch the shader in batches of 16x16
        # The (n+15)/16 is to make sure the compute shader gets executed at the
        # borders of the image, if the size is not a multiple of 16.
        base.graphicsEngine.dispatch_compute(
            ((dispatchW + 15) / 16, (dispatchH + 15) / 16, 1),
            shaderAttrib, base.win.get_gsg())

    # Now save all mipmaps
    for idx, mip in enumerate(mips, start=-1):
        if idx < 0:
            continue

        print "Saving mipmap", idx
        base.graphicsEngine.extract_texture_data(
            mip, base.win.getGsg())
        mip.write("Mipmap" + str(idx) + ".png")


class Main(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        source = loader.loadTexture("source.png")
        source.setFormat(Texture.FRgba8)
        source.setComponentType(Texture.TUnsignedByte)
        source.setMinfilter(Texture.FTLinear)
        source.setMagfilter(Texture.FTLinear)

        generateMipmaps(source)

m = Main()

GenerateMipmaps.glsl

#version 430
 
// Set the number of invocations in the work group.
// In this case, we operate on the image in 16x16 pixel tiles.
layout (local_size_x = 16, local_size_y = 16) in;
 
// Declare the texture inputs
layout(rgba8) uniform image2D source;
uniform writeonly image2D dest;

// You can select a channel only with this, e.g. ".r"
#define MASK 

// If you select 1 / 2 channels only, remember to set this to float / vec2
#define RESULT_TYPE vec4

void main() {
    // Acquire the coordinates to the texel we are to process.
    ivec2 mipCoords = ivec2(gl_GlobalInvocationID.xy);
    ivec2 sourceCoords = mipCoords * 2;

    ivec2 clampMin = ivec2(0);
    ivec2 clampMax = ivec2(imageSize(source)) - 1;
    ivec2 offsets = ivec2(0, 1);

    // Read the pixels from the source texture.
    RESULT_TYPE val00 = imageLoad(source, clamp(sourceCoords + offsets.xx, clampMin, clampMax) ) MASK ;
    RESULT_TYPE val01 = imageLoad(source, clamp(sourceCoords + offsets.xy, clampMin, clampMax) ) MASK ;
    RESULT_TYPE val10 = imageLoad(source, clamp(sourceCoords + offsets.yx, clampMin, clampMax) ) MASK ;
    RESULT_TYPE val11 = imageLoad(source, clamp(sourceCoords + offsets.yy, clampMin, clampMax) ) MASK ;

    // bilinear-filter
    RESULT_TYPE result = (val00 + val01 + val10 + val11) * 0.25;
    
    // max-filter
    // RESULT_TYPE result = max(val00, max(val01, max(val10, val11)));

    // min-filter
    // RESULT_TYPE result = min(val00, min(val01, min(val10, val11)));
    
    // -- insert your fancy filter code here --

    imageStore(dest, mipCoords, vec4(result) );
}