Search code examples
glslshaderwebglbezier

How to draw multiple Bézier curves using WebGL and only one vertex shader?


I want to implement a WebGL program capable of displaying multiple cubic Bézier curves on a canvas by using only one vertex shader. I want this shader to compute a given number of points on each curve and to repeat the process for each curve.

Say the number of cubic Bézier curves to draw is N. A cubic Bézier curve being defined by 4 control points, I will have N * 4 control points to give to the vertex shader.

Say the number of points to compute for each curve is precision. I compute a list ts which contains precision interpolation values on the curve (from 0 to 1). Say precision is 6, I would end up with the following ts list: [0, 0.2, 0.4, 0.6, 0.8, 1].

Using these two buffers (the list of N * 4 control points, and the ts list), I want the vertex shader to compute all interpolation points for each of the N curves in parallel.

So for each t in ts, and each curve, I want to compute the position of the point curve(t), where:

curve(t) = (1 - t)³ * p0  +  3 * (1 - t)² * t * p1  +  3 * (1 - t) * t² * p2  +  t³ * p3`

with p0, p1, p2 and p4 are the 4 control points defining curve.

My vertex shader code is the following:

attribute vec2 p0;
attribute vec2 p1;
attribute vec2 p2;
attribute vec2 p3;
attribute float t;

uniform mat4 transformation;

varying vec4 fragmentColor;

void main() {
    float tSquared = t * t;
    float tCube = tSquared * t;
    float _t = 1.0 - t;
    float _tSquared = _t * _t;
    float _tCube = _tSquared * _t;
    vec4 p = transformation * vec4(_tCube * p0 + 3.0 * _tSquared * t * p1 + 3.0 * _t * tSquared * p2 + tCube * p3, 1.0, 1.0);
    fragmentColor = vec4(1.0) - p;
    gl_PointSize = 4.0;
    gl_Position = p;
}

The WebGL code I tried is the following:

let vertexShader = context.createShader(context.VERTEX_SHADER)
context.shaderSource(vertexShader, await (await fetch('./shaders/multi-bezier-points-computer.glsl')).text())
context.compileShader(vertexShader)
if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) {
    console.error(context.getShaderInfoLog(vertexShader))
    context.deleteShader(vertexShader)
    return
}

let fragmentShader = context.createShader(context.FRAGMENT_SHADER)
context.shaderSource(fragmentShader, await (await fetch('./shaders/fragment-shader.glsl')).text())
context.compileShader(fragmentShader)
if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) {
    console.error(context.getShaderInfoLog(fragmentShader))
    context.deleteShader(fragmentShader)
    return
}

const program: WebGLProgram = context.createProgram()
context.attachShader(program, vertexShader)
context.attachShader(program, fragmentShader)
context.linkProgram(program)
if (!context.getProgramParameter(program, context.LINK_STATUS)) {
    console.error(context.getProgramInfoLog(program))
    context.deleteProgram(program)
    return
}

context.useProgram(program)

const precision = 30

let controlPoints = new Float32Array([
    -0.5, 0.5,
    -0.25, -0.5,
    0.25, 0.5,
    0.5, -0.5,
    0.5, -0.5,
    0.45, 0.5,
    -0.1, -0.2,
    -0.7, 0.7
])
const controlPointsBuffer = context.createBuffer()
context.bindBuffer(context.ARRAY_BUFFER, controlPointsBuffer)
context.bufferData(context.ARRAY_BUFFER, controlPoints, context.STATIC_DRAW)
const p0 = context.getAttribLocation(program, 'p0')
const p1 = context.getAttribLocation(program, 'p1')
const p2 = context.getAttribLocation(program, 'p2')
const p3 = context.getAttribLocation(program, 'p3')
const stride = 2 * Float32Array.BYTES_PER_ELEMENT
context.vertexAttribPointer(p0, 2, context.FLOAT, false, stride, 0)
context.vertexAttribPointer(p1, 2, context.FLOAT, false, stride, stride)
context.vertexAttribPointer(p2, 2, context.FLOAT, false, stride, 2 * stride)
context.vertexAttribPointer(p3, 2, context.FLOAT, false, stride, 3 * stride)
context.vertexAttribDivisor(p0, precision)
context.vertexAttribDivisor(p1, precision)
context.vertexAttribDivisor(p2, precision)
context.vertexAttribDivisor(p3, precision)
context.enableVertexAttribArray(p0)
context.enableVertexAttribArray(p1)
context.enableVertexAttribArray(p2)
context.enableVertexAttribArray(p3)

let ts = new Float32Array(precision * Math.ceil(controlPoints.length / 12))
for (let j = 0; j < Math.ceil(controlPoints.length / 12); j++) {
    for (let i = 0; i < precision; i++) {
        ts[i + j * precision] = i / (precision - 1)
    }
}
const tsBuffer = context.createBuffer()
context.bindBuffer(context.ARRAY_BUFFER, tsBuffer)
context.bufferData(context.ARRAY_BUFFER, ts, context.STATIC_DRAW)
let t = context.getAttribLocation(program, 't')
context.vertexAttribPointer(t, 1, context.FLOAT, false, 0, 0)
context.enableVertexAttribArray(t)
const transformation = context.getUniformLocation(program, 'transformation')
let matrix = [
    Math.cos(0), -Math.sin(0), 0, 0,
    Math.sin(0), Math.cos(0), 0, 0,
    0, 0, 0, 0,
    0, 0, 0, 1
]
context.uniformMatrix4fv(transformation, false, matrix)
context.clear(context.COLOR_BUFFER_BIT)
context.drawArrays(context.POINTS, 0, ts.length)

With this code, only the points of the first curve are being drawn, but I can't figure out why. I suppose that it has to do with the vertexAttribPointer and vertexAttribDivisor parameters but haven't found the reason. This image shows the result I'm getting (i.e.: only the points of the first curve are drawn): Only the points for the first curve are drawn

Can someone help me fix this?


Solution

  • The typical approach for this would be the following...

    1. Populate a vertex buffer with the basis coefficients. (You're doing 't' values here, which is fine, but you may as well fold the computation into the buffer given that you'll be repeating the computation quite a lot!).
    let ts = new Float32Array(precision * 4)
    for(let i = 0; i < precision; ++i) {
       let t = i / (precision - 1.0)
       let it = 1.0 - t
       ts[4*i + 0] = it*it*it
       ts[4*i + 1] = 3.0*t*it*it
       ts[4*i + 2] = 3.0*t*t*it
       ts[4*i + 3] = t*t*t 
    }
    

    Specify this vertex attribute as a vec4 with a vertex attribute divisor of zero.

    1. Populate a vertex buffer with all of the control points. Set top the stride for each vertex accordingly, but with a vertex attribute divisor of 1! i.e. This is wrong:

    context.vertexAttribDivisor(p0, precision)

    The divisor is the number of instances it takes before the value advances a step. You seem to have confused this to mean the number of vertices in each instance!

    You should be wanting:

    context.vertexAttribDivisor(p0, 1)
    context.vertexAttribDivisor(p1, 1)
    context.vertexAttribDivisor(p2, 1)
    context.vertexAttribDivisor(p3, 1)
    
    1. Finally, make sure you render the vertices using glDrawArraysInstanced, e.g. this is very wrong:

    context.drawArrays(context.POINTS, 0, ts.length)

    You want to draw multiple curves in a single batch, so use the instanced method instead (where the last parameter specifies the number of curves to draw, i.e. control_point array / 4*2 floats)

    context.drawArraysInstanced(context.POINTS, 0, ts.length, controlPoints.length / 8)