如何用单个顶点着色器在WebGL中绘制多条三次贝塞尔曲线?
多曲线WebGL绘制问题:仅显示第一条三次贝塞尔曲线
我要实现一个WebGL程序,通过单个顶点着色器在画布上显示多条三次贝塞尔曲线,让着色器为每条曲线计算指定数量的插值点。
已知条件:
- 曲线数量为N,每条曲线由4个控制点定义,需传入N*4个控制点
- 每条曲线的计算精度为
precision,生成包含precision个0到1之间插值值的ts数组(比如precision=6时,ts为[0,0.2,0.4,0.6,0.8,1])
需求:通过控制点缓冲区和ts缓冲区,让顶点着色器并行计算N条曲线的所有插值点,计算遵循三次贝塞尔公式:
curve(t) = (1 - t)³ * p0 + 3*(1 - t)²tp1 + 3*(1 - t)t²p2 + t³*p3
当前问题:编写了顶点着色器和WebGL代码后,运行仅显示第一条曲线的点,推测问题出在vertexAttribPointer和vertexAttribDivisor参数设置上,请求排查修复。
顶点着色器代码
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; }
WebGL实现代码
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)
问题排查与修复
1. 核心错误点
- 控制点缓冲区步长(stride)错误:当前步长仅设置为单个点的字节数,导致控制点被交叉读取,无法区分不同曲线的控制点组。
- 顶点属性除数(vertexAttribDivisor)错误:给控制点属性设置的除数为
precision,逻辑上应该是每绘制一条曲线(一个实例)切换一次控制点,除数应为1。 - 曲线数量计算错误:用
controlPoints.length / 12计算曲线数,实际每条曲线占8个浮点数(4个点×2个分量),应改为controlPoints.length / 8。 - 绘制API错误:使用普通
drawArrays无法配合实例化属性,需改用drawArraysInstanced实现多实例绘制。
2. 修改后的关键代码
控制点缓冲区配置修正
// 步长改为每条曲线的总字节数:4个点×2个分量×每个float的字节数 const stride = 4 * 2 * Float32Array.BYTES_PER_ELEMENT; // 修正每个控制点的偏移量,对应组内的第0/1/2/3个点 context.vertexAttribPointer(p0, 2, context.FLOAT, false, stride, 0); context.vertexAttribPointer(p1, 2, context.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT); context.vertexAttribPointer(p2, 2, context.FLOAT, false, stride, 4 * Float32Array.BYTES_PER_ELEMENT); context.vertexAttribPointer(p3, 2, context.FLOAT, false, stride, 6 * Float32Array.BYTES_PER_ELEMENT); // 设置除数:每1个实例(一条曲线)切换一次控制点 context.vertexAttribDivisor(p0, 1); context.vertexAttribDivisor(p1, 1); context.vertexAttribDivisor(p2, 1); context.vertexAttribDivisor(p3, 1);
ts数组生成修正
// 正确计算曲线数量 const curveCount = controlPoints.length / (4 * 2); let ts = new Float32Array(precision * curveCount); for (let j = 0; j < curveCount; j++) { for (let i = 0; i < precision; i++) { ts[i + j * precision] = i / (precision - 1); } }
绘制API修正
// 用实例化绘制API,参数:绘制模式、起始顶点、每个实例的顶点数、实例数(曲线数) context.drawArraysInstanced(context.POINTS, 0, precision, curveCount);
3. 修正逻辑说明
- 实例化绘制配合
vertexAttribDivisor(1),让每组控制点对应一个曲线实例,每个实例绘制precision个顶点(对应一组t值)。 - 修正步长后,控制点缓冲区会按曲线组读取数据,避免交叉混淆。
- 实例化绘制自动处理多曲线的顶点分配,无需手动扩展顶点数。
内容的提问来源于stack exchange,提问作者Guillaume Voiron
相关产品推荐
相关产品推荐

