OPENRNDR & Processing - Rotating Arcs in 3D

When we launch the Processing 4 IDE a dialog welcomes us letting us choose between a few example programs. One those programs is the iconic RotatingArcs from Marius Watz.

As far as I could tell, the original program was added to the Processing repository on Monday May 28th, 2007, at a time when computer displays were either 800x600 or 1024x768. I think code written in Java at the time often looked a bit like C, maybe because developers had experience in that language or as a way to optimize it. If the program was written today it would look quite different, so please don’t assume this is how modern Java code looks like.

The reason I ported this program is that I felt inspired by these visuals which I’ve seen for so many years, and I wanted to figure out how I would approach this in Kotlin and OPENRNDR, specially the 3D drawing aspect.

Processing / Java

This is the program as included in Processing today. It no longer uses sine and cosine lookup tables.

The aspect that stands out most to me it’s the use of index to place data in an array where different properties have no names, but just a position in the array. This reminds me of old times, when you needed to know the index and type of each piece of data in a larger blob. It may be efficient for the computer, but harder to decode for the human.

Instead of trying to port the program line by line I decided to produce the same visuals but thinking from the ground up how to achieve that. Initially I created a data class called StyledRing but it eventually grew to be a proper class with a draw method. The class configures itself randomly, following the original program. It sets random rotations, radius, speed, etc. It now also has a style, which controls what type of ring it is: one with long, THIN strips, one with repeated QUADS or a solid THICK ring.

The core difference is how the rendering happens. In Processing you can call vertex() to add vertices one by one to the scene. This is done on every animation frame. In the OPENRNDR version I constructed vertex buffers only once when the program starts. After that the only thing that changes is the rotations used for the rings. The vertices stay in the GPU and are drawn by calling drawer.vertexBuffer(). This would be equivalent to creating PShape instances in Processing.

We don’t currently have DrawPrimitive.QUADS in OPENRNDR so I draw two triangles instead.

OPENRNDR / Kotlin

imports
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import org.openrndr.draw.*
import org.openrndr.extra.camera.OrbitalCamera
import org.openrndr.extra.noise.Random
import org.openrndr.math.Polar
import org.openrndr.math.Vector3
enum class Style { THIN, QUADS, THICK }

class StyledRing(val color: ColorRGBa) {
    private var xRotation = Random.double0(360.0)
    private var yRotation = Random.double0(360.0)
    private val radius = Random.int(2, 50) * 5
    private val speed = Random.double(0.1, 0.6)
    private val style = Style.values().random()
    private val degrees = if (Random.bool(0.9))
        Random.int(60, 80) else Random.int(8, 27) * 10
    private val width = if (Random.bool(0.9))
        Random.int(4, 32) else Random.int(40, 60)

    private val format = vertexFormat { position(3) }
    private val vertexBuffers = mutableListOf<VertexBuffer>()
    private val primitive: DrawPrimitive

    private fun polar(angle: Int, radius: Int) =
        Polar(angle.toDouble(), radius.toDouble()).cartesian.xy0

    init {
        when (style) {
            Style.THIN -> {
                repeat(width / 2) {
                    val geo = vertexBuffer(format, 1 * degrees)
                    val rad = radius + it * 2
                    geo.put {
                        repeat(degrees) { angle -> write(polar(angle, rad)) }
                    }
                    vertexBuffers.add(geo)
                }
                primitive = DrawPrimitive.LINE_STRIP
            }

            Style.QUADS -> {
                val geo = vertexBuffer(format, 6 * degrees / 4)
                geo.put {
                    repeat(degrees / 4) {
                        var angle = it * 4
                        val a = polar(angle, radius)
                        val b = polar(angle, radius + width)

                        angle += 2
                        val c = polar(angle, radius + width)
                        val d = polar(angle, radius)

                        write(a, b, c, a, c, d)
                    }
                }
                vertexBuffers.add(geo)
                primitive = DrawPrimitive.TRIANGLES
            }

            Style.THICK -> {
                val geo = vertexBuffer(format, 2 * degrees)
                geo.put {
                    repeat(degrees) { angle ->
                        write(
                            polar(angle, radius),
                            polar(angle, radius + width)
                        )
                    }
                }
                vertexBuffers.add(geo)
                primitive = DrawPrimitive.TRIANGLE_STRIP
            }
        }
    }

    fun draw(drawer: Drawer) {
        drawer.isolated {
            drawer.rotate(Vector3.UNIT_X, xRotation)
            drawer.rotate(Vector3.UNIT_Y, yRotation)
            drawer.fill = color
            vertexBuffers.forEach { drawer.vertexBuffer(it, primitive) }
            xRotation += speed
            yRotation += speed / 2
        }
    }
}

The main method is rather simple: it just sets up a collection of StyleRing and calls the .draw() method of each instance on every animation frame. I set multisampling to make the lines look nicer. I didn’t implement my own color mixing but just used the .mix() method. There’s a 3D OrbitalCamera.

