WebGL 2工作原理
WebGL在GPU上的工作基本上分为两部分
- 第一部分是将顶点(或数据流)转换到裁剪空间坐标
- 就是将传入的位置坐标,转换为0-1的值,并绘制出来
- 每个顶点的坐标(传入的值)通过顶点着色器计算转换为裁剪空间坐标
- 转换后的值设置为一个特殊的gl_Position变量
- 这个变量就是该顶点转换到裁剪空间中的坐标值
假设你正在画三角形,顶点着色器每完成三次顶点处理,WebGL就会用这三个顶点画一个三角形
- 第二部分是基于第一部分的结果绘制像素点(填充颜色)
- 计算出这三个顶点对应的像素后,就会光栅化这个三角形,
- “光栅化”其实就是“用像素画出来” 的花哨叫法。
对于每一个像素,它会调用你的片段着色器询问你使用什么颜色。
你通过给片段着色器的一个特殊变量gl_FragColor设置一个颜色值,实现自定义像素颜色。
处理每个像素时片段着色器可用信息很少,幸运的是我们可以给它传递更多信息
想要从顶点着色器传值到片段着色器,我们可以定义“可变量(varyings)”。
绘制一个三角形
(在此代码仅作演示用,别想着直接复制就运行)
js代码
// 定义一个三角形坐标填充到缓冲里 function setGeometry(gl) { gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ 0, -100, 150, 125, -175, 100]), gl.STATIC_DRAW); } // 绘制场景 function drawScene() { ... // 绘制几何体 var primitiveType = gl.TRIANGLES; var offset = 0; var count = 3; //说明要画几个点 gl.drawArrays(primitiveType, offset, count); }
GLSL代码
在顶点着色器(vertex-shader)中定义一个varying(可变量)用来给片段着色器传值。
varying vec4 v_color; //传递值的变量 ... void main() { // 将位置和矩阵相乘 gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); // 从裁减空间转换到颜色空间 // 裁减空间范围 -1.0 到 +1.0 // 颜色空间范围 0.0 到 1.0 v_color = gl_Position * 0.5 + 0.5; }
在片段着色器中定义同名varying变量。
precision mediump float; varying vec4 v_color; //传递值的变量 void main() { gl_FragColor = v_color; }
WebGL会将同名的可变量从顶点着色器输入到片段着色器中。颜色随位置变化。
在这里可以这样理解,两个同名的变量指向了同一个内存地址,所以其值是共用的(自认为)
在此两个着色器的运行逻辑:
- 计算了三个顶点,调用了三次顶点着色器,所以也只计算出了三个颜色值
- (必须先顶点再片段着色器吗)(并行运行的吗?不应该三角形只填充一种颜色吗)
- WebGL先获得顶点着色器中计算的三个颜色值,在光栅化三角形时将会根据这三个值进行插值。 每一个像素在调用片段着色器时,可变量的值是与之对应的插值。
运行之后的效果
解析
开始详细分析:
首先这是我们输入的三个顶点值
运行三次顶点着色器程序,转换后的三个裁剪空间坐标值
通过这行代码 v_color = gl_Position * 0.5 + 0.5; 得到了三个颜色值。
不想写了先歇会(2025.4.22)
绘制两个三角形
js
// 寻找顶点着色器中需要的数据 var positionLocation = gl.getAttribLocation(program, "a_position"); var colorLocation = gl.getAttribLocation(program, "a_color"); ... // 给颜色数据创建一个缓冲 var colorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); // 设置颜色 setColors(gl); ... // 给矩形的两个三角形 // 设置颜色值并发到缓冲 function setColors(gl) { // 生成两个随机颜色 var r1 = Math.random(); var b1 = Math.random(); var g1 = Math.random(); var r2 = Math.random(); var b2 = Math.random(); var g2 = Math.random(); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( [ r1, b1, g1, 1, r1, b1, g1, 1, r1, b1, g1, 1, r2, b2, g2, 1, r2, b2, g2, 1, r2, b2, g2, 1]), gl.STATIC_DRAW); }
js(不同的逻辑部分)
//*接下来渲染时,请从我绑定的缓冲区中读取颜色数据,并传递给着色器中的 aColor 属性。* gl.enableVertexAttribArray(colorLocation); // 绑定颜色缓冲 gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); // 告诉颜色属性怎么从 colorBuffer (ARRAY_BUFFER) 中读取颜色值 var size = 4; // 每次迭代使用4个单位的数据 var type = gl.FLOAT; // 单位数据类型是32位的浮点型 var normalize = false; // 不需要归一化数据 var stride = 0; // 0 = 移动距离 * 单位距离长度sizeof(type) // 每次迭代跳多少距离到下一个数据 var offset = 0; // 从绑定缓冲的起始处开始 gl.vertexAttribPointer( colorLocation, size, type, normalize, stride, offset)
其中的关键是 gl.enableVertexAttribArray(colorLocation);将缓冲区中的值赋值给colorLocation
在这里会有一个疑问,为什么 gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); 调用了两次
叮!快来看看我和文心一言的奇妙对话~点击链接 https://yiyan.baidu.com/share/vUIFGNyXpM
第一次调用
- 创建一个新的缓冲区对象 colorBuffer,用于存储顶点的颜色数据。
- 通过 gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer) 将 colorBuffer 绑定到 WebGL 的 ARRAY_BUFFER 目标。
- 这一步告诉 WebGL:“接下来所有对 ARRAY_BUFFER 的操作(如 gl.bufferData)都会作用到 colorBuffer 上。”
第二次调用
- 在渲染每一帧时,需要重新绑定 colorBuffer 到 ARRAY_BUFFER,以便着色器能够从该缓冲区读取颜色数据。
- 这一步是动态渲染的关键:每次调用 drawScene 时,WebGL 需要知道从哪里获取顶点的颜色数据。
为什么需要重新绑定?
- WebGL 的状态机特性要求每次渲染前显式设置所有必要状态(如绑定的缓冲区、启用的属性等)。
- 即使 colorBuffer 之前绑定过,其他操作(如绑定 positionBuffer)可能会覆盖当前绑定状态,因此需要重新绑定以确保正确性。
// 画几何体 var primitiveType = gl.TRIANGLES; var offset = 0; var count = 6; gl.drawArrays(primitiveType, offset, count);
GLSL代码
顶点着色器
attribute vec2 a_position; attribute vec4 a_color; uniform mat3 u_matrix; varying vec4 v_color; void main() { // Multiply the position by the matrix. gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1); // Copy the color from the attribute to the varying. v_color = a_color; }
片段着色器
precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }
buffer和attribute的代码是干什么的?
缓冲操作是在GPU上获取顶点和其他顶点数据的一种方式。 gl.createBuffer创建一个缓冲;gl.bindBuffer是设置缓冲为当前使用缓冲; gl.bufferData将数据拷贝到缓冲,这个操作一般在初始化完成。
一旦数据存到缓冲中,还需要告诉WebGL怎么从缓冲中提取数据传给顶点着色器的属性。
要做这些,首先需要获取WebGL给属性分配的地址,如下方代码所示
// 询问顶点数据应该放在哪里 var positionLocation = gl.getAttribLocation(program, "a_position"); var colorLocation = gl.getAttribLocation(program, "a_color");
这一步一般也是在初始化部分完成。
一旦知道了属性的地址,在绘制前还需要发出三个命令。
//这个命令是告诉WebGL我们想从缓冲中提供数据。 gl.enableVertexAttribArray(location); //这个命令是将缓冲绑定到 ARRAY_BUFFER 绑定点,它是WebGL内部的一个全局变量。 gl.bindBuffer(gl.ARRAY_BUFFER, someBuffer); gl.vertexAttribPointer( location, numComponents, typeOfData, normalizeFlag, strideToNextPieceOfData, offsetIntoBuffer); /`这个命令告诉WebGL从 ARRAY_BUFFER 绑定点当前绑定的缓冲获取数据。 每个顶点有几个单位的数据(1 - 4),单位数据类型是什么(BYTE, FLOAT, INT, UNSIGNED_SHORT, 等等...), stride 是从一个数据到下一个数据要跳过多少位,最后是数据在缓冲的什么位置。 单位个数永远是 1 到 4 之间。 如果每个类型的数据都用一个缓冲存储,stride 和 offset 都是 0 。 对 stride 来说 0 表示 “用符合单位类型和单位个数的大小”。 对 offset 来说 0 表示从缓冲起始位置开始读取。 它们使用 0 以外的值时会复杂得多,虽然这样会取得一些性能能上的优势, 但是一般情况下并不值得,除非你想充分压榨WebGL的性能。 希望这些关于缓冲和属性的内容对你来说讲的足够清楚。`/
(此文章就渲染原理进行了简单的讲述,实际的实现代码请移步原文章)
参考:
WebGL 工作原理
WEBGL原理_webgl渲染原理-CSDN博客
- WebGL先获得顶点着色器中计算的三个颜色值,在光栅化三角形时将会根据这三个值进行插值。 每一个像素在调用片段着色器时,可变量的值是与之对应的插值。
- 第二部分是基于第一部分的结果绘制像素点(填充颜色)