14

I draw lots of quadratic Bézier curves in my OpenGL program. Right now, the curves are one-pixel thin and software-generated, because I'm at a rather early stage, and it is enough to see what works.

Simply enough, given 3 control points (P0 to P2), I evaluate the following equation with t varying from 0 to 1 (with steps of 1/8) in software and use GL_LINE_STRIP to link them together:

B(t) = (1 - t)2P0 + 2(1 - t)tP1 + t2P2

Where B, obviously enough, results in a 2-dimensional vector.

This approach worked 'well enough', since even my largest curves don't need much more than 8 steps to look curved. Still, one pixel thin curves are ugly.

I wanted to write a GLSL shader that would accept control points and a uniform thickness variable to, well, make the curves thicker. At first I thought about making a pixel shader only, that would color only pixels within a thickness / 2 distance of the curve, but doing so requires solving a third degree polynomial, and choosing between three solutions inside a shader doesn't look like the best idea ever.

I then tried to look up if other people already did it. I stumbled upon a white paper by Loop and Blinn from Microsoft Research where the guys show an easy way of filling the area under a curve. While it works well to that extent, I'm having trouble adapting the idea to drawing between two bouding curves.

Finding bounding curves that match a single curve is rather easy with a geometry shader. The problems come with the fragment shader that should fill the whole thing. Their approach uses the interpolated texture coordinates to determine if a fragment falls over or under the curve; but I couldn't figure a way to do it with two curves (I'm pretty new to shaders and not a maths expert, so the fact I didn't figure out how to do it certainly doesn't mean it's impossible).

My next idea was to separate the filled curve into triangles and only use the Bézier fragment shader on the outer parts. But for that I need to split the inner and outer curves at variable spots, and that means again that I have to solve the equation, which isn't really an option.

Are there viable algorithms for stroking quadratic Bézier curves with a shader?

