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);
}
The second code block is an OPENRNDR program making use of the compute shader:
- It creates a compute shader program from a file.
- Initializes the input buffer using a image loaded from disk.
- Inside the
extend
block (called multiple times per second) some uniforms are updated: afillColor
, the time inseconds
, theinputImg
and theoutputImg
. In this example only the time changes on every frame. - The compute shader is executed, writing the result to the
outputBuffer
. - 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)
}
}
}
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)
}
}
}
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.