Makie: how to improve plotting performance with thousands of 3D objects

I need to plot several thousands of quite simple 3D objects (boxes, cylinders, …) and for this I do create a mesh for each one implementing GeometryBasics.coordinates(...) and GeometryBasics.faces(...). Then I plot them either using mesh!(...) and/or wireframe!(...) for each created mesh. It works nicely but when I increase the number objects starts being very very slow. In practice, I can not do more than a few thousand.
What I am doing wrong? Should I merge the meshes and call mesh!(...) once? Is there an utility to do it? Thanks for your help.

If it’s just boxes + cylinders, you should be able to use meshscatter, which uses instancing and should be much faster.
Otherwise, merging is indeed a good idea, here is an example:

1 Like

Help me improve the performance for plotting many wireframe? The previous answer worked for normal mesh of shapes but I do not know how to apply this to wireframe.

Current MWE exhibiting growing time to plot wireframes:

 using GLMakie, GeometryBasics, BenchmarkTools, Printf

function colormesh(geometry,color)
    mesh1 = normal_mesh(geometry)
    npoints = length(GeometryBasics.coordinates(mesh1))
    return GeometryBasics.pointmeta(
        mesh1;
        shading=fill(false,npoints),
        color=fill(color, npoints),
        transparency=fill(true,npoints),
        overdraw=fill(true,npoints),
    )
end

function setup(nRects,alpha,b,p)
    time = @elapsed begin
        println("\tGenerating random geometries and colors")
        #Make a bunch of rectangles with associated color information
        rectangles = Rect3f[]
        colors = RGBAf[]

        base_mean,base_std = b[1],b[2]
        pos_mean,pos_std = p[1],p[2]

        for i in 1:nRects
            base = (rand(Vec3f).-base_mean)./base_std
            position = (rand(Vec3f).-pos_mean)./pos_std
            rectangle = Rect3f(base,position)
            color = RGBAf(rand(Float32,3)...,alpha)
            push!(rectangles,rectangle)
            push!(colors,color)
        end
    end
    @printf("\tSetup done, took %f seconds\n",time)
    return rectangles,colors
end

function basicPlot()
    time = @elapsed begin
        println("\tCreating figure and axis (basicPlot)")
        fig = Figure()
        ax = Axis3(
            fig[1, 1],
            xlabel = "x label",
            ylabel = "y label",
            zlabel = "y label",
            title = "Title"
                )
    end
    @printf("\tBasic fig and ax created, took %f seconds\n",time)
    return fig,ax
end

function meshPlotRects(rectangles,colors)
    time = @elapsed begin
        println("\tCreating figure and axis (meshPlotRects)")
        meshes = map(colormesh,rectangles,colors)
        println("\t\tMeshes created")
        fig = Figure()
        fig, ax, meshplot = mesh(merge(meshes))
        println("\t\tMesh plotted with fig and ax created")
    end
    @printf("\tDone, took %f seconds\n",time)
    return fig,ax
end

function setView!(ax)
    time = @elapsed begin
        println("\tSetting the view")
        #Setup the view
        scene = ax.scene
        center!(scene)
        cam = cameracontrols(scene)
        cam.attributes[:projectiontype][] = Makie.Orthographic
        cam.zoom_mult[] = 10.0f0
        update_cam!(scene, cam)
    end
    @printf("\tDone, took %f seconds\n",time)
end

function doWireframe(ax,rectangles,colors)
    time = @elapsed begin
        println("\tWireframe started")
        for (rect,c) in zip(rectangles,colors)
            dark_c = RGBAf([c.r,c.g,c.b]*.5...,1.)
            wireframe!(ax, rect; color=dark_c, transparency=true)
        end
    end
    @printf("\tWireframe done, took %f seconds\n",time)
end

displayPlots(fs)=[display(GLMakie.Screen(),f) for f in fs]

function main(nRects)
    simple = false
    alpha = .10
    time = @elapsed begin
        @printf("\nProcessing for %d rectangles\n",nRects)
        rectangles,colors = setup(nRects,alpha,(.5,.05),(10.,2.))
        fig,ax = simple ? basicPlot() : meshPlotRects(rectangles,colors)
        doWireframe(ax,rectangles,colors)
        if !simple setView!(ax) end
    end
    @printf("Done, took %f seconds\n",time)
    return fig
end

figs = (main(i) for i in (1,1,10,100,1_000,10_000))

