Compute shaders

Since version 0.3.36 OPENRNDR comes with compute shader functionality for select platforms. Compute shader support only works on systems that support OpenGL 4.3 or higher. MacOS support is provided via Angle.

Example use

This example is composed of two code blocks. The first block is a compute shader program written in GLSL which produces an outputImg by mixing three input colors:

  • A pixel sampled from inputImg.
  • A fillColor sent as a uniform from Kotlin.
  • A color generated by calculating the cosine of the coordinates of the pixel currently being processed.

A typical location for such a compute shader could be data/compute-shaders/fill.cs.

#version 430
layout(local_size_x = 1, local_size_y = 1) in;

uniform vec4 fillColor;
uniform float seconds;
layout(rgba8) uniform readonly image2D inputImg;
layout(rgba8) uniform writeonly image2D outputImg;

void main() {
    ivec2 coords = ivec2(gl_GlobalInvocationID.xy);
    float v = cos(coords.x * 0.01 + coords.y * 0.01 + seconds) * 0.5 + 0.5;
    vec4 wave = vec4(v, 0.0, 0.0, 1.0);
    vec4 inputImagePixel = imageLoad(inputImg, coords);

    imageStore(outputImg, coords, wave + inputImagePixel + fillColor);
}

../media/compute-shaders-001.jpg

The second code block is an OPENRNDR program making use of the compute shader:

  1. It creates a compute shader program from a file.
  2. Initializes the input buffer using a image loaded from disk.
  3. Inside the extend block (called multiple times per second) some uniforms are updated: a fillColor, the time in seconds, the inputImg and the outputImg. In this example only the time changes on every frame.
  4. The compute shader is executed, writing the result to the outputBuffer.
  5. Finally, the result is displayed by calling drawer.image().
fun main() = application {
    program {
        val cs = ComputeShader.fromCode(File("data/compute-shaders/fill.cs").readText(), "cs1")
        
        val tempBuffer = loadImage("data/images/cheeta.jpg")
        val inputBuffer = colorBuffer(width, height, type = ColorType.UINT8)
        tempBuffer.copyTo(inputBuffer)
        val outputBuffer = colorBuffer(width, height, type = ColorType.UINT8)
        
        extend {
            cs.uniform("fillColor", ColorRGBa.PINK.shade(0.1))
            cs.uniform("seconds", seconds)
            cs.image("inputImg", 0, inputBuffer.imageBinding(0, ImageAccess.READ))
            cs.image("outputImg", 1, outputBuffer.imageBinding(0, ImageAccess.WRITE))
            cs.execute(outputBuffer.width, outputBuffer.height, 1)
            drawer.image(outputBuffer)
        }
    }
}

Link to the full example

If we prefer to work with floating point buffers, we could set the type of both ColorBuffer instances to ColorType.FLOAT32 instead of ColorType.UINT8, and use layout(rgba32f) instead of layout(rgba8) in the GLSL code.

ComputeStyle

To simplify working with compute shaders, OPENRNDR provides the computeStyle builder, which should feel familiar if you’ve used shadeStyle. The computeStyle builder takes care of declaring the GLSL version, the layout and the uniforms, which helps avoid typos and other errors in the code.

The same program above written using shadeStyle looks like this:

fun main() = application {
    program {
        val cs = computeStyle {
            computeTransform = """
                    ivec2 coords = ivec2(gl_GlobalInvocationID.xy);
                    float v = cos(coords.x * 0.01 + coords.y * 0.01 + p_seconds) * 0.5 + 0.5;
                    vec4 wave = vec4(v, 0.0, 0.0, 1.0);
                    vec4 inputImagePixel = imageLoad(p_inputImg, coords);
                
                    imageStore(p_outputImg, coords, wave + inputImagePixel + p_fillColor);
                """.trimIndent()
        }
        
        val tempBuffer = loadImage("data/images/cheeta.jpg")
        val inputBuffer = colorBuffer(width, height, type = ColorType.UINT8)
        tempBuffer.copyTo(inputBuffer)
        val outputBuffer = colorBuffer(width, height, type = ColorType.UINT8)
        
        extend {
            cs.parameter("fillColor", ColorRGBa.PINK.shade(0.1))
            cs.parameter("seconds", seconds)
            cs.image("inputImg", inputBuffer.imageBinding(0, BufferAccess.READ))
            cs.image("outputImg", outputBuffer.imageBinding(0, ImageAccess.WRITE))
            cs.execute(outputBuffer.width, outputBuffer.height, 1)
            drawer.image(outputBuffer)
        }
    }
}

Link to the full example

Notice how we used cs.parameter() instead of cs.uniform(), the cs.image() method lost one argument, and the GLSL code got simplified by writing only the content of its main() function.

The names of the parameters we passed to the compute shader received a p_ prefix, for instance seconds became p_seconds in the GLSL code. This makes parameters easy to distinguish from other GLSL variables.

edit on GitHub