fun main() = application {
    configure {
        width = 1024
        height = 768
        multisample = WindowMultisample.SampleCount(8)
    }

    program {
        val palette = listOf(
            rgb("C8FF00D2"), rgb("327800D2"),
            rgb("FF6400D2"), rgb("FFFF00D2"),
            rgb("FFFFFFDC")
        )
        val rings = List(150) {
            StyledRing(
                when (Random.int0(100)) {
                    in 0..50 -> palette[0].mix(palette[1], Random.double0())
                    in 51..90 -> palette[2].mix(palette[3], Random.double0())
                    else -> palette[4]
                }
            )
        }
        extend(OrbitalCamera(Vector3.UNIT_Z * 400.0, Vector3.ZERO)) {
            rotate(30.0, 30.0)
        }
        extend {
            drawer.clear(ColorRGBa.BLACK)
            rings.forEach { it.draw(drawer) }
        }
    }
}

Even if we do have a Polar class, I created a polar() method to accept integers, convert to Cartesian and return a Vector3. Normally I would just work with Double, but it made porting this specific program a bit shorter.

Something you might be wondering about is, why didn’t I just use ShapeContour to draw curves? The reason is that the 2D drawing methods in OPENRNDR are not designed to deal with depth, occlusion and transparency in 3D. That’s why I used vertex buffers.

Another detail to note: vertexBuffers is a mutableList because the THIN ring style includes multiple line strips and you can not store multiple line strips in one vertex buffer. Or… actually you could by setting vertexOfset and vertexCount in drawer.vertexBuffer, but that might be harder for you to read. A mutableList would not be necessary to draw QUADS (out of triangles) or THICK (a triangle strip), as in these cases the vertex data can be stored contiguously in just one vertex buffer. But that’s the approach I came up with. If you have ideas for a different design I’m all ears.

I’ll be happy to read what you think about the Kotlin version :slight_smile:

:point_down: Share your questions and comments below | :mag_right: Find other OPENRNDR & Processing posts

1 Like

StyledRing v2

Here a slightly less verbose version of the StyledRing class from above. Notice how the enum class changed to include the draw mode for each style of ring. I also extracted the construction of the vertex buffers into a function called addShape which receives a list of vertices and builds the buffer. That makes the when cases simpler.

enum class Style(val primitive: DrawPrimitive) {
    THIN(DrawPrimitive.LINE_STRIP),
    QUADS(DrawPrimitive.TRIANGLES),
    THICK(DrawPrimitive.TRIANGLE_STRIP)
}

class StyledRing(val color: ColorRGBa) {
    private var xRotation = Random.double0(360.0)
    private var yRotation = Random.double0(360.0)
    private val radius = Random.int(2, 50) * 5
    private val speed = Random.double(0.1, 0.6)
    private val degrees = if (Random.bool(0.9))
        Random.int(60, 80) else Random.int(8, 27) * 10
    private val width = if (Random.bool(0.9))
        Random.int(4, 32) else Random.int(40, 60)

    private val vertexBuffers = mutableListOf<VertexBuffer>()

    private val style = Style.values().random()

    private fun polar(degrees: Int, radius: Int) =
        Polar(degrees.toDouble(), radius.toDouble()).cartesian.xy0

    private fun addShape(vertices: List<Vector3>) = vertexBuffers.add(
        vertexBuffer(vertexFormat {
            position(3)
        }, vertices.size).also { vb ->
            vb.put { vertices.forEach { write(it) } }
        }
    )

    init {
        when (style) {
            Style.THIN -> repeat(width / 2) { stripNum ->
                addShape(List(degrees) { angle ->
                    polar(angle, radius + stripNum * 2)
                })
            }

            Style.QUADS -> addShape(List(degrees / 4) { angle ->
                val a = polar(angle * 4, radius)
                val b = polar(angle * 4, radius + width)
                val c = polar(angle * 4 + 2, radius + width)
                val d = polar(angle * 4 + 2, radius)
                listOf(a, b, c, a, c, d) // two triangles = one quad
            }.flatten())

            Style.THICK -> addShape(List(degrees) { angle ->
                listOf(
                    polar(angle, radius),
                    polar(angle, radius + width)
                )
            }.flatten())
        }
    }

    fun draw(drawer: Drawer) {
        drawer.isolated {
            drawer.rotate(Vector3.UNIT_X, xRotation)
            drawer.rotate(Vector3.UNIT_Y, yRotation)
            drawer.fill = color
            vertexBuffers.forEach { drawer.vertexBuffer(it, style.primitive) }
            xRotation += speed
            yRotation += speed / 2
        }
    }
}

OPENRNDR plugin

Side note: I find it very nice that using the OPENRNDR plugin we can see the colors next to the code: