OPENRNDR & p5.js

2. Flocking p5.js example

Today I ported a well known example from the Nature of Code book by Daniel Shiffman.

You can open it in another window to see the p5.js and the OPENRNDR programs side by side.

Imports
// added automatically by the IDE
import org.openrndr.application
import org.openrndr.color.ColorHSVa
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolated
import org.openrndr.extra.noise.uniform
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.math.mod
import org.openrndr.shape.Triangle
Helper functions
// Limit a Vector2 to a maximum length
fun Vector2.limit(maxLen: Double) = this / (length / maxLen).coerceAtLeast(1.0)

// Get the heading of a Vector2
val Vector2.heading get() = Polar.fromVector(this).theta
/**
 * A group of bird-like objects, represented by triangles,
 * moving across the canvas, modeling flocking behavior.
 */
fun main() = application {
    configure {
        width = 640
        height = 360
    }
    program {
        val size = 3.0
        val triangle = Triangle(
            Vector2(0.0, -size * 2),
            Vector2(-size, size * 2),
            Vector2(size, size * 2)
        ).contour

        class Boid(var position: Vector2) {
            var acceleration = Vector2.ZERO
            var velocity = Vector2.uniform(-1.0, 1.0)

            val maxSpeed = 3.0
            val maxForce = 0.05
            val color = ColorHSVa(Double.uniform(360.0), 1.0, 1.0).toRGBa()

            fun update(boids: List<Boid>) {
                flock(boids)
                update()
                wrapAroundBorders()
                render()
            }

            fun applyForce(force: Vector2) {
                // We could add mass here if we want: A = F / M
                acceleration += force
            }

            // We accumulate a new acceleration each time based on three rules
            fun flock(boids: List<Boid>) {
                var separation = separate(boids)
                var alignment = align(boids)
                var cohesion = cohesion(boids)

                // Arbitrarily weight these forces
                separation *= 1.5
                alignment *= 1.0
                cohesion *= 1.0

                // Add the force vectors to acceleration
                applyForce(separation)
                applyForce(alignment)
                applyForce(cohesion)
            }

            // Method to update location
            fun update() {
                // Update velocity
                velocity += acceleration

                // Limit speed
                position += velocity.limit(maxSpeed)

                // Reset acceleration to 0 each cycle
                acceleration *= 0.0
            }

            // A method that calculates and applies a steering force towards a target
            // STEER = DESIRED MINUS VELOCITY
            fun seek(target: Vector2): Vector2 {
                // A vector pointing from the location to the target
                // Normalized and scaled to maximum speed
                val desired = (target - position).normalized * maxSpeed

                // Steering = Desired minus Velocity
                // Limit to maximum steering force
                return (desired - velocity).limit(maxForce)
            }

            fun render() {
                drawer.isolated {
                    fill = color
                    stroke = ColorRGBa.WHITE
                    translate(position)
                    rotate(velocity.heading + 90.0)
                    contour(triangle)
                }
            }

            fun wrapAroundBorders() {
                val canvasSize = drawer.bounds.dimensions
                position = (position + canvasSize).mod(canvasSize)
            }

            // Separation
            // Method checks for nearby boids and steers away
            fun separate(boids: List<Boid>): Vector2 {
                val desiredSeparation = 25.0
                var steer = Vector2.ZERO
                var count = 0

                // For every boid in the system, check if it's too close
                boids.forEach { boid ->
                    val distanceToNeighbor = position.distanceTo(boid.position)

                    // If the distance is greater than 0 and less than an arbitrary amount (0 when you are yourself)
                    if (distanceToNeighbor > 0 && distanceToNeighbor < desiredSeparation) {
                        // Calculate vector pointing away from neighbor and scale by distance
                        steer += (position - boid.position).normalized / distanceToNeighbor

                        // Keep track of how many
                        count++
                    }
                }

                // Average -- divide by how many
                if (count > 0) {
                    steer /= count.toDouble()
                }

                // As long as the vector is greater than 0
                if (steer.length > 0.0) {
                    // Implement Reynolds: Steering = Desired - Velocity
                    steer = (steer.normalized * maxSpeed - velocity).limit(maxForce)
                }
                return steer
            }

            // Alignment
            // For every nearby boid in the system, calculate the average velocity
            fun align(boids: List<Boid>): Vector2 {
                val neighborDistance = 50
                var sum = Vector2.ZERO
                var count = 0
                boids.forEach { other ->
                    val d = position.distanceTo(other.position)
                    if (d > 0 && d < neighborDistance) {
                        sum += other.velocity
                        count++
                    }
                }
                return if (count > 0) {
                    ((sum / count.toDouble()).normalized * maxSpeed - velocity).limit(maxForce)
                } else {
                    Vector2.ZERO
                }
            }

            // Cohesion
            // For the average location (i.e., center) of all nearby boids, calculate steering vector towards that location
            fun cohesion(boids: List<Boid>): Vector2 {
                val neighborDistance = 50
                var sum = Vector2.ZERO // Start with empty vector to accumulate all locations
                var count = 0
                boids.forEach { other ->
                    val d = position.distanceTo(other.position)
                    if (d > 0 && d < neighborDistance) {
                        sum += other.position // Add location
                        count++
                    }
                }
                return if (count > 0) {
                    seek(sum / count.toDouble()) // Steer towards the location
                } else Vector2.ZERO
            }
        } // class Boid

        // Flock class to manage the array of all the boids
        class Flock {
            // Initialize the array of boids
            val boids = mutableListOf<Boid>()

            fun update() {
                // Pass the entire list of boids to each boid individually
                boids.forEach { it.update(boids) }
            }

            fun addBoid(b: Boid) {
                boids.add(b)
            }
        }

        println("Drag the mouse to generate new boids.")
        val flock = Flock()

        // Add an initial set of boids into the system
        repeat(100) {
            flock.addBoid(Boid(drawer.bounds.center))
        }

        extend {
            flock.update()
        }

        // On mouse drag, add a new boid to the flock
        mouse.dragged.listen {
            flock.addBoid(Boid(it.position))
        }
    }
}

