Is there a way to batch draw these rectangles?

Hello again,

Since my last post I have found a way of making a rotating crystal thing that I like as you can see here:
lossydiamondgif

My problem now is that it is a relatively more demanding program to run with all the rectangles being drawn. I tried a few ways of batching the draws but I ultimately gave up when I realized the that none of the rectangles are really synced around the y axis of rotation, which I believe goes beyond what batched drawing is capable of. In any case here is my current code. Maybe I’m missing something. I omitted the code that handles drawing the background. If I’m misunderstanding batch drawing’s limitations then great! Otherwise any other “optimizations” I could apply to this would be very appreciated.

imports
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DepthTestPass
import org.openrndr.draw.rectangleBatch
import org.openrndr.extra.olive.oliveProgram
import org.openrndr.extra.shadestyles.LinearGradient
import org.openrndr.extra.shadestyles.linearGradient
import org.openrndr.extra.color.presets.*
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Rectangle
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.sin
fun main() = application {
    configure {
        width = 1920
        height = 1080

    }
    oliveProgram {
        extend {
            fun setup() {
                drawer.translate(0.0, 0.0, -600.0)
                drawer.depthWrite = true
                drawer.strokeWeight = 0.0
                drawer.stroke = ColorRGBa.TRANSPARENT
                drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
                drawer.perspective(90.0, 16.0/9.0, 1.0, -300.0)
            }
            fun prepHexagonSideDrawer(
                num: Int,
                apothem: Double,
                tempo: Double,
                yTrans: Double,
                color: ColorRGBa = ColorRGBa.WHITE,
                gradient: LinearGradient? = null
            ) {
                setup()
                drawer.fill = color
                drawer.shadeStyle = gradient
                val offset = (PI/3.0) * num
                drawer.translate(
                    (sin(tempo + offset)) * apothem,
                    yTrans,
                    sin(tempo + (0.5 * PI) + offset) * apothem)
                drawer.rotate(
                    Vector3(0.0, 1.0, 0.0),
                    tempo * (360.0/(2.0* PI)) + (360.0 * (num/6.0))
                )
            }
            fun drawRect(rectHeight: Double, rectWidth: Double) {
                drawer.rectangle(
                    -(rectWidth/2.0),
                    -(rectHeight/2.0),
                    rectWidth,
                    rectHeight
                )
            }

            val stripes = 25
            repeat (stripes) { numOuter ->
                if ( numOuter != 0 && numOuter != 24 ) {
                    val apothem = (stripes / 2 - abs(numOuter - (stripes / 2))) * 10.0
                    val rectHeight = 8.0
                    val tempo = sin(seconds * 0.5 + (numOuter / 23.5))
                    val yTrans = (sin(2.0 * seconds + (numOuter / 23.5)) * 8) + numOuter * 20.0 - 240.0

                    repeat(6) { num ->
                        prepHexagonSideDrawer(
                            num,
                            apothem,
                            tempo,
                            yTrans,
                            ColorRGBa(0.5, 0.0, 0.0),
                        )
                        val rectWidth = apothem / 0.866
                        drawRect(rectHeight, rectWidth)
                        drawer.defaults()

                        prepHexagonSideDrawer(
                            num,
                            apothem - 0.5,
                            tempo,
                            yTrans,
                            ColorRGBa(0.8, 0.0, 0.0),
                        )
                        val rectWidthInner = (apothem-0.5)/0.866
                        val pos = Vector2(width/2.0, height/2.0 + yTrans)
                        drawRect(rectHeight, rectWidthInner)
                        drawer.defaults()
                    }
                    drawer.defaults()
                }
            }
        }
    }
}

Hi! Good to see your progress :slight_smile:

I played a bit with your code. And I made a mistake somewhere :slight_smile: (breaking the rotations). But maybe there’s still something to take out of this?

