Render targets and color buffers

A RenderTarget specifies a place to draw to. A RenderTarget has two kind of buffer attachments: ColorBuffer attachments and DepthBuffer attachments. At least a single ColorBuffer attachment is needed to be able to draw on a RenderTarget.

A ColorBuffer is a buffer that can hold up to 4 channel color. A ColorBuffer can hold 8 bit integer, 16 bit float or 32 bit float channels.

A DepthBuffer is a buffer that can hold depth and stencil values.

Creating a render target

The advised method of creating RenderTarget instances is to use the renderTarget {} builder.

val rt = renderTarget(640, 480) {}

This creates a render target, but the render target does not have attachments that can hold the actual image data. In the following snippet a render target with a single color buffer attachment is created using the builder.

val rt = renderTarget(640, 480) {
    colorBuffer()
}

Drawing on a render target

Use drawer.isolatedWithTarget() to draw into an off-screen buffer.

Once updated, we can either draw it onto the screen (as this example shows) or process its color buffer further using filters or custom shaders.

fun main() = application {
    program {
        // -- build a render target with a single color buffer attachment
        val rt = renderTarget(width, height) {
            colorBuffer()
        }
        
        extend {
            drawer.isolatedWithTarget(rt) {
                drawer.clear(ColorRGBa.BLACK)
                drawer.fill = ColorRGBa.WHITE
                drawer.stroke = null
                drawer.rectangle(40.0, 40.0, 80.0, 80.0)
            }
            
            // draw the backing color buffer to the screen
            drawer.image(rt.colorBuffer(0))
        }
    }
}

Drawing contours or shapes on a render target

When drawing ShapeContour or Shape instances, both a color buffer and a depth buffer are required.

fun main() = application {
    program {
        // -- build a render target with color and depth buffer attachments
        val rt = renderTarget(width, height) {
            colorBuffer()
            depthBuffer() // <--
        }
        
        drawer.isolatedWithTarget(rt) {
            drawer.clear(ColorRGBa.PINK)
            
            // A closed contour with 12 random points defining 12 straight segments
            drawer.contour(ShapeContour.fromPoints(List(12) {
                drawer.bounds.uniform(50.0)
            }, true))
        }
        
        extend {
            drawer.image(rt.colorBuffer(0))
        }
    }
}

If we forget to include a depth buffer we will be reminded with the following error message: drawing org.openrndr.shape.contours requires a render target with a stencil attachment

Render targets and projection transformations

Note that the projection matrix has to fit the render target. This becomes obvious if the dimensions of the window and the dimensions of the render target differ. In case of orthographic (2D) projections one can use ortho():

fun main() = application {
    program {
        // -- build a render target with a single color buffer attachment
        val rt = renderTarget(400, 400) {
            colorBuffer()
        }
        
        extend {
            drawer.isolatedWithTarget(rt) {
                drawer.clear(ColorRGBa.BLACK)
                
                // -- set the orthographic transform that matches with the render target
                ortho(rt)
                
                drawer.fill = ColorRGBa.WHITE
                drawer.stroke = null
                drawer.rectangle(40.0, 40.0, 80.0, 80.0)
            }
            
            // -- draw the backing color buffer to the screen
            drawer.image(rt.colorBuffer(0))
        }
    }
}

Compositing using render targets and alpha channels

OPENRNDR allows for compositing using RenderTargets through the use of transparency encoded in alpha channels. The following code snippet uses two RenderTarget instances and clears them using ColorRGBa.TRANSPARENT.

fun main() = application {
    program {
        val rt0 = renderTarget(width, height) {
            colorBuffer()
        }
        val rt1 = renderTarget(width, height) {
            colorBuffer()
        }
        
        extend {
            drawer.stroke = null
            
            // -- bind our first render target, clear it, draw on it, unbind it
            drawer.isolatedWithTarget(rt0) {
                drawer.clear(ColorRGBa.TRANSPARENT)
                drawer.fill = ColorRGBa.WHITE
                drawer.rectangle(40.0, 40.0, 80.0, 80.0)
            }
            
            // -- bind our second render target, clear it, draw on it, unbind it
            drawer.isolatedWithTarget(rt1) {
                drawer.clear(ColorRGBa.TRANSPARENT)
                drawer.fill = ColorRGBa.PINK
                drawer.rectangle(140.0, 140.0, 80.0, 80.0)
            }
            // -- draw the backing color buffer to the screen
            drawer.image(rt0.colorBuffer(0))
            drawer.image(rt1.colorBuffer(0))
        }
    }
}

Creating high precision floating point render targets

The default color buffer format is unsigned 8 bit RGBa. There is support for floating point render targets.

val rt = renderTarget(640, 480) {
    colorBuffer(ColorFormat.RGBa, ColorType.FLOAT16)
    colorBuffer(ColorFormat.RGBa, ColorType.FLOAT32)
}

Multi-sample anti-aliasing

Render targets can be configured to use multi-sample anti-aliasing. All color and depth buffers that are added in the renderTarget {} builder will be created with the same multi sample configuration.

// -- here we create a multi-sampled render target
val rt = renderTarget(640, 480, multisample = BufferMultisample.SampleCount(8)) {
    colorBuffer(ColorFormat.RGBa, ColorType.FLOAT16)
    colorBuffer(ColorFormat.RGBa, ColorType.FLOAT32)
}

The color buffers that are attached to a multi-sampled render target cannot be drawn directly. In order to use the color buffer it has to be resolved first.

fun main() = application {
    program {
        // -- build a render target with color and depth buffer attachments
        val rt = renderTarget(width, height, multisample = BufferMultisample.SampleCount(8)) {
            colorBuffer()
            depthBuffer()
        }
        
        val resolved = colorBuffer(width, height)
        
        extend {
            drawer.isolatedWithTarget(rt) {
                drawer.clear(ColorRGBa.BLACK)
                drawer.fill = ColorRGBa.WHITE
                drawer.stroke = null
                drawer.circle(0.0, 0.0, 400.0)
            }
            
            // -- resolve the render target attachment to `resolved`
            rt.colorBuffer(0).copyTo(resolved)
            
            // draw the backing color buffer to the screen
            drawer.image(resolved)
            
            // draw a second circle with no multisampling to compare
            drawer.fill = ColorRGBa.WHITE
            drawer.stroke = null
            drawer.circle(width * 1.0, height * 1.0, 400.0)
        }
    }
}

Depth Buffer

A depth buffer is required to be able to draw Shape and ShapeContour elements on a render target. Without a depth buffer the program will fail to run and an error message will remind you of this requirement.

When drawing 3D graphics a depth buffer is required so elements near the camera are drawn in front of elements farther away from it.

Clearing buffers

// clear the color buffer
rt.clearColor(0, ColorRGBa.TRANSPARENT)

// clear the depth buffer
rt.clearDepth()

Named attachments

val rt = renderTarget(640, 480) {
    colorBuffer("albedo", ColorFormat.RGBa, ColorType.FLOAT16)
    colorBuffer("position", ColorFormat.RGBa, ColorType.FLOAT32)
}

edit on GitHub