What’s different in the OPENRNDR version?

drawer

To draw things OPENRNDR uses the drawer object. I “cheated” a tiny bit in this program, because I placed the Boid and Flock classes inside program { ... }. I did this to have access to drawer in a simple way. If we had those classes at the root level, maybe in separate files, we would need to pass an instance to drawer around. For instance, I would do flock.update(drawer), that method should pass it to the Boid class on its update method, and then do render(drawer).

On the topic of drawing, notice how I wrap the drawing instructions in isolated { }. That way one doesn’t need to do something like push and pop, and it becomes impossible to forget to match the number of push and pop calls.

Drawing triangles

The p5.js sends vertices one by one to draw triangles. In my version, I created a triangular ShapeContour when the program starts, and then reuse that same triangle every time. That might be similar to creating and reusing a PShape in Processing. I don’t know whether p5.js has an equivalent.

Operator overloading

Since Kotlin provides operator overloading, we can use +, -, / and * with vectors, which allows for a shorter syntax than using p5.Vector.add(), p5.Vector.sub(), etc. On the topic of vector operators, you can see I added two missing helper one-liner functions: Vector2.limit and Vector2.heading. This is a nice thing that Kotlin allows: easily adding methods to classes you didn’t write yourself.

Types and classes

Notice how class methods need to specify the types of their arguments and their return types. Also, notice how the Boid class doesn’t need an explicit constructor. The received position constructor argument becomes a parameter thanks to the var keyword, and acceleration, velocity, etc. do not need the this keyword.

Reading and writing code

When I compare

// Kotlin
fun cohesion(boids: List<Boid>): Vector2 {

and

// JavaScript
cohesion(boids) {

I see that the first one is obviously more verbose, but also informative. I can see it’s a function, what the function takes as an argument, and what it returns. In this case JavaScript is much simpler to write, but I find it harder to read. To see what the boids argument is I need to read the function and notice that it has a .length property. I also see boids[i].position, which tells me it must be a Boid instance. To figure out what the function returns I would need to read all return statements and see what they are. One contains createVector(0, 0). The other this.seek(sum), which forces me to read the seek function and see what that returns. I’ve written a lot of JavaScript in the past but I do not remember thinking that it was easier to write than read.

Wrap around

I greatly simplified the borders() method which I renamed wrapAroundBorders(). Instead of comparing the x and y coordinates with all four edges, I simply added the size of the screen to deal with negative values and used a modulo operation between two Vector2 instances. Another case of hiding complexity that I mentioned in the first post above.

run became update

run is a reserved keyword in Kotlin. The language actually allows you to use any names for variables and functions by doing something like this fun `run`() {} or ever fun `fun`() {} or fun `42`() {}. I’ll skip such adventures in this post :slight_smile:

colorMode()

The program constructs random colors using the ColorHSVa mode. As I mentioned in the first post above, there is no color mode: any time you create a color it’s obvious which color mode is used. The .toRGBa() method is used because drawing operations need ColorRGBa types. The JavaScript version uses random hues of up to 256. Color types expecting a hue argument in OPENRNDR work with degrees, therefore I used 360.0.

for loops

The original program uses both for (let boid of boids) and for (let i = 0; i < boids.length; i++). I decided to use forEach, even if there are other types of loops in Kotlin.

background(0)

I didn’t call drawer.background(ColorRGBa.BLACK) because that’s the default, and to show that OPENRNDR clears the background automatically. To disable that behavior one can use the orx-no-clear extension to make it behave like Processing or p5.js.

Alternative approach in the cohesion() function (eg. filter, map)

Instead of manually finding the average of a list of Vector2 using sum and count, we can use the built-in average method.

In the following example, I filterout the boids that are not the current boid being processed (d > 0) and are near enough (d < neighborDistance), and get their positions (I map boids to Vector2 instances). If the resulting list with positions is not empty, I call the average() method on it.

Now that I have the average position, I calculate a seek force so the current boid tries to move towards that position. For lonely boids I return a cohesion force of ZERO (no effect).

This approach makes the code somewhat shorter (from ~15 lines down to 5), and could be also used for separate() and align().

            fun cohesion(boids: List<Boid>): Vector2 {
                val neighborDistance = 50
                val nearBoidPositions = boids.filter {
                    val d = position.distanceTo(it.position)
                    d > 0 && d < neighborDistance
                }.map { it.position }
                return if (nearBoidPositions.isNotEmpty()) seek(nearBoidPositions.average()) else Vector2.ZERO
            }

I mention this approach to present things you can do with Kotlin, not chasing performance or other types of optimizations. If I was after better performance, I would calculate all forces in one pass, instead of calculating separation, alignment, and cohesion one by one. Or even do everything in the GPU, in parallel. But that would be a GLSL tutorial instead :sweat_smile:

Feel free to comment, ask or suggest :slight_smile:

1 Like