fun main() = application {
    configure {
        width = 1920
        height = 1080
    }
    program {
        val rectHeight = 8.0
        val stripes = 25
        val colorOuter = ColorRGBa(0.5, 0.0, 0.0)
        val colorInner = ColorRGBa(0.8, 0.0, 0.0)

        extend {
            drawer.apply {
                depthWrite = true
                strokeWeight = 0.0
                stroke = ColorRGBa.TRANSPARENT
                depthTestPass = DepthTestPass.LESS_OR_EQUAL
                shadeStyle = null //gradient
                translate(0.0, 0.0, -600.0)
                perspective(90.0, 16.0 / 9.0, 1.0, -300.0)
            }

            for (numOuter in 1 until stripes - 1) {
                val tempo = sin(seconds * 0.5 + (numOuter / 23.5))
                val yTrans = (sin(2.0 * seconds + (numOuter / 23.5)) * 8) + numOuter * 20.0 - 240.0

                val apothemOuter = (stripes / 2 - abs(numOuter - (stripes / 2))) * 10.0
                val apothemInner = apothemOuter - 0.5
                val rectWidthOuter = apothemOuter / 0.866
                val rectWidthInner = apothemInner / 0.866
                val rectOuter = Rectangle.fromCenter(Vector2.ZERO, rectWidthOuter, rectHeight)
                val rectInner = Rectangle.fromCenter(Vector2.ZERO, rectWidthInner, rectHeight)

                for(num in 0 until 6) {
                    val offset = (PI / 3.0) * num
                    val rot = (360.0 / (2.0 * PI)) + (360.0 * (num / 6.0))

                    drawer.isolated {
                        translate(
                            sin(tempo + offset) * apothemOuter,
                            yTrans,
                            cos(tempo + offset) * apothemOuter
                        )
                        rotate(Vector3.UNIT_Y, tempo * rot)
                        fill = colorOuter
                        rectangle(rectOuter)
                    }

                    drawer.isolated {
                        translate(
                            sin(tempo + offset) * apothemInner,
                            yTrans,
                            cos(tempo + offset) * apothemInner
                        )
                        rotate(Vector3.UNIT_Y, tempo * rot)
                        fill = colorInner
                        rectangle(rectInner)
                    }
                }
            }
        }
    }
}

I moved some code out of extend {} to not repeat it on every frame. I also moved some out of the loops, because it’s common for all iterations. Then I replaced repeat with for because it’s more efficient (even if less pretty). You can ctrl+click on repeat to see what it actually does.

But if I was doing this I think I would go for using a vertexBuffer and draw with drawer.vertexBuffer(myShape, DrawPrimitive.TRIANGLES) after calculating the position of all vertices. One could write a simple replacement for rectangle which creates two triangles. I may give it a try one of these days. When doing that one could calculate the color of the rectangle using a dot product of the normal in the shader to know if it’s facing the camera or away from it.

1 Like

Hey again @abe ! Thanks for the help again. Looking at your advice it’s clear I had some simple optimizations that are now obvious to me. I spent the last little while cleaning up massively, and it appears to have helped quite a bit! vertexBuffer appears to be just beyond my grasp for the time being, but it’s good to know there are further options for when I feel like getting my hands really dirty.

I had a question, is there a particular reason that batch drawing shapes does is only supported on two dimensions? is it possible we might see batch drawing across three dimensions?

Based on the program above and the one in your previous post I created this, which may help using those VertexBuffers. You just need to figure out where to put the quad points in space.

import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.color.presets.DARK_SEA_GREEN
import org.openrndr.extra.color.presets.DIM_GRAY
import org.openrndr.extra.color.presets.SEA_GREEN
import org.openrndr.extra.videoprofiles.gif
import org.openrndr.math.Vector3

/**
 * id: 4b11876b-63bc-4491-9e0d-b41ef1bb8b3e
 * description: Shows how to render a list of Quads as shaded triangles
 * and edges. Can be used to create designs based on 3D Quads and render them.
 * tags: #3D #Quad
 */

data class Quad(val a: Vector3, val b: Vector3, val c: Vector3, val d: Vector3)

