WebGL鼠标事件绘制像素:顶点缓冲区不足致绘制调用失败
我正在学习WebGL,想了解如何把鼠标位置事件传递给GPU,目标是做一个简单的绘图程序:当WebGL Canvas触发mouseover或mousedown事件时,给对应位置的像素上色。我知道用2D Canvas实现更简单,但目的是学习JS与GPU通过缓冲区(buffer)和uniforms通信的方式。
我的思路是每次鼠标移动时创建新缓冲区、绑定缓冲区、设置数据并调用drawArrays(参考Stack Overflow建议,不修改现有缓冲区)。相关代码如下:
const canvas = document.getElementById('canvas'); const gl = getGlContext('canvas', { preserveDrawingBuffer: true }); // 编译并附加着色器的工具函数 const { program } = setup(gl, vert1, frag1); gl.useProgram(program); // 初始化占位缓冲区——如果没有这个,WebGL会提示没有绑定缓冲区,哪怕之后我会创建并绑定另一个? const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); const positionAttributeLocation = gl.getAttribLocation(program, 'a_position'); const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution'); gl.enableVertexAttribArray(positionAttributeLocation); gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); // 设置分辨率 gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); canvas.addEventListener('mousemove', (e) => { const x = e.x - canvas.offsetLeft + 1; const y = e.y - canvas.offsetTop + 1; const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([x, y]), gl.STATIC_DRAW); gl.vertexAttrib2f(positionAttributeLocation, x, y); gl.drawArrays(gl.POINTS, 0, 3); });
运行代码后,鼠标移入Canvas时画布变空白,且出现错误:
Vertex buffer is not big enough for the draw call
我原本以为只需要长度为2的小型缓冲区存储单个[x,y]坐标,绑定缓冲区并设置GPU中的位置顶点属性后,绘制调用就能按照片段着色器设置的颜色绘制该像素坐标。显然我对缓冲区及缓冲区数据的设置理解有误,请问事件监听器中的代码哪里出错了?
附录:着色器代码
顶点着色器:
// 顶点着色器 // 在着色器内将像素坐标转换为裁剪空间,而非在JS中处理 attribute vec2 a_position; uniform vec2 u_resolution; void main() { // 将位置从像素转换为0.0到1.0 vec2 zeroToOne = a_position / u_resolution; // 从0->1转换为0->2 vec2 zeroToTwo = zeroToOne * 2.0; // 从0->2转换为-1->+1(裁剪空间) vec2 clipSpace = zeroToTwo - 1.0; gl_Position = vec4(clipSpace, 0.0, 1.0); }
片段着色器:
// 片段着色器 precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = vec4(1,0,1,1); }
问题根源与修正方案
你的代码存在两个关键错误:
drawArrays顶点数量参数不匹配
调用gl.drawArrays(gl.POINTS, 0, 3)表示要绘制3个顶点,但你传入的缓冲区仅包含1个顶点的数据(Float32Array([x, y])是2个浮点数,对应1个vec2顶点)。GPU尝试读取3个顶点的内容,缓冲区数据量不足,因此抛出"Vertex buffer is not big enough"错误。混用缓冲区属性与常量属性
你先通过gl.vertexAttribPointer将a_position设置为从缓冲区读取数据,随后又调用gl.vertexAttrib2f直接给该属性赋值常量。这会让a_position变成常量属性(不再从缓冲区读取数据),但之前开启的gl.enableVertexAttribArray(positionAttributeLocation)仍会让WebGL尝试从缓冲区读取数据,进而触发错误。
另外,每次鼠标移动都创建新缓冲区会造成内存浪费,复用单个缓冲区更高效。
修正后的代码示例
const canvas = document.getElementById('canvas'); const gl = getGlContext('canvas', { preserveDrawingBuffer: true }); const { program } = setup(gl, vert1, frag1); gl.useProgram(program); // 创建单个缓冲区复用 const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // 预先分配缓冲区空间,后续更新数据 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(2), gl.DYNAMIC_DRAW); const positionAttributeLocation = gl.getAttribLocation(program, 'a_position'); const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution'); gl.enableVertexAttribArray(positionAttributeLocation); gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height); // WebGL坐标系原点在左下角,需反转鼠标y坐标 canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = canvas.height - (e.clientY - rect.top); // 绑定缓冲区并更新数据 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array([x, y])); // 绘制1个点 gl.drawArrays(gl.POINTS, 0, 1); });
额外说明
- WebGL的y轴原点在左下角,而鼠标事件的y轴原点在左上角,必须反转y坐标才能对应画布的正确位置。
- 使用
gl.bufferSubData更新缓冲区数据比重复创建新缓冲区更高效。 - 若要保留历史绘制内容,
preserveDrawingBuffer: true可以实现,但更推荐的方式是记录所有历史点,每一帧重绘(前者会影响WebGL性能)。
内容的提问来源于stack exchange,提问作者Seth Lutske

