I'm working on BlenderBIM (architecture addon for Blender) and we have a bunch of shaders to support plan annotations in viewports. Mainly it's simple shapes based of lines - lines, segment based circles, triangles etc. Our approach was to take the each edge verts to geometry shader and based on their position create neccessary shapes by adding geometry with EmitVertex and EndPrimitive.

Now we need to support Metal backend for Mac M1 (and supporting it's very important given the performance it has compared to other backend on Mac). What I've learned there is no geometry shaders on Metal but there are some Metal features that allow to manipulate geometry in vertex shader. So we do need to rework our shaders.

I've found that there are 3 options:

  1. Write two glsl shaders for each type of annotation - one for Metal and one for non-Metal. Creates more complexity and it will be harder to maintain.

  2. Write one more complex glsl shader for each type of annotation - it will do the work both for Metal and non-Metal from the vertex shader. It seems possible since on non-Metal we can emulate Metal feature that gets position of the neighbour verts by passing additional data to vertex shader. But it also will add more complexity to rather simple shaders. Not sure if there are other techical limitations we might meet.

  3. Calculate all verts positions in python and just use builtin shader for lines. It's seems the most simple and robust solution that will definitely work and (hopefully) won't break in the future. So basically we'll have 1 geometry function to generate geometry for each annotation type. It will be in python and it will be atleast as simple as geometry shaders we currently have or simpler.

    coords = [(1, 1, 1), (-2, 0, 0), (-2, -1, 3), (0, 1, 1)]
    shader = gpu.shader.from_builtin('3D_POLYLINE_UNIFORM_COLOR')
    batch = batch_for_shader(shader, 'LINES', {"pos": coords})
    shader.bind() # required to change uniforms of the shader
    shader.uniform_float("viewportSize", (bpy.context.area.width, bpy.context.area.height))
    shader.uniform_float("color", (1,0,0,1))
    shader.uniform_float("lineWidth", 5.0)

From my point of view solution #3 looks really good.

Since we're using just python to generate all coords it should be a bit slower than geometry shader and it also doesn't have those compiler optimizations. But we tested it and both pure python and custom geometry shader seems to get laggy with big amounts of edges anyway, so I'm not sure if performance of the geometry shaders is that advantageous.

Other thing we missing from custom shader is option to have custom frag shader - the only use for it currently is dashed lines but we can either avoid dashed lines or to create 1 custom shader to create dashed lines without other geometry manipulation.

Looking for some outside opinions on the best way to perform this transition to support Metal too. Any other advantages of geometry shaders I might be missing? Maybe there are some other catch for this transition?

PS Our shaders code can be found here.

  • 2
    $\begingroup$ I don't know if it's applicable in your case, but one pipeline almost everyone doesn't notice is to use Python + Geometry Nodes - perhaps because almost no one is proficient in both. Since Geometry Nodes are implemented in C, using Geometry Nodes from Python is similar to using numpy: you communicate through a limited interface, but gain the performance by escaping the Python interpreter. Actually numpy is a good way to send data to geometry nodes (set positions of vertices foreach_set) and read it back (evaluate the object, read positions foreach_get) $\endgroup$ Apr 28, 2023 at 23:39


You must log in to answer this question.

Browse other questions tagged .