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):
Can someone help me fix this?
The typical approach for this would be the following...
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.
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)
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)