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
}
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)
}
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
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
}