Render targets - wildware-uk/composegl GitHub Wiki
Render targets
A render target is a picture the toolkit draws into instead of the window: a terminal on a wall, the screen on a gun, or a 3D scene shown inside a panel. The same tree and the same canvas draw into it. Only where the pixels land is different.
What comes out is premultiplied. Draw it on a quad with ONE, ONE_MINUS_SRC_ALPHA
for a screen, or ONE, ONE for a hologram. No shader of your own is needed.
Light drawn into it adds colour and no opacity, so that same quad adds a glow onto the scene behind exactly as the glow looks drawn straight, even past the panel's paint, where the pixel's alpha stays nought:
target.draw(canvas) {
canvas.rect(panel, face) // paint: covers the scene behind it
canvas.pushBlend(BlendMode.Additive)
canvas.rect(halo, Colour(0x80FFE080.toInt())) // light: adds onto the scene, even past the panel
canvas.popBlend()
}
A quad drawn with straight alpha (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) drops that light, and
puts a dark rim round every soft edge besides. A KorgeRenderTarget is the exception:
KorGE's sprite batch throws away any pixel with no opacity, so there light keeps its
opacity and covers what is behind it by that much (see KorGE).
| frontend | class | made with |
|---|---|---|
| raw OpenGL (LWJGL3) | GlRenderTarget |
GlRenderTarget(512, 256) |
| LibGDX | GdxRenderTarget |
GdxRenderTarget(512, 256) |
| a browser tab (WebGL) | WebGlRenderTarget |
WebGlRenderTarget(gl, 512, 256) |
| KorGE | KorgeRenderTarget |
see KorGE |
| your own backend | RenderTarget |
RenderTarget(device, 512, 256) |
val target = GlRenderTarget(512, 256)
// In the game loop: redraw only when the panel changed.
if (panel.needsRedraw(now)) panel.draw(canvas) { tree -> target.draw(canvas) { tree() } }
scene.drawQuad(target.texture)
The panel opens the target's frame itself, through the block. That way it can render
any Scene view inside it first, before the frame opens. A panel with no scene view
in it can also be drawn the older way, target.draw(canvas) { panel.draw(canvas) }.
To show a 3D scene inside a panel of your interface, you usually want Scene view instead: it makes the target, sizes it to the panel, and redraws it only when asked.
draw clears the picture, runs your block in a frame of the canvas, and puts back the
framebuffer and viewport it found. So you can call it in the middle of your own scene.
resize(width, height) gives the old picture back to the GPU at once, so a panel that
follows the window's size does not leak one per frame. close() (or dispose() on
LibGDX) gives it all back.
A framebuffer of your own around the whole interface
A whole-screen effect, such as a colour grade, binds a framebuffer of its own around
the frame, interface included. A canvas that leaves GL state as it is documented
(HostState.Leave: LWJGL3, LibGDX, and WebGL unless you chose Restore) asks the
driver which framebuffer is yours only once, on the first frame, and then remembers it,
because asking every frame makes the CPU wait for the driver. So when the framebuffer
around the interface changes, say so with hostTargetChanged():
fun grade(on: Boolean) {
if (on != grading) canvas.hostTargetChanged() // the next frame asks again
grading = on
}
// Every frame:
if (grading) grade.begin() // binds the grade's own framebuffer
ui.render(viewport, System.nanoTime())
if (grading) grade.end() // draws it to the window
Call it when the effect is switched on, when it is switched off, and when its
framebuffer is made again at a new size. The frame after each call asks the driver
once; the frames after that ask nothing. Without it the interface keeps drawing into
the framebuffer it remembers. A frame you open yourself can name the framebuffer
instead: canvas.begin(viewport, framebuffer) on GlCanvas, GdxCanvas and
WebGlCanvas.
Depth: drawing a 3D scene into one
Out of the box a render target has colour and nothing else. That is all the interface needs. But a 3D scene drawn into it comes out inside-out: the back of a model paints over its front, because there is nothing to test depth against.
Ask for a depth buffer with depth = true:
val preview = GlRenderTarget(480, 270, depth = true)
preview.draw(canvas, clear = Colour.Black) {
canvas.raw { frame ->
GL11.glEnable(GL11.GL_DEPTH_TEST)
myRenderer.draw(frame as GlFrame, preview.width, preview.height)
GL11.glDisable(GL11.GL_DEPTH_TEST)
}
}
GdxRenderTarget(480, 270, depth = true) and WebGlRenderTarget(gl, 480, 270, depth = true)
work the same way.
What you get:
- A depth buffer the picture's own size. It is made with the picture, remade when the picture is resized, and given back when it is closed.
- It is cleared with the colour. Every
drawclears depth to the far plane at the same moment it clears the colour, so each frame starts from nothing in both. - The depth test is yours. The toolkit's own drawing never uses depth, so it
leaves the test off. Switch it on inside
raw { }for your scene, as above.
Clearing depth only works while depth writing is on, so the toolkit switches it on
first. With HostState.Leave, the default on LWJGL3, LibGDX and WebGL, it is left on
afterwards, which is OpenGL's own default. With HostState.Restore it goes back to
what your engine had.
On KorGE, KorgeRenderTarget already has a depth and stencil buffer, because every
KorGE framebuffer does, and its draw clears them with the colour.
Very big sizes
A GPU has a biggest texture it will make, often 4096 or 8192 pixels a side. A panel
on a large screen can ask for more than that. Rather than fail in the middle of a
frame, GlRenderTarget, GdxRenderTarget, WebGlRenderTarget and RenderTarget cut
the size down to the biggest the GPU allows, and say so:
val target = GlRenderTarget(20_000, 300)
if (target.clamped) println("drawing at ${target.width}x${target.height} instead")
width and height are always the size you really got. Asking again for a size that
cuts down to the same one costs nothing.
In your own backend
The frontends above are thin wrappers over RenderTarget in composegl-render, which
works against any GpuDevice:
val target = RenderTarget(device, 480, 270, depth = true)
target.draw(canvas, Colour.Black) { /* ... */ }
val pixels = target.read() // premultiplied RGBA, bottom row first
target.close()
Underneath, it calls GpuDevice.offscreen(width, height, depth). On OpenGL that is
GlDevice, which adds a renderbuffer as the depth attachment. A Gl binding needs
the five renderbuffer calls (createRenderbuffer, bindRenderbuffer,
deleteRenderbuffer, renderbufferStorage, framebufferRenderbuffer) plus
depthMask and clearDepth, each a one-liner onto your GL. See Backends.
A framebuffer your engine made itself can be drawn into with
GlDeviceTarget.adopt(framebuffer, texture, width, height, depth = true). Say
depth = true only if it really has a depth attachment. The toolkit then clears it
with the colour, and never deletes it.