How to draw batched lines with different style?

Hello, I’m trying to draw a bunch o lines, each with a different style. If I understand correctly, to draw straight lines in OPENRNDR, I should use the LineSegment class.

I read in the docs that for other primitive shapes I can use

  • static approach: drawer.<<primitive>>Batch { }
  • dynamic approach: drawer.<<primitive>>s { }

which gives me <<primitive>>BatchBuilder where I can set the style and create the primitive object using build functions.

However, there doesn’t seem to be any drawer.lineSegments() function gives me LineSegmentBatchBuilder. As far as I can see, all lineSegments overloads take a list of vectors or LineSegments. In fact, the LineSegmentBatchBuilder class does not seem to exist.

If I use drawer.lineSegment() to draw each line separately, the performance drops quite quickly for relatively small number of lines.

How can I draw batched lines each with a different style, please?

Hi @anoniim!

You are right, Drawing primitives batched | OPENRNDR GUIDE only seems to show circles, rectangles and points, not lines.

Maybe that could be added to the framework based on the other 3 examples…

Meanwhile, I found this program among my sketches.

import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.draw.vertexBuffer
import org.openrndr.draw.vertexFormat
import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.simplex3D
import org.openrndr.extra.noise.uniform
import org.openrndr.extra.noise.withVector2Output

fun main() = application {
    configure {
        width = 800
        height = 800
    }
    program {
        // create a buffer and specify it's format and size.
        val geometry = vertexBuffer(vertexFormat {
            position(3)
            color(4)
        }, 2 * 210000)

        // create an area with some padding around the edges
        val area = drawer.bounds.offsetEdges(-50.0)

        val n = simplex3D.withVector2Output()
        // populate the vertex buffer.
        geometry.put {
            for (i in 0 until geometry.vertexCount / 2) {
                val p = area.uniform()
                write(p.vector3(z = 0.0)) // start
                write(Random.vector4(0.0, 1.0)) // color

                write(
                    (p + n(5, p.x * 0.01, p.y * 0.01, 0.0) * 5.0)
                        .vector3(z = 0.0)
                ) // end
                write(Random.vector4(0.0, 1.0)) // color
            }
        }

        extend {
            // shader using the color attributes from our buffer
            drawer.shadeStyle = shadeStyle {
                fragmentTransform = "x_fill = va_color;"
            }
            drawer.vertexBuffer(geometry, DrawPrimitive.LINES)
        }
    }
}

Maybe it helps you get started?

There’s also a simpler way:

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.uniform
import org.openrndr.math.Vector4

fun main() = application {
    program {
        val count = 5000
        val segs = List(count) {
            drawer.bounds.uniform(50.0).vector3(z = 0.0)
        }
        val thicks = List(count) {
            Random.double(1.0, 8.0)
        }
        val colors = List(count) {
            ColorRGBa.fromVector(Vector4.uniform(0.0, 1.0))
        }
        extend {
            drawer.lineSegments(segs, thicks, colors)
        }
    }
}

But I haven’t measured the performance, and I don’t know why the number of colors and thicknesses equals the number of vertices.

Maybe you can test and say how the performance compares?

Thanks @abe, you are very helpful as always!

The following lineSegments overload is sufficient for what I need. I should have really noticed it the first time I was looking at it!

fun lineSegments(segments: List<Vector3>, weights: List<Double>, colors: List<ColorRGBa>)

I played with it a bit (and checked the implementation of that function) and noticed a couple of things

  • The size of the vector list must be even. It crashes with ArrayIndexOutOfBounds otherwise
  • The odd items from weights and colors are ignored, only the even items are used for the corresponding line

There is no overload for Vector2, and it’s far less convenient than the BatchBuilder, but does the job! It also performs well even with large number of lines.

I’ll look into vertexBuffers when I hit the limits of this solution.

Thanks!

One more caveat for people who draw in 2D like me :slight_smile:

Since the only lineSegments() overload that allows setting colors takes Vector3, drawStyle.lineCap is not taken into account. Line caps are only applied when drawing lines in 2D.

2 Likes

I’ve hit another roadblock when I wanted to draw batched rectangles with different shade styles (gradient fill).

The RectangleBatchBuilder doesn’t support adding shadeStyle to individual rectangles (only seems to support the basic stroke, strokeWeight and fill).

There is Drawer.rectangles(batch: RectangleBatch, count: Int) where RectangleBatch constructor takes geometry and drawStyle

RectangleBatch(val geometry: VertexBuffer, val drawStyle: VertexBuffer)

but that is expecting the following format of drawStyle:

val drawStyleFormat = vertexFormat {
    attribute("fill", VertexElementType.VECTOR4_FLOAT32)
    attribute("stroke", VertexElementType.VECTOR4_FLOAT32)
    attribute("strokeWeight", VertexElementType.FLOAT32)
}

so I wouldn’t be able to set different shade style to individual rectangles that way either.

@abe, it seems like my only option is to use shaders as you suggested above. I recall you mentioning somewhere else on the forum that you might record a shader tutorial so I wonder whether that eventually happened? :innocent:

I looked at a few videos on YouTube and read a couple of articles so I have some basic idea how shaders work but would still appreciate any links to good tutorials or documentation to learn more.

I guess what I’ll have to do is to use Drawer.vertexBuffer() to draw my rectangles as custom geometry and set shadeStyle with fragmentTransform which creates the gradient.