zneak
  • 134,922
  • 42
  • 253
  • 328
  • Wow +1. This seems doable all the way. What do you want to have on the GPU? Just give the Bezier control points and let the GPU handle everything? – Marnix Apr 06 '11 at 08:55
  • @Marnix Yeah, that's what I'd like: submit three control points and a thickness, then have the GPU output the curve. – zneak Apr 06 '11 at 13:17
  • see [rendering 2D cubic Bezier with GLSL](https://stackoverflow.com/a/60113617/2521214) you just change the interpolation equations from cubic to bezier ... – Spektre Oct 05 '20 at 08:27

3 Answers3

3

This partly continues my previous answer, but is actually quite different since I got a couple of central things wrong in that answer.

To allow the fragment shader to only shade between two curves, two sets of "texture" coordinates are supplied as varying variables, to which the technique of Loop-Blinn is applied.

varying vec2 texCoord1,texCoord2;
varying float insideOutside;

varying vec4 col;

void main()
{   
    float f1 = texCoord1[0] * texCoord1[0] - texCoord1[1];
    float f2 = texCoord2[0] * texCoord2[0] - texCoord2[1];

    float alpha = (sign(insideOutside*f1) + 1) * (sign(-insideOutside*f2) + 1) * 0.25;
    gl_FragColor = vec4(col.rgb, col.a * alpha);
}

So far, easy. The hard part is setting up the texture coordinates in the geometry shader. Loop-Blinn specifies them for the three vertices of the control triangle, and they are interpolated appropriately across the triangle. But, here we need to have the same interpolated values available while actually rendering a different triangle.

The solution to this is to find the linear function mapping from (x,y) coordinates to the interpolated/extrapolated values. Then, these values can be set for each vertex while rendering a triangle. Here's the key part of my code for this part.

    vec2[3] tex = vec2[3]( vec2(0,0), vec2(0.5,0), vec2(1,1) );

    mat3 uvmat;
    uvmat[0] = vec3(pos2[0].x, pos2[1].x, pos2[2].x);
    uvmat[1] = vec3(pos2[0].y, pos2[1].y, pos2[2].y);
    uvmat[2] = vec3(1, 1, 1);

    mat3 uvInv = inverse(transpose(uvmat));

    vec3 uCoeffs = vec3(tex[0][0],tex[1][0],tex[2][0]) * uvInv;
    vec3 vCoeffs = vec3(tex[0][1],tex[1][1],tex[2][1]) * uvInv;

    float[3] uOther, vOther;
    for(i=0; i<3; i++) {
        uOther[i] = dot(uCoeffs,vec3(pos1[i].xy,1));
        vOther[i] = dot(vCoeffs,vec3(pos1[i].xy,1));
    }   

    insideOutside = 1;
    for(i=0; i< gl_VerticesIn; i++){
        gl_Position = gl_ModelViewProjectionMatrix * pos1[i];
        texCoord1 = tex[i];
        texCoord2 = vec2(uOther[i], vOther[i]);
        EmitVertex();
    }
    EndPrimitive();

Here pos1 and pos2 contain the coordinates of the two control triangles. This part renders the triangle defined by pos1, but with texCoord2 set to the translated values from the pos2 triangle. Then the pos2 triangle needs to be rendered, similarly. Then the gap between these two triangles at each end needs to filled, with both sets of coordinates translated appropriately.

The calculation of the matrix inverse requires either GLSL 1.50 or it needs to be coded manually. It would be better to solve the equation for the translation without calculating the inverse. Either way, I don't expect this part to be particularly fast in the geometry shader.

RD1
  • 3,305
  • 19
  • 28
0

You should be able to use technique of Loop and Blinn in the paper you mentioned.

Basically you'll need to offset each control point in the normal direction, both ways, to get the control points for two curves (inner and outer). Then follow the technique in Section 3.1 of Loop and Blinn - this breaks up sections of the curve to avoid triangle overlaps, and then triangulates the main part of the interior (note that this part requires the CPU). Finally, these triangles are filled, and the small curved parts outside of them are rendered on the GPU using Loop and Blinn's technique (at the start and end of Section 3).

An alternative technique that may work for you is described here: Thick Bezier Curves in OpenGL

EDIT: Ah, you want to avoid even the CPU triangulation - I should have read more closely.

One issue you have is the interface between the geometry shader and the fragment shader - the geometry shader will need to generate primitives (most likely triangles) that are then individually rasterized and filled via the fragment program.

In your case with constant thickness I think quite a simple triangulation will work - using Loop and Bling for all the "curved bits". When the two control triangles don't intersect it's easy. When they do, the part outside the intersection is easy. So the only hard part is within the intersection (which should be a triangle).

Within the intersection you want to shade a pixel only if both control triangles lead to it being shaded via Loop and Bling. So the fragment shader needs to be able to do texture lookups for both triangles. One can be as standard, and you'll need to add a vec2 varying variable for the second set of texture coordinates, which you'll need to set appropriately for each vertex of the triangle. As well you'll need a uniform "sampler2D" variable for the texture which you can then sample via texture2D. Then you just shade fragments that satisfy the checks for both control triangles (within the intersection).

I think this works in every case, but it's possible I've missed something.

Community
  • 1
  • 1
RD1
  • 3,305
  • 19
  • 28
  • It's easy to do if we throw in a little CPU help. Still, I'd rather try to do with with the GPU only. It doesn't _look_ impossible, it just feels like I'm too new to GLSL and graphics in general to complete a solution myself. – zneak Apr 04 '11 at 13:12
  • Oh, right. I didn't interpret the "GPU only" too strictly just because the CPU at least needs to be involved in passing the control points. – RD1 Apr 06 '11 at 01:36
  • I'd add that if the width is large enough the triangulation is trivial, and even when not it can be pre-calculated. But, I think I see your issue, and I'll edit with an alternative that I think is closer to what you want. – RD1 Apr 06 '11 at 01:57
  • Thanks for the new information. It seems your solution does what I tried to do but didn't figure how to. Still, being new to shaders, the GLSL code required to make it work is kind of unclear to me; do you think you could provide an example of how to use the two samplers? – zneak Apr 08 '11 at 01:06
  • @zneak - actually, sorry, I misremembered the Loop-Blinn algorithm. You don't need two samplers, you just need the two sets of uv-coordinates. Basically you can make both of them "varying vec2" variables. Then you need to specify their values at each vertex - this requires taking a matrix inverse in the geometry shader to map the "texture" coordinates from one triangle to another - basically this extrapolates the Loop-Blinn coordinates to any point. The bad news is that the matrix inverse plus simple triangulation is almost certainly slower on the GPU than the CPU. But, it can be done. – RD1 Apr 09 '11 at 12:17
  • See my another answer - it follows on from this one, but basically I had some of the details wrong in this answer. – RD1 Apr 10 '11 at 08:56
0

I don't know how to exactly solve this, but it's very interesting. I think you need every different processing unit in the GPU:

Vertex shader
Throw a normal line of points to your vertex shader. Let the vertex shader displace the points to the bezier.

Geometry shader

Let your geometry shader create an extra point per vertex.

foreach (point p in bezierCurve)
    new point(p+(0,thickness,0)) // in tangent with p1-p2        

Fragment shader
To stroke your bezier with a special stroke, you can use a texture with an alpha channel. You can check the alpha channel on its value. If it's zero, clip the pixel. This way, you can still make the system think it is a solid line, instead of a half-transparent one. You could apply some patterns in your alpha channel.

I hope this will help you on your way. You will have to figure out things yourself a lot, but I think that the Geometry shading will speed your bezier up.


Still for the stroking I keep with my choice of creating a GL_QUAD_STRIP and an alpha-channel texture.

Marnix
  • 6,384
  • 4
  • 43
  • 78
  • Vertex shaders are run before geometry shader, though I don't think it changes anything. – zneak Apr 06 '11 at 15:39