Real-time graphics rendering (generative art)

I am looking for a package or a toolchain which allows me to render generative visuals as fast as possible (as for example with processing, p5 or other creative languages).

The reason I would like to use julia is, that my models are written in jl and i prefer not to transport everything to a new language. I am using GLMakie atm, But it is way too slow.

The dream would be to stream the Video to an other app via NDI or Syphon.

1 Like

I did use Blender with Matlab for one project. I would take a look at it. There are a number of posts for using it with Julia.

2 Likes

thanks! i didnt know that this exists. in this project i do only create a 2 pixel image. so i wont use blender, but for an other one this is an option!

I am using GLMakie atm, But it is way too slow.

GLMakie can be pretty fast (state of the art even for some things), but there are a couple of things that will make things very slow.
Can you pin down what’s too slow?

1 Like

I think at the moment i do sth quite stupid:

using Images
using NearestNeighbors

function voronoiColorMap(
    graph;
    saving::Bool=true,
    outdir::String="output/",
    xResolution::Int=1024,
    yResolution::Int=1024,
    xRange=(-1,1),
    yRange=(-1,1),
    filename::String="visualization",
    stateColors=Dict(1 => RGB(1,0,0), 0 => RGB(0,0,0), -1 => RGB(0,0,1)),
    ignoreBoundary::Bool=false
)
    # Ensure output directory ends with a slash
    outdir = endswith(outdir, "/") ? outdir : outdir * "/"
   
    # Pre-allocate arrays and extract data in one pass
    nVertices = nv(graph)
    verticesCoord = zeros(Float64, 2, nVertices)
    states = zeros(Int, nVertices)
   
    # Extract vertex data in chunks for better cache performance
    vertex_chunk_size = 128  # Adjust based on your system's cache size
    vertices_list = collect(vertices(graph))
   
    Threads.@threads for chunk_start in 1:vertex_chunk_size:nVertices
        chunk_end = min(chunk_start + vertex_chunk_size - 1, nVertices)
        for i in chunk_start:chunk_end
            v = vertices_list[i]
            pos = getVertexPosition(graph, v)
            @inbounds verticesCoord[1, i] = pos[1]
            @inbounds verticesCoord[2, i] = pos[2]
            @inbounds states[i] = Int(getVertexState(graph, v))
        end
    end
   
    # Build the tree once (outside of any loops)
    kdtree = KDTree(verticesCoord)
   
    # Pre-allocate the image array
    img = Matrix{RGB{Float64}}(undef, yResolution, xResolution)
   
    # Pre-compute x and y coordinates
    xCoords = LinRange(xRange[1], xRange[2], xResolution)
    yCoords = LinRange(yRange[1], yRange[2], yResolution)
   
    # Pre-allocate query point array to avoid allocations in the loop
    query_point = zeros(Float64, 2)
   
    # Process image in chunks for better cache performance
    chunk_size = 128  # Adjust based on your system's cache size
   
    # Create a mapping from state values to colors (avoid dictionary lookup)
    state_color_map = [stateColors[-1], stateColors[0], stateColors[1]]
   
    # Use @simd and @inbounds for inner loops when possible
    @time Threads.@threads for i_chunk in 1:cld(xResolution, chunk_size)
        # Calculate start and end indices for this chunk
        i_start = (i_chunk - 1) * chunk_size + 1
        i_end = min(i_chunk * chunk_size, xResolution)
       
        for i in i_start:i_end
            x = xCoords[i]
            query_point[1] = x
           
            for j in 1:yResolution
                query_point[2] = yCoords[j]
               
                # Use inrange query if possible to avoid sorting operations
                id, dist = knn(kdtree, query_point, 1)
                idx = id[1]
               
                # Fast indexing into pre-computed color map
                # Transform state (-1, 0, 1) to index (1, 2, 3)
                state_idx = states[idx] + 2  
                @inbounds img[j, i] = state_color_map[state_idx]
            end
        end
    end
   
    if saving
        save(outdir * filename * ".png", img)
    end
   
    return img
end

I want do to basically some shader type stuff. The vertices with colors are defined and the intermediate pixels have the same coler as its nearest neighbour.
The problem is the second loop, which takes roughly 10 times too long.

PS: Note that this is not anymore my GLMakie implementation. I did something slightly different in that plot.

1 Like

ok well that doesn’t help me figure out why your GLMakie code is slowšŸ˜‚

1 Like

But I guess if you really need a shader, this would be interesting:

1 Like

oh this looks promising. but I guess it will take a while until it is finished right?

Hm, its kind of finished, but hold back by a weird bug, which we have fixed once already, ok that bug was really stupid and we already fixed it…
I guess we could try to merge it soon!

4 Likes

So nice! Oh please let me know once it is part of the release!

Do you have an example of the graph, so a fully working MWE?

I just use MetaGraphsNext.jl . I will do one later today or tomorrow morning. :slight_smile:

here you go. i did simplify it a bit using a llm. but this is the main logic