println("\nDrawing windows")
time = @elapsed displayPlots(figs)
@printf("Drawing took %f seconds\n",time)

println("\n:: PAUSED :: (Press any key to exit.)")
readline()

Which produces the following output:

As you can see the wireframe portion grows nonlinearly in time to execute.

      N                 Time(s)       Delay      Step
     ----------------------------------------------------
     1 rectangle       0.002785          1x       N/A
    10 rectangle       0.031733         11x       11x
   100 rectangle       0.475575        171x       16x
 1_000 rectangle      20.538121       7375x       43x
10_000 rectangle    1645.453709    590,827x       80x

Compared to mesh(merge(meshes)) which seems pretty stable

      N                 Time(s)       Delay      Step
     ----------------------------------------------------
     1 rectangle       0.007688          1x      N/A
    10 rectangle       0.042709          6x       6x
   100 rectangle       0.019673          3x     0.5x
 1_000 rectangle       0.033248          4x     1.3x
10_000 rectangle       0.241189          3x     0.8x

I can appreciate that the timing is not particularly exact but regardless wireframe!() plotting is taking forever for some reason.

PS - I do not think is in any way related to the multiple window display as the same thing occurs for fewer windows or even a single one where the rectangle count is high.

Can you do one wireframe of the merged mesh?
One plot per box is a really bad idea for performance, especially if it goes into the thousands…

As in?
wireframe!(ax, meshes; color=:black, transparency=true)

Does not work:

ERROR: LoadError: MethodError: no method matching coordinates(::Vector{GeometryBasics.Mesh{3, Float32, TriangleP{3, Float32, PointMeta{3, Float32, Point{3, Float32}, (:normals, :shading, :color, :transparency, :overdraw), Tuple{Vec{3, Float32}, Bool, ColorTypes.RGBA{Float32}, Bool, Bool}}}, FaceView{TriangleP{3, Float32, PointMeta{3, Float32, Point{3, Float32}, (:normals, :shading, :color, :transparency, :overdraw), Tuple{Vec{3, Float32}, Bool, ColorTypes.RGBA{Float32}, Bool, Bool}}}, PointMeta{3, Float32, Point{3, Float32}, (:normals, :shading, :color, :transparency, :overdraw), Tuple{Vec{3, Float32}, Bool, ColorTypes.RGBA{Float32}, Bool, Bool}}, NgonFace{3, OffsetInteger{-1, UInt32}}, StructArrays.StructVector{PointMeta{3, Float32, Point{3, Float32}, (:normals, :shading, :color, :transparency, :overdraw), Tuple{Vec{3, Float32}, Bool, ColorTypes.RGBA{Float32}, Bool, Bool}}, NamedTuple{(:position, :normals, :shading, :color, :transparency, :overdraw), Tuple{Vector{Point{3, Float32}}, Vector{Vec{3, Float32}}, Vector{Bool}, Vector{ColorTypes.RGBA{Float32}}, Vector{Bool}, Vector{Bool}}}, Int64}, Vector{NgonFace{3, OffsetInteger{-1, UInt32}}}}}})
Closest candidates are:
coordinates(::TupleView) at C:\Users\DIi.julia\packages\GeometryBasics\KE3OI\src\viewtypes.jl:39
coordinates(::Simplex) at C:\Users\DIi.julia\packages\GeometryBasics\KE3OI\src\basic_types.jl:149
coordinates(::GeometryBasics.Ngon) at C:\Users\DIi.julia\packages\GeometryBasics\KE3OI\src\basic_types.jl:73

Neither does
wireframe!(ax, rectangles; color=:black, transparency=true)

ERROR: LoadError: MethodError: no method matching coordinates(::Vector{HyperRectangle{3, Float32}})
Closest candidates are:
coordinates(::TupleView) at C:\Users\DIi.julia\packages\GeometryBasics\KE3OI\src\viewtypes.jl:39
coordinates(::Simplex) at C:\Users\DIi.julia\packages\GeometryBasics\KE3OI\src\basic_types.jl:149
coordinates(::GeometryBasics.Ngon) at C:\Users\DIi.julia\packages\GeometryBasics\KE3OI\src\basic_types.jl:73

wireframe docs do not cover a use case other than

wireframe(x, y, z)
wireframe(positions)
wireframe(mesh)

No, the merged mesh, as you do in your example for the mesh call…
So:

wireframe!(ax, merge(meshes); color=:black, transparency=true)

Ahhh…

