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
Share your questions and comments below | Find other OPENRNDR & Processing posts