3
\$\begingroup\$

Let's say that you want to do bezier curve or surface evaluation on the GPU (fragment shader, geometry shader, compute shader, OpenCL, etc). Does anyone know of any interesting tricks of techniques to do so? Im looking for GPU specific techniques, but I already know the equations for n degree bezier curves, triangles and tetrahedrons. Thanks!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I've seen tricks for rendering bezier curves onto triangles/quadrilaterals using fragment shaders (eg. http.developer.nvidia.com/GPUGems3/gpugems3_ch25.html ) although there's some CPU-side preprocessing to determine the vertex spatial & texture coordinates, triangulation indices, etc. Is that type of technique of interest, or are you looking for solutions that take raw control points, and do all processing GPU-side? \$\endgroup\$
    – DMGregory
    Commented May 7, 2015 at 17:19
  • \$\begingroup\$ I'd say CPU pre-processing is ok, since the data has to get to the GPU somehow, and different data formats may lend themselves well to specific GPU techniques, but am looking for minimal CPU work (or a technique that focuses on the GPU/run time side at least, if that makes sense). Thanks for that info! \$\endgroup\$
    – Alan Wolfe
    Commented May 7, 2015 at 17:27
  • 1
    \$\begingroup\$ I think you want tesselation shader \$\endgroup\$ Commented May 7, 2015 at 19:23
  • \$\begingroup\$ definitely a valid solution too, thanks ratchet! Know any other ways, or interesting techniques / tricks? Sorry this is a bit broad... trying to find info about the variety of techniques people are using out there. \$\endgroup\$
    – Alan Wolfe
    Commented May 7, 2015 at 19:25

1 Answer 1

8
\$\begingroup\$

Using a tessellation shader, you can efficiently build useful representations of Bezier curves and surfaces all on the hardware. You can do this in conjunction with fully dynamic tessellation levels to implement automatic level-of-detail. It's a very simple concept. I'll illustrate using a Bezier curve tessellation although the principles extend trivially to Bezier surfaces.


First of all, you must define the input geometry in patches, which are made up of control points. This can be seen as a generalization of primitives and vertices. The important thing to note is that control points do not necessarily have to coincide with points on the resulting curve or surface. I will define a cubic Bezier curve as a 4-point patch:

Bezier curve control points

A cubic Bezier surface, then, would be defined as a 16-point patch. Note that only 2 of the control points for our Bezier curve patch will coincide with the actual geometry, and in the same way only 4 of the control points of the Bezier surface patch will.

In terms of draw call submission, the only difference is that, for example, in OpenGL one must specify the number of control points per patch and also draw with the GL_PATCHES mode.

glPatchParameteri(GL_PATCH_VERTICES, 4);
glDrawArrays(GL_PATCHES, 0, 4);

The next step is to write the vertex and tessellation control shaders. Despite the name, control points are sent through the vertex shader as if they are vertices, and they may store the same data. In a tessellation pipeline, the vertex shader's job is simplified quite a bit, as it only needs to pass on the control point data, possibly applying per-instance attributes and/or transforming them into view space.

In our simplified case, we will only be passing on the position of each control point. I will use GLSL to implement the example shaders.

layout(location = 0) in vec3 aPos;
out vec3 vPos;
void main() {
    vPos = aPos;
}

Note that we do not need to set gl_Position (yet).

The tessellation control shader is almost as simple, as we will be using hardcoded tessellation levels. The interesting thing about the tessellation control shader is that it may access all of the control points of the current patch, like how the geometry shader may access all of the vertices of the current primitive.

layout(vertices = 4) out; // 4 points per patch
in vec3 vPos[];
out vec3 tcPos[];
void main() {
    tcPos[gl_InvocationID] = vPos[gl_InvocationID];
    if(gl_InvocationID == 0) { // levels only need to be set once per patch
        gl_TessLevelOuter[0] = 1; // we're only tessellating one line
        gl_TessLevelOuter[1] = 100; // tessellate the line into 100 segments
    }
}

The reason two tessellation levels are needed is because the GPU doesn't tessellate individual lines, it tessellates what it calls isolines, which fill up a rectangular area in the tessellation basis space. Since we're only tessellating one line we don't need to worry about how that works.

Note: The tessellation control shader is actually optional. A shader as simple as this can be replaced by setting hardcoded values for the tessellation levels from the application itself, although that is typically not done in practice as it is not very useful.


The final new step is to write the tessellation evaluation shader, where we finally get into evaluating the Bezier curve (or surface). First, I'll define a few helper functions for evaluating linear, quadratic, and cubic Bezier curves.

vec3 bezier2(vec3 a, vec3 b, float t) {
    return mix(a, b, t);
}
vec3 bezier3(vec3 a, vec3 b, vec3 c, float t) {
    return mix(bezier2(a, b, t), bezier2(b, c, t), t);
}
vec3 bezier4(vec3 a, vec3 b, vec3 c, vec3, d, float t) {
    return mix(bezier3(a, b, c, t), bezier3(b, c, d, t), t);
}

Now we can get to writing the meat and potatoes of the shader, which ends up being quite simple.

layout(isolines) in;
in vec3 tcPos[];
uniform mat4 uMVP;
void main() {
    float t = gl_TessCoord.x;
    vec3 ePos = bezier4(tcPos[0], tcPos[1], tcPos[2], tcPos[3], t);
    gl_Position = uMVP * vec4(ePos, 1);
}

The tessellation evaluation shader ends up being yet another shader that can access any control point within the current patch. It is also where we do our vertex transformation and finally output the gl_Position to be rasterized. Geometry shaders and fragment shaders are agnostic of whether tessellation has been used, so I won't go over examples for them. Otherwise, the tessellation pipeline is now complete.

The gl_TessCoord built-in variable probably demands some explanation. Basically, it provides the coordinates in what I call the "tessellation basis space". This is different for each type of tessellation. With isolines, the x component specifies the distance along the line from 0-1 and the y component identifies which line it is. With quads, the x and y components specify where on the quad the vertex is, similar to UV coordinates, again within the 0-1 range for both. With triangles, the x, y, and z components are the barycentric coordinates of the vertex. All that means is that if you multiply each component by its corresponding control point and add them together and divide by the sum of the coordinates, you will get the tessellated vertex. A great property of barycentric coordinates is that they work well with homogeneous coordinates; you do not have to then divide by their sum as the w component will take care of that.

In the case of evaluating a Bezier (or any other parametric) curve or surface, the components are used as the parameters. In the case of doing displacement mapping, the components are used for sampling the displacement map along with the UV coordinates. Typically these two techniques are used together along with dynamic tessellation levels to provide a very rich automatic geometry LOD system.


Hopefully this is pretty self-explanatory in places where I didn't go into detail. If something doesn't make sense, just comment and I will add detail where needed.

Disclaimer: this code is completely untested but, to the best of my knowledge, should work.

\$\endgroup\$

You must log in to answer this question.

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