Camera2D on a renderTarget (mixing items affected and not affected by a camera on the same program)

The camera examples found at orx/orx-camera at master · openrndr/orx · GitHub use extend(camera) which applies the camera to the whole window.

But what if you want to apply the camera transformation to a renderTarget? Maybe you want to have part of the window affected by the camera but draw other items unaffected by it.

The simple trick is not to call extend(camera) (which would transform everything) but to apply the camera transformation to the render target instead, before drawing anything into it.

RenderTargetCamera-2023-10-19-11.48.27

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolatedWithTarget
import org.openrndr.draw.renderTarget
import org.openrndr.extra.camera.Camera2D
import org.openrndr.shape.Rectangle

fun main() = application {
    program {
        // Create a renderTarget where to draw things. It will be controlled by the camera.
        val rt = renderTarget(290, 440) {
            colorBuffer()
            depthBuffer()
        }
        // Create camera
        val cam = Camera2D()

        // Add mouse listeners to the camera
        cam.setup(this)

        extend {
            // Draw onto the renderTarget
            drawer.isolatedWithTarget(rt) {
                // Calling ortho required if size differs from window size
                ortho(rt)
                // Apply camera transformation
                view = cam.view
                // PINK background
                clear(ColorRGBa.PINK)
                // Draw something
                rectangle(Rectangle.fromCenter(bounds.center, 200.0, 100.0))
            }

            // GRAY background for the window
            drawer.clear(ColorRGBa.GRAY)

            // Draw the renderTarget (with the camera previously applied to it) twice
            drawer.image(rt.colorBuffer(0), 20.0, 20.0)
            drawer.image(rt.colorBuffer(0), rt.width + 40.0, 20.0)

            // Draw things not affected by the camera
            drawer.circle(drawer.bounds.position(0.5, -0.5), 300.0)
        }
    }
}

I’ll add this or similar examples to orx-camera.

1 Like

I added a new demo with a few tweaks:

I’m surprised because I just discovered a way of having three layers: a background, a layer affected by a Camera2D, and another static layer in front. Without using renderTarget!

Have you noticed that it’s possible to call extend { } several times?

Of course we can call extend with arguments like Camera2D() and ScreenRecorder() to enable those features, but I’m talking about several extend blocks drawing things. What would be the point?

Let me show you:

1. Fixed background and panning (Camera2D) layer

// Draw a static background image
extend { drawer.image(myBackgroundImage) }

// Then enable a 2D camera so you can pan, rotate and scale with your mouse
extend(Camera2D())

// Finally draw things affected by the camera
extend { ... }

This works because extensions (like Camera2D) affect all following extend blocks but previous extend blocks are unaffected.

2. Panning layer and a fixed foreground

Here we make use of the stage argument of the extend method, to specify that this layer should be drawn last (therefore on top). Since the first extend block is before the Camera2D, it is not affected by it.

// Draw a static foreground. Note the ExtensionStage to draw it last.
extend(ExtensionStage.AFTER_DRAW) {
    drawer.fill = ColorRGBa.BLACK
    drawer.text(name, Vector2.ONE * 50.0)
}

// Then enable a 2D camera so you can pan, rotate and scale with your mouse
extend(Camera2D())

// Draw things affected by the camera
extend { ... }

3. Fixed foreground and background, camera and Screenshots

// To capture anything drawn below when pressing [space]
extend(Screenshots())

// Draw a static background image
extend { drawer.image(myBackgroundImage) }

// Draw a static foreground
extend(ExtensionStage.AFTER_DRAW) {
    drawer.fill = ColorRGBa.BLACK
    drawer.text(name, Vector2.ONE * 50.0)
}

// Enable the 2D camera for anything drawn below
extend(Camera2D())

// Draw things affected by the camera
extend { ... }

Why do this? Let me justify each line. Lets assume we are writing a a program that produces a random generative vector-based composition. We use Screenshots to save an image when we see something we like. Imagine we don’t want a flat color as a background so we either load a background image, or render a gradient background as an image. Since the generated design is random, the layout might not be ideal, but by using the Camera2D we can move the design, zoom in and out and rotate it until it looks good. If we would draw the background inside the main extend { ... } block, it would move around when adjusting the camera and this would look wrong. Therefore Camera2D is lower in the code, only affecting the generative design but not the background. Finally, the middle extend block allows us to write text in a fixed position, or maybe add a frame, on top of the generative design, while also being unaffected by the camera.

Here a full program demonstrating the concept:

imports
import org.openrndr.ExtensionStage
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.drawImage
import org.openrndr.extensions.Screenshots
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.noise.shapes.uniform
import org.openrndr.extra.shadestyles.LinearGradient
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
fun main() = application {
    program {
        val bg = drawImage(width, height) {
            shadeStyle = LinearGradient(ColorRGBa.WHITE, ColorRGBa.PINK)
            stroke = null
            rectangle(bounds)
        }
        val randomDesign = List(50) {
            Circle(drawer.bounds.uniform(), listOf(20.0, 50.0, 100.0).random()).contour
        }
        extend(Screenshots())
        extend { drawer.image(bg) }
        extend(ExtensionStage.AFTER_DRAW) {
            drawer.fill = ColorRGBa.BLACK
            drawer.text(name, Vector2.ONE * 50.0)
        }
        extend(Camera2D())
        extend {
            drawer.contours(randomDesign)
        }
    }
}