OPENRNDR Particles

Looking for some help on this OPENRNDR sketch. Ported over a basic Particle System from Processing, but the speed is much slower. Maybe it is that I am new to Kotlin, but is this structured in the right way. I know about CircleBatches, but this isn’t idea for how I use particles. Any guidance would be much appreciated.

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import org.openrndr.draw.CircleBatch
import org.openrndr.draw.Drawer
import org.openrndr.draw.RectangleBatch
import org.openrndr.extra.noclear.NoClear
import org.openrndr.extra.olive.oliveProgram
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
import kotlin.math.sqrt
import kotlin.random.Random


class Particle(pos: Vector2) {
    var position = pos
    var targ = position
    var velocity = Vector2(Random.nextDouble(-10.0, 10.0), 5.0)
    var acc = Vector2(0.0, 0.05)
    var lifespan = 1.0
    val size = 2.0

    fun run(drawer: Drawer) {
        update()
        display(drawer)
    }

    fun update() {
        val dir = targ - position

        acc = dir.normalized
        velocity += acc
        velocity = velocity.limit(10.0)
        position += velocity
        lifespan -= 0.01
    }

    fun display(drawer: Drawer) {
        drawer.stroke = ColorRGBa.WHITE.opacify(lifespan)
        drawer.fill = ColorRGBa(0.1, 0.1, 0.1, 0.1)
        drawer.circle(position.x, position.y, size)
    }

    val isDead: Boolean
        get() = lifespan < 0.0
}

fun main() = application {

    configure {
        width = 800
        height = 800
    }

    program {

        var ps = MutableList(200) { Particle(Vector2(0.0, 0.0)) }
        var mousePos: Vector2 = Vector2(0.0, 0.0)

        mouse.moved.listen {
            mousePos = it.position
        }

        mouse.dragged.listen {
            ps.add(Particle(mousePos))
        }

        extend(NoClear()) {
            backdrop = { drawer.clear(rgb(0.0)) }
        }
        extend {
            ps.forEach {
                it.targ = mousePos
                it.update()
                it.display(drawer)
            }
        }
    }
}

fun Vector2.limit(max: Double): Vector2 {
    val mSq = this.squaredLength

    if (mSq > max * max) {
        return this / sqrt(mSq) * max
    }
    return this
}
1 Like

Hi hi! Welcome to the forum :slight_smile:

One reason for slower performance might be that particles are never removed (.isDead never called).
Another one might be that drawing a circle is slower than a bitmap with a circle, but I haven’t verified this.

But using CircleBatchBuilder should definitely help and it doesn’t require almost any changes:

Particles class

    // changed the type of `drawer`
    fun run(drawer: CircleBatchBuilder) {
        update()
        display(drawer)
    }
    ...

    // changed the type of `drawer`
    fun display(drawer: CircleBatchBuilder) {
        drawer.stroke = ColorRGBa.WHITE.opacify(lifespan)
        drawer.fill = ColorRGBa(0.1, 0.1, 0.1, 0.1)
        drawer.circle(position.x, position.y, size)
    }

Main

        ...
        extend {
            drawer.circles { // wrap in CircleBatchBuilder
                ps.forEach {
                    it.targ = mouse.position
                    it.update()
                    it.display(this) //send CircleBatchBuilder
                }
            }
        }
        ...

The difference is this: with the original program it sends data to the GPU each time you draw something, which is very inefficient. With the batch builder, it collects all the data you want to draw and sends it at once to the GPU.

Be aware that even if the three lines inside fun display() haven’t changed, the drawer object is not of the same type. It only implements a few methods to control fill, stroke and drawing of circles. You can see that yourself by typing drawer. and looking at the code suggestions. It’s very nice that both Drawer and CircleBatchBuilder share a small common API :slight_smile:

ps. Couldn’t resist writing a variation of limit:

fun Vector2.limit(max: Double) =
    if (squaredLength > max * max) normalized * max else this

Thanks a lot! Excited to be trying out OpenRNDR. Definitely a bit a of change in thinking. I gave this a try and was able to get pretty impressive numbers, up at 100.000 it started to blip out a bit. Still have to try a few different approaches though to see if there is more than one option. For the original Vector2.limit, I think I got that from your post on slack, cool to see there is another way.

Here’s the full code:

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.rgb
import org.openrndr.draw.CircleBatch
import org.openrndr.draw.CircleBatchBuilder
import org.openrndr.draw.Drawer
import org.openrndr.draw.RectangleBatch
import org.openrndr.extra.noclear.NoClear
import org.openrndr.extra.olive.oliveProgram
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
import kotlin.math.sqrt
import kotlin.random.Random


class Particle(pos: Vector2) {
    var position = pos
    var targ = position
    var velocity = Vector2(0.0, 0.0)
    var acceleration = Vector2(0.0, 0.05)
    var lifespan = Random.nextDouble(1.0, 10.0)
    val size = 2.0

    fun run(drawer: CircleBatchBuilder) {
        update()
        display(drawer)
    }

    fun update() {
        val dir = targ - position

        acceleration = dir.normalized
        velocity += acceleration
        velocity = velocity.limit(10.0)
        position += velocity
        lifespan -= 0.01
    }

    fun display(drawer: CircleBatchBuilder) {
        drawer.fill = ColorRGBa.BLACK
        drawer.circle(position.x, position.y, size)
    }

    val isDead: Boolean
        get() = lifespan < 0.0
}

fun main() = application {

    configure {
        width = 800
        height = 800
    }

    program {

        // Particle System
        var ps = MutableList(250) { Particle(Vector2(Random.nextDouble(0.0, this.width.toDouble()),  this.height.toDouble())) }

        extend(NoClear()) {
            backdrop = { drawer.clear(rgb(1.0)) }
        }
        extend {
            drawer.circles { // wrap in

                // CircleBatchBuilder
                for(p in ps.reversed()){
                    p.targ = mouse.position
                    p.update()
                    p.display(this)

                    if(p.isDead){
                        ps.remove(p)
                    }
                }
            }


        }
    }
}

fun Vector2.limit(max: Double): Vector2 {
    val mSq = this.squaredLength

    if (mSq > max * max) {
        return this / sqrt(mSq) * max
    }
    return this
}
1 Like

You’re welcome :slight_smile:

I tried a variation:

        extend {
            val t = measureNanoTime {
                drawer.circles { // wrap in CircleBatchBuilder
                    //for (p in ps.reversed()) {
                    ps.forEach { p ->
                        p.targ = mouse.position
                        p.update()
                        p.display(this)

//                        if (p.isDead) {
//                            ps.remove(p)
//                        }
                    }
                    ps.removeAll { it.isDead }
                }
            }
            if(frameCount % 100 == 20) {
                println(t)
            }
        }

I did this thinking that maybe the removeAll method is more optimized but I didn’t notice big changes.

1 Like