Hi! @Alessandro and I have recorded now 11 videos but none yet about shaders. He actually suggested going into that topic :slight_smile:

I guess if you use shaders you wouldn’t need multiple shadeStyles but one shader that produces different looks depending on the uniforms (or attributes) you pass to it.

How would you like the various gradients to differ? Are they all the same type (linear, radial)? Can the rectangles have various orientations? Is it animated? Trying to imagine how it looks like :slight_smile:

Could you draw everything by layers? First everything that uses one shade style, then the next one, etc? (so you don’t need to change the style per rectangle).

1 Like

Came here searching for the same thing, but for triangles. I looked into how Drawer.rectangles {} works, and apparently it uses the DrawPrimitive.TRIANGLE under the hood. But there is no similar wrapper for triangles! :frowning:

I think wrappers for all primitives would be quite helpful. Also, is there no easy way to implement at least a batched shapes(shapes: List<Shape>, colors: List<ColorRGBa>)?

Hi :slight_smile: Could you describe what you are trying to achieve?

What about using a vertexBuffer to draw triangles?

About drawing shapes, is it the case that you are drawing many shapes with different colors and doing it one by one is not fast enough?

Yep, exactly that. Vertex buffers seem like a too heavy-weight solution for such a simple task. Since there exists the segments(segments, weights, colors) wrapper, I’d very much prefer using a similar shapes(...) function. Or in the case of triangles, there could be a BatchBuilder like with circles or rectangles.

I guess for now I can write such a wrapper for triangles myself using the link you provided (thanks!).

I played with this approach to drawing triangles, but I’m not sure if it makes things faster. It provides a triangle drawer with a maximum number of triangles (to avoid resizing the buffer). Then one can draw a variable amount of triangles on every frame (up to the maximum). It allows to specify a color per vertex, although one could simplify it to specify a color per triangle.

3-Color triangle drawer

imports
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.draw.*
import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.uniform
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
data class ColoredTriangle(
    val p0: Vector2, val p1: Vector2, val p2: Vector2,
    val c0: Vector4, val c1: Vector4, val c2: Vector4
)
class TriangleDrawer(private val drawer: Drawer, maxTriangles: Int) {
    private val geom = vertexBuffer(vertexFormat {
        position(3)
        color(4)
    }, 3 * maxTriangles)

    fun draw(tris: List<ColoredTriangle>) {
        geom.put {
            tris.forEach { tri ->
                write(tri.p0.x.toFloat(), tri.p0.y.toFloat(), 0f)
                write(tri.c0)
                write(tri.p1.x.toFloat(), tri.p1.y.toFloat(), 0f)
                write(tri.c1)
                write(tri.p2.x.toFloat(), tri.p2.y.toFloat(), 0f)
                write(tri.c2)
            }
        }
        val count = (tris.size * 3).coerceAtMost(geom.vertexCount)
        drawer.shadeStyle = shadeStyle {
            fragmentTransform = "x_fill = va_color;"
        }
        drawer.vertexBuffer(geom, DrawPrimitive.TRIANGLES, 0, count)
    }
}
fun main() {
    application {
        //configure { multisample = WindowMultisample.SampleCount(8) }
        program {
            val triangleDrawer = TriangleDrawer(drawer, 1000)
            val area = drawer.bounds.offsetEdges(-50.0)
            extend {
                val count = Random.int(1, 1000)
                triangleDrawer.draw(List(count) {
                    ColoredTriangle(
                        Vector2.uniform(area),
                        Vector2.uniform(area),
                        Vector2.uniform(area),
                        Vector4.uniform(0.0, 1.0),
                        Vector4.uniform(0.0, 1.0),
                        Vector4.uniform(0.0, 1.0)
                    )
                })
            }
        }
    }
}

The size of the triangles does affect the performance, so drawing a thousand overlapping translucent triangles is quite heavy. One can get a high frame rate by drawing smaller triangles with less overlap.

1 Like

Just as I was writing about how I failed miserably, you come and save the day! :pray:

I am a total noob in low-level drawing, so all of my attempts to piece together the documentation, chatgpt advice and common sence were fruitless :smiling_face_with_tear: But it turns out the only thing I needed is your "x_fill = va_color;" spell.

This code works perfectly fine for 10K triangles:

fun Drawer.triangles(triangles: List<Triangle>, colors: List<ColorRGBa>) = isolated {
    require(triangles.size == colors.size)
    val n = triangles.size
    val vb = vertexBuffer(vertexFormat {
        position(3)
        color(4)
    }, 3 * n)
    vb.put {
        for (i in 0 until n) {
            triangles[i].run {
                val c = colors[i]
                write(x1.xy0)
                write(c)
                write(x2.xy0)
                write(c)
                write(x3.xy0)
                write(c)
            }
        }
    }
    shadeStyle = shadeStyle {
        fragmentTransform = "x_fill = va_color;"
    }
    vertexBuffer(vb, DrawPrimitive.TRIANGLES)
    vb.destroy()
}

when being called like

extend {
    drawer.triangles(triangles, colors)
}

(probably worth noting that I’m changing the colors elements from GlobalScope coroutines)

Edit: “perfectly fine for 10K” non-intersecting trianlges. Still, perfectly fine for 1K intersecting semi-transparent triangles.

Edit 2: I see how vertexBuffer{} is called on every frame, but it doesn’t seem to cause performance issues. However, I just got an OutOfMemoryError, so I added vb.destroy() to prevent that.

1 Like