0
\$\begingroup\$

I am trying to create a vector rendering library for OpenGL in Rust. Currently, the goal is to produce nanovg-like rendering. I have found two approaches to doing my rendering:

  • The pure shader approach, where I just render a single fullscreen quad in OpenGL, and use shaders draw all my vector shapes
  • The pure geometry approach, where I create a canvas, add all my shapes, triangulate the vertices on CPU, then upload the vertices to GPU and render with a basic vertex and fragment shader

For instance, using the pure shader approach, I would potentially use some GLSL code that looks like this:

uniform vec2 u_resolution;

vec3 circle(vec2 uv, vec2 center, float radius, vec3 color) {
    float x = sqrt(pow(uv.x - center.x, 2.) + pow(uv.y - center.y, 2.));
    if (x < radius) {
        return color;
    } else {
        return vec3(1., 1., 1.);
    }
}

vec3 rectangle(vec2 uv, vec2 p1, vec2 p2, vec3 color)
{
    if (uv.x > p1.x && uv.x < p2.x && uv.y > p1.y && uv.y < p2.y)
    {
        return color;
    } else {
        return vec3(1.0, 1.0, 1.0);
    }
}

void main() {
    vec2 uv = gl_FragCoord.xy/u_resolution.xy;
    vec3 color1 = circle(uv, vec2(0.3, 0.5), 0.2, vec3(1.0, 0.0, 1.0));
    vec3 color2 = rectangle(uv, vec2(0.3, 0.2), vec2(0.7, 0.5), vec3(0.5, 0.2, 1.0));
    vec3 color = mix(color1, color2, color1);

    gl_FragColor = vec4(color,1.0);
}

whereas with the pure geometry approach, I would use some code that looks like this:

fn main() {
    let canvas = Canvas::new();
    canvas.addCircle(0.1, 0.2, 0.3);
    canvas.addRect(0.5, 0.2, 4.0, 5.0);
    let vertices = triangulate(canvas.to_vertices());
    // Some general OpenGL code to render some vertices etc.
}

However, both approaches seem to have issues - the pure shader approach is very difficult to add a large number of shapes, and the pure geometry approach is hard to get working and possibly not as efficient. Does anyone have any suggestions for an alternate approach?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

If you want to be able to scale up to hundreds of shapes or complex shapes like Bézier splines and text glyphs, geometry is the way to go. You do not want to write shader functions to describe the shape of each character in an entire font, let alone multiple fonts or multilingual support. And you don't want to have to re-compile the e entire shader every tima a shape needs to change, including all the code for the shapes that didn't change.

Using geometry, each render pays only for the fragments that actually get drawn to, rather than evaluating an increasingly expensive shading function for every empty pixel on the screen. Most shapes won't change every frame/repaint, so the vertices calculated previously can be re-used, keeping the repaint efficient. For large canvases you can also take advantage of culling to avoid revisiting/repainting shapes outside the visible or modified region.

You're not wrong that the geometry approach involves a lot of work, but that work is an investment in scalability. There are also some tricks you can use to render exact, anti-aliased curves within a triangle, rather than densely subdividing curves down to straight-line segments, so the geometry complexity does not get out of hand. Rendering Vector Art on the GPU from GPU Gems 3 is a great resource about this.

Letter e as Bézier splines and triangles

Image from GPU Gems 3

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .