yomotsu
yomotsu

Reputation: 1472

How to draw Cubic Bezier in WebGL

I'm trying to make SVG (and other 2D vector graphics) renderer in WebGL.
So far, I've figured out how to draw Quadratic Bezier with a triangle.

Here is the code.

var createProgram = function ( vsSource, fsSource ) {

  var vs = gl.createShader( gl.VERTEX_SHADER );
  gl.shaderSource( vs, vsSource );
  gl.compileShader( vs );

  var fs = gl.createShader( gl.FRAGMENT_SHADER );
  gl.shaderSource( fs, fsSource );
  gl.compileShader( fs );

  var program = gl.createProgram();
  gl.attachShader( program, vs );
  gl.attachShader( program, fs );
  gl.linkProgram( program );

  return program;

}

var vsSource = `
precision mediump float;
attribute vec2 vertex;
attribute vec2 attrib;
varying vec2 p;
void main(void) {
  gl_Position = vec4(vertex, 0.0, 1.0);
  p = attrib;
}
`;

var fsSource = `
precision mediump float;
varying vec2 p;
void main(void) {
  if (p.x*p.x - p.y > 0.0) {
    // discard;
    gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
  } else {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
}
`;


var canvas = document.querySelector( 'canvas' );
var gl = canvas.getContext( 'webgl' ) ||
         canvas.getContext( 'experimental-webgl' );

gl.clearColor( 0.5, 0.5, 0.5, 1.0 );

var shapeData = [
  -0.5, 0,
   0.5, 0,
   0,   1
];

var curveAttr = [
  0,   0,
  1,   1,
  0.5, 0
];



var program = createProgram( vsSource, fsSource );
gl.useProgram( program );
var vertexLoc1 = gl.getAttribLocation( program, 'vertex' );
var attribLoc1 = gl.getAttribLocation( program, 'attrib' );

gl.clear( gl.COLOR_BUFFER_BIT );



gl.useProgram( program );
gl.enableVertexAttribArray(vertexLoc1);
gl.enableVertexAttribArray(attribLoc1);

var vertexBuffer1 = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vertexBuffer1 );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( shapeData ), gl.STATIC_DRAW );
gl.vertexAttribPointer(vertexLoc1, 2, gl.FLOAT, false, 0, 0);

var vertexBuffer2 = gl.createBuffer();
gl.bindBuffer( gl.ARRAY_BUFFER, vertexBuffer2 );
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( curveAttr ), gl.STATIC_DRAW );
gl.vertexAttribPointer(attribLoc1, 2, gl.FLOAT, false, 0,0);

gl.drawArrays( gl.TRIANGLES, 0, shapeData.length / 2 );
<canvas></canvas>

My question is how to draw Cubic Bezier, like above.

I guess it should be done with 2 or a few triangles. And also, I understand there is noway to convert Cubic Bezier to Quadratic.

Upvotes: 2

Views: 2887

Answers (1)

MvG
MvG

Reputation: 60968

Why quadratic works

Let's first understand why this works for quadratic. As you know, a quadratic Bézier curve is described as

(1−t)2∙A + 2 t(1−t)∙B + t2∙C .

Now if you plug the curve attributes into this formula, you get

(1−t)2∙(0, 0) + 2 (1−t)t∙(1/2, 0) + t2∙(1, 1) = (0, 0) + (tt2, 0) + (t2, t2) = (t, t2)

So by squaring the first coordinate and subtracting the second, you always get 0 for a point on the curve.

Cubic is more difficult

Triangles are particularly easy. If you have a triangle with corners A, B and C, then for any point P inside the triangle (or in fact anywhere in the plane) there is a unique way to write P as αABC with α+β+γ=1. This is essentially just a transformation between different 2D coordinate systems.

With cubic Bézier curves you have four defining points. The convex hull of these is a quadrilateral. While the parametrized representation of the curve still defines it in terms of linear combinations of these four points, this process is no longer easily reversible: you can't take a point in the plane and decompose it uniquely into the coefficients of the linear combination. Even if you take homogeneous coordinates (i.e. projective interpolation for your parameters), you still have to have your corners in a plane if you want to avoid seams at the inner triangle boundaries. Since you can get cubic Bézier curves to self-intersect, there can even be points on the Bézier curve which correspond to more than one value of t.

One way to tackle cubic

What you can do is have a closer look at the implicit representation. When you have

Px = (1−t)3Ax + 3 (1−t)2tBx + 3 (1−t)t2Cx + t3Dx
Py = (1−t)3Ay + 3 (1−t)2tBy + 3 (1−t)t2Cy + t3Dy

you can use a computer algebra system (or manual resultant computation) to eliminate t from these equations, resulting in a sixth-degree equation in all the other variables which characterizes the fact that the point (Px, Py) lies on the curve. To simplify things, you can choose an affine coordinate system such that
Ax = Ay = Bx = Dy = 0, By = Dx = 1
in other words you use A as the origin, AD as the x unit vector and AB as the y unit vector. Then with respect to this coordinate system, point C has some specific coordinates (Cx, Cy) which you'll have to compute. If you use these coordinates as attributes for the vertices, then the linear interpolation of that attribute results in (Px, Py), which are the coordinates of the current point with respect to that coordinate system.

Using these coordinates, the condition for the point to lie on the curve is, according to my Sage computation, as follows:

0 = (-27*Cy^3 + 81*Cy^2 - 81*Cy + 27)*Px^3
  + (81*Cx*Cy^2 - 162*Cx*Cy - 27*Cy^2 + 81*Cx + 54*Cy - 27)*Px^2*Py
  + (-81*Cx^2*Cy + 81*Cx^2 + 54*Cx*Cy - 54*Cx - 9*Cy + 9)*Px*Py^2
  + (27*Cx^3 - 27*Cx^2 + 9*Cx - 1)*Py^3
  + (27*Cy^3 + 81*Cx*Cy - 81*Cy^2 + 81*Cy - 54)*Px^2
  + (-54*Cx*Cy^2 - 81*Cx^2 + 81*Cx*Cy + 81*Cx + 27*Cy - 54)*Px*Py
  + (27*Cx^2*Cy - 9*Cx)*Py^2
  + (-81*Cx*Cy + 27)*Px

The things in parentheses only depend on the coordinates of the control points, so they could become uniforms or per-face attributes in your shader code. In the fragment shader you'd plug in Px and Py from the interpolated position attribute, and use the sign of the result to decide what color to use.

There is a lot of room for improvements. It might be that a more clever way of choosing the coordinate system leads to a simpler formula. It might be that such a simpler formula, or perhaps even the formula above, could be simplified a lot by using the distributive law in a clever way. But I don't have time now to hunt for better formulations, and the above should be enough to get you going.

There are also some problems with my choice of coordinate system in specific situations. If B lies on the line AD, you may want to swap the roles of A and D and of B and C. If both B and C lie on that line, then the Bézier curve is itself a line, which is another special case although it's easy to implement. If A and D are the same point, you could write a different equation using AB and AC as the basis vectors. Distinguishing all these special cases, with some leeway for numeric errors, can be quite painful. You could avoid that by e.g. just making A the origin, essentially just translating your coordinate system. The resulting equation would be more complicated, but also more general since it would cover all the special cases simultaneously.

Upvotes: 3

Related Questions