fun main() = application {
    configure {
        width = 400
        height = 400
        multisample = WindowMultisample.SampleCount(4)
    }
    program {
        val cam = Orbital()
        cam.eye = -Vector3.UNIT_Z * 150.0

        /**
         *               5              6
         *              +--------------+
         *             /|             /|
         *            / |            / |
         *           /  |           /  |
         *          +--------------+   |
         *        1 |   |        2 |   |
         *          |   |          |   |
         *          |   |4         |   |7
         *          |   +----------|---+
         *          |  /           |  /
         *          | /            | /
         *          |/             |/
         *          +--------------+
         *        0              3
         */

        // The vertices of a cube
        val cubeVerts = listOf(
            Vector3(-50.0, -50.0, -50.0),
            Vector3(-50.0, 50.0, -50.0),
            Vector3(50.0, 50.0, -50.0),
            Vector3(50.0, -50.0, -50.0), // front
            Vector3(-50.0, -50.0, 50.0),
            Vector3(-50.0, 50.0, 50.0),
            Vector3(50.0, 50.0, 50.0),
            Vector3(50.0, -50.0, 50.0) // back
        )

        // Here we create the 3D design, in this case a simple Cube.
        // We could do any other shape based on Quads. A cloud of quads for instance. Or something like:
        // https://openrndr.discourse.group/t/is-there-a-way-to-batch-draw-these-rectangles/356

        val quads = mutableListOf<Quad>()
        quads.add(Quad(cubeVerts[0], cubeVerts[1], cubeVerts[2], cubeVerts[3])) // front
        quads.add(Quad(cubeVerts[7], cubeVerts[6], cubeVerts[5], cubeVerts[4])) // back
        quads.add(Quad(cubeVerts[4], cubeVerts[5], cubeVerts[1], cubeVerts[0])) // left
        quads.add(Quad(cubeVerts[3], cubeVerts[2], cubeVerts[6], cubeVerts[7])) // right
        quads.add(Quad(cubeVerts[1], cubeVerts[5], cubeVerts[6], cubeVerts[2])) // top
        quads.add(Quad(cubeVerts[3], cubeVerts[7], cubeVerts[4], cubeVerts[0])) // bottom

        val cube = quads.toTriangles()
        val cubeWire = quads.toWire()
        val shaded = shadeStyle {
            fragmentTransform = "x_fill.rgb *= v_viewNormal.z;"
        }

        //extend(ScreenRecorder()) { gif() }
        extend(cam)
        extend {
            drawer.clear(ColorRGBa.DIM_GRAY)

            drawer.shadeStyle = shaded
            drawer.fill = ColorRGBa.SEA_GREEN
            drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)

            drawer.shadeStyle = null
            drawer.fill = ColorRGBa.DARK_SEA_GREEN
            drawer.vertexBuffer(cubeWire, DrawPrimitive.LINES)
        }
    }
}

/**
 * Converts a list of [Quad] into a vertex buffer containing pairs of
 * vertices. Each [Quad] is 4 pairs (start and end points of a segment)
 *
 * @return A [VertexBuffer] to be drawn using [DrawPrimitive.LINES]
 */
private fun List<Quad>.toWire(): VertexBuffer {
    val buffer = vertexBuffer(vertexFormat {
        position(3)
    }, size * 4 * 2)
    buffer.put {
        forEach {
            write(it.a, it.b)
            write(it.b, it.c)
            write(it.c, it.d)
            write(it.d, it.a)
        }
    }
    return buffer
}

/**
 * Converts a list of [Quad] into a vertex buffer containing triplets of
 * vertices defining triangles and a normal for each vertex. That means that
 * each [Quad] becomes 2 triangles (6 vertices) and 6 normals. There is some
 * duplication as all six vertices have the same normals (the whole quad
 * has the same normal in each of the 6 vertices).
 *
 * @return A [VertexBuffer] to be drawn using [DrawPrimitive.TRIANGLES]
 */
private fun List<Quad>.toTriangles(): VertexBuffer {
    val quadCount = size
    val buffer = vertexBuffer(vertexFormat {
        position(3) // = vec3
        normal(3)   // = vec3
    }, quadCount * 6 * 2) // 6 verts per quad, times 2 (because we create position AND normal)
    buffer.put {
        forEach {
            val up = (it.b - it.a).cross(it.d - it.a).normalized
            write(it.a, up)
            write(it.b, up)
            write(it.c, up)
            write(it.a, up)
            write(it.c, up)
            write(it.d, up)
        }
    }
    return buffer
}

The Quads don’t need to be rectangular, but the points should be in the same plane. Also, one can do any kind of 3D models with quads, not just cubes, that was just the simplest :slight_smile: If anyone needs an exercise → create a Torus.

1 Like

Awesome, thanks @abe ! I’ll try this out.