# Minimal Working Example for Voronoi Plotting with MetaGraph
# Dependencies: MetaGraphsNext, Graphs, Images, FileIO, NearestNeighbors

using Graphs, MetaGraphsNext, LinearAlgebra
using Images, FileIO, NearestNeighbors

# Simple vertex properties structure
struct VertexData
    state::Int
    pos::Tuple{Float64, Float64}
end

# Function to create a graph with position and state data
function createDataGraph(n::Int; positionMap=(i) -> (0.0, 0.0))
    base_graph = Graph(n)
    
    # Vertex data preparation
    vertices_description = [
        Symbol("node$i") => VertexData(1, positionMap(i)) for i in 1:n
    ]
    
    # No edges initially
    edges_description = Pair{Tuple{Symbol, Symbol}, Nothing}[]

    g = MetaGraph(
        base_graph,
        vertices_description,
        edges_description,
    )

    return g
end

# Helper functions for getting properties
function getState(graph::MetaGraph, v::Int)
    return graph[Symbol("node$v")].state
end

function getPos(graph::MetaGraph, v::Int)
    return graph[Symbol("node$v")].pos
end

function setState!(graph::MetaGraph, v::Int, value::Int)
    currentProps = graph[Symbol("node$v")]
    new_props = VertexData(value, currentProps.pos)
    return set_data!(graph, Symbol("node$v"), new_props)
end

# Create a simple 2D grid of points
function create2DGrid(nx::Int, ny::Int; w::Float64=1.0, h::Float64=1.0)
    n = nx * ny
    
    # Position mapping function
    function positionMap(i::Int)
        # Convert linear index to 2D coordinates
        row = div(i - 1, nx) + 1
        col = mod(i - 1, nx) + 1
        
        x = (col - 1) * w / (nx - 1)
        y = (row - 1) * h / (ny - 1)
        
        return (x, y)
    end
    
    return createDataGraph(n; positionMap=positionMap)
end

# Voronoi plotting function
function voronoiPlot(
    graph::MetaGraph;
    saving::Bool=true,
    outdir::String="./",
    xResolution::Int=512,
    yResolution::Int=512,
    xRange=(0,1),
    yRange=(0,1),
    filename::String="voronoi_plot",
    stateColor=Dict(1 => RGB(1,0,0), 0 => RGB(0,0,0), -1 => RGB(0,0,1))
)
    # Ensure output directory ends with a slash
    outdir = endswith(outdir, "/") ? outdir : outdir * "/"
   
    # Extract vertex data
    nVertices = nv(graph)
    verticesCoord = zeros(Float64, 2, nVertices)
    states = zeros(Int, nVertices)
   
    vertices_list = vertices(graph)
    for (i, v) in enumerate(vertices_list)
        pos = getPos(graph, v)
        verticesCoord[1, i] = pos[1]
        verticesCoord[2, i] = pos[2]
        states[i] = getState(graph, v)
    end
   
    # Build KD-tree for nearest neighbor search
    kdtree = KDTree(verticesCoord)
   
    # Pre-allocate the image array
    img = zeros(RGB, yResolution, xResolution)
   
    # Pre-compute coordinates
    xCoords = LinRange(xRange[1], xRange[2], xResolution)
    yCoords = LinRange(yRange[1], yRange[2], yResolution)
   
    # Generate Voronoi diagram
    for i in 1:xResolution
        x = xCoords[i]
        for j in 1:yResolution
            y = yCoords[j]
            
            # Find nearest vertex
            id, _ = knn(kdtree, [x, y], 1)
            idx = id[1]
            
            # Color pixel based on state
            state_val = states[idx]
            img[j, i] = stateColor[state_val]
        end
    end
   
    if saving
        mkpath(outdir)  # Create directory if it doesn't exist
        FileIO.save(outdir * filename * ".png", img)
        println("Image saved to: ", outdir * filename * ".png")
    end
   
    return img
end

# Function to set random states
function randomStates!(graph::MetaGraph; states=[-1, 1])
    for v in vertices(graph)
        setState!(graph, v, rand(states))
    end
end

println("Creating Voronoi Plot MWE...")

# Parameters
nx, ny = 20, 20  # Grid size
w, h = 1.0, 1.0  # Domain size

# Create 2D grid
println("Creating 2D grid...")
graph = create2DGrid(nx, ny; w=w, h=h)

# Set random states
println("Setting random states...")
randomStates!(graph; states=[-1, 1])

# Create custom colors
pink = RGB(0.80, 0.52, 0.82)
buff = RGB(1.00, 0.75, 0.43)

stateColor = Dict(1 => pink, 0 => RGB(0,0,0), -1 => buff)

# Generate Voronoi plot
println("Generating Voronoi plot...")
img = voronoiPlot(
    graph;
    saving=true,
    xResolution=512,
    yResolution=512,
    xRange=(0, w),
    yRange=(0, h),
    stateColor=stateColor,
    filename="voronoi_example"
)

println("Done!")
return img