That does work. Except each of the cube faces splits into a triangle which is not desired

Versus

Much faster though!

Drawing windows

Processing for 1 rectangles
Generating random geometries and colors
Setup done, took 0.006586 seconds
Creating figure and axis (meshPlotRects)
Meshes created
Mesh plotted with fig and ax created
Done, took 23.402483 seconds
Setting the view
Done, took 0.019364 seconds
Done, took 23.523272 seconds

Processing for 1 rectangles
Generating random geometries and colors
Setup done, took 0.000337 seconds
Creating figure and axis (meshPlotRects)
Meshes created
Mesh plotted with fig and ax created
Done, took 0.012360 seconds
Setting the view
Done, took 0.000512 seconds
Done, took 0.168637 seconds

Processing for 10 rectangles
Generating random geometries and colors
Setup done, took 0.006376 seconds
Creating figure and axis (meshPlotRects)
Meshes created
Mesh plotted with fig and ax created
Done, took 0.037795 seconds
Setting the view
Done, took 0.000732 seconds
Done, took 0.125865 seconds

Processing for 100 rectangles
Generating random geometries and colors
Setup done, took 0.000338 seconds
Creating figure and axis (meshPlotRects)
Meshes created
Mesh plotted with fig and ax created
Done, took 0.022643 seconds
Setting the view
Done, took 0.001973 seconds
Done, took 0.057639 seconds

Processing for 1000 rectangles
Generating random geometries and colors
Setup done, took 0.000724 seconds
Creating figure and axis (meshPlotRects)
Meshes created
Mesh plotted with fig and ax created
Done, took 0.095724 seconds
Setting the view
Done, took 0.016857 seconds
Done, took 0.145963 seconds

Processing for 10000 rectangles
Generating random geometries and colors
Setup done, took 0.012091 seconds
Creating figure and axis (meshPlotRects)
Meshes created
Mesh plotted with fig and ax created
Done, took 0.551681 seconds
Setting the view
Done, took 0.187104 seconds
Done, took 0.804036 seconds
Drawing took 46.364086 seconds

I messed around trying to figure out how else to create valid mesh input of a type that would not be rendered as triangles

  • GeometryBasics.Polytope() which calls one of:
"""
The fully concrete Ngon type, when constructed from a point type!
"""
function Polytope(::Type{<:NNgon{N}}, P::Type{<:AbstractPoint{NDim,T}}) where {N,NDim,T}
    return Ngon{NDim,T,N,P}
end
"""
The Simplex Polytope element type when indexing an array of points with a SimplexFace
"""
function Polytope(P::Type{<:AbstractPoint{Dim,T}},
                  ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T}
    return Simplex{Dim,T,N,P}
end
"""
The fully concrete Simplex type, when constructed from a point type!
"""
function Polytope(::Type{<:NSimplex{N}}, P::Type{<:AbstractPoint{NDim,T}}) where {N,NDim,T}
    return Simplex{NDim,T,N,P}
end
Base.show(io::IO, x::LineP) = print(io, "Line(", x[1], " => ", x[2], ")")
  • Simplex{3, Float32, 4} which calls:
function (::Type{<:NSimplex{N1}})(p0::P, points::Vararg{P,N2}) where {P<:AbstractPoint{Dim,T},
                                                             N1, N2} where {Dim,T}
    @assert N1 == N2+1
    return Simplex{Dim,T,N1,P}(SVector(p0, points...))
end
  • Or GeometryBasics.Ngon{3, Float32, 4} which is equivalent to GeometryBasics.Quadrilateral{3,Float32} which calls:
function (::Type{<:NNgon{N1}})(p0::P, points::Vararg{P,N2}) where {P<:AbstractPoint{Dim,T},
                                                          N1, N2} where {Dim,T}
    @assert N1 == N2+1
    return Ngon{Dim,T,N1,P}(SVector(p0, points...))
end
Base.show(io::IO, x::NNgon{N}) where {N} = print(io, "Ngon{$N}(", join(x, ", "), ")")

I am out of my depth and could not seem to get to an input of the types expected to produce a non triangular mesh.

A prod in the right direction would be further appreciated :slight_smile:

Sorry, GeometryBasics is a mess and I still haven’t found time to clean it up
Try:

merge(mesh.(rectangles; facetype=QuadFace{Int}))

Btw, sadly only color can be passed as an attribute to the mesh itself, so all the other attributes you have here need to get passed to the mesh call separately:

1 Like