为世界添彩 - WebGL 中的颜色与着色器变量
欢迎回来!在第一部分,我们已经了解了 WebGL 的“渲染管线”,以及两个核心的“工人”——顶点着色器与片元着色器。我们通过 attribute
变量,成功地将顶点坐标从 JavaScript “投喂”给了顶点着色器。
今天的目标是:不仅仅告诉 GPU 在哪里画,还要告诉它画什么颜色。
新的问题:如何传递颜色?
你可能会想:“很简单,在片元着色器里直接改颜色不就行了?”
// 片元着色器 (旧代码)
void main() {// gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0); // 紫红色gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); // 现在改成绿色!
}
没错,但这依然是给整个图形上同一个颜色。我们想要的是一个五彩斑斓的三角形,比如顶角是红色,左下角是绿色,右下角是蓝色。
这意味着,颜色数据也必须是跟顶点一一对应的。既然位置是每个顶点都不同的 attribute
,那颜色自然也可以!
打通任督二脉:Varying 变量
好,假设我们成功地把每个顶点的颜色,像坐标一样,通过另一个 attribute
传给了顶点着色器。
但这里有个关键问题:顶点着色器只负责处理顶点(三个角),而片元着色器负责处理图形内部的每一个像素。顶点着色器知道三个角的颜色,它怎么告诉片元着色器“中间那些像素该涂什么颜色”呢?
答案就是今天的主角:varying
变量。
你可以把 varying
想象成一座桥梁,连接着顶点着色器和片元着色器。它的工作机制非常神奇:
- 你在顶点着色器里,给一个
varying
变量赋值(比如,把从attribute
接收到的顶点颜色赋给它)。 - GPU 开始绘制图形。当它填充两个顶点之间的像素时,它会自动地、线性地“插值”这个
varying
变量。 - 在片元着色器里,你接收这个
varying
变量。此时你收到的值,已经是 GPU 为你平滑计算好的、当前像素点“应该有”的值。
听起来有点抽象?看这张图:
我们只定义了三个角的颜色,中间所有像素的颜色都是 GPU 通过 varying
变量自动“渐变”出来的。这就是 WebGL 中创造平滑渐变的秘密!
GLSL 变量家族小结
现在,我们认识了 GLSL 中负责“通信”的三种主要变量类型:
attribute
:从 JavaScript 到顶点着色器。用于传递每个顶点都不同的数据,如位置、颜色、纹理坐标等。只能在顶点着色器中使用。varying
:从顶点着色器到片元着色器。用于传递经过插值计算的数据。必须在两个着色器中成对声明。uniform
:(我们下一篇会用到) 从 JavaScript 到两个着色器。用于传递对所有顶点都相同的数据,比如一个全局的变换矩阵或光照颜色。
搞清楚它们的职责,是掌握 GLSL 的关键。
开工!改造我们的代码
理论讲完了,我们来动手修改上一篇的代码。
1. 更新顶点着色器
我们需要它接收颜色数据,并把它传递给片元着色器。
// 新增一个 attribute 来接收颜色数据
attribute vec4 a_color;
// 新增一个 varying 变量作为桥梁
varying vec4 v_color;void main() {gl_Position = vec4(a_position, 0.0, 1.0);// 将接收到的颜色直接传递给 varying 变量v_color = a_color;
}
2. 更新片元着色器
它不再使用写死的颜色,而是接收从顶点着色器传来的、经过插值的颜色。
precision mediump float;// 声明同名的 varying 变量来接收数据
varying vec4 v_color;void main() {// 使用插值后的颜色作为当前像素的颜色gl_FragColor = v_color;
}
3. 大改版:JavaScript
JavaScript 的工作要多一些,因为它现在需要管理并发送两份数据(位置和颜色)。
一个常见的、性能更好的做法是数据交错 (Interleaving)。我们不再创建两个独立的数组,而是把一个顶点所有的数据(位置、颜色)都放在一起,存进一个大数组和同一个 Buffer 中。
[X1, Y1, R1, G1, B1, A1, X2, Y2, R2, G2, B2, A2, ...]
这样做的好处是数据更紧凑,GPU 读取效率更高。但这也意味着,我们需要更精确地告诉 WebGL 如何从这一个 Buffer 里,分别解析出位置和颜色。这就是 gl.vertexAttribPointer
函数中 stride
和 offset
参数大显身手的时候了。
stride
:告诉 WebGL “一整套顶点数据”有多长(占多少字节)。简单说,就是跳多远才能到下一组数据的开头。offset
:告诉 WebGL 当前这个attribute
的数据,在一套数据里是从哪里(第几个字节)开始的。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>WebGL 教程 2:彩色三角形</title><style>body { background-color: #333; color: #eee; text-align: center; }canvas { background-color: #000; border: 1px solid #555; }</style>
</head>
<body onload="main()"><h1>为世界添彩 - WebGL 中的颜色!</h1><canvas id="webgl-canvas" width="500" height="500"></canvas><!-- 顶点着色器代码 (已更新) --><script id="vertex-shader" type="x-shader/x-vertex">attribute vec2 a_position;// 新增:接收顶点的颜色 a_color (RGBA)attribute vec4 a_color;// 新增:varying 变量,用于将颜色传递给片元着色器varying vec4 v_color;void main() {gl_Position = vec4(a_position, 0.0, 1.0);// 将从 attribute 接收到的颜色,赋值给 varying 变量v_color = a_color;}</script><!-- 片元着色器代码 (已更新) --><script id="fragment-shader" type="x-shader/x-fragment">precision mediump float;// 新增:接收从顶点着色器传来的、已插值的颜色varying vec4 v_color;void main() {// 使用这个插值后的颜色作为像素的最终颜色gl_FragColor = v_color;}</script><script>function main() {// ... (步骤 1, 2, 3 与上一篇相同:获取上下文、编译链接着色器) ...const canvas = document.getElementById('webgl-canvas');const gl = canvas.getContext('webgl');if (!gl) { alert('WebGL not supported!'); return; }const vertexShaderSource = document.getElementById('vertex-shader').text;const fragmentShaderSource = document.getElementById('fragment-shader').text;const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);const program = createProgram(gl, vertexShader, fragmentShader);// 4. 找到 attribute 的位置const positionAttributeLocation = gl.getAttribLocation(program, "a_position");// 新增:找到 a_color 的位置const colorAttributeLocation = gl.getAttribLocation(program, "a_color");// 5. 创建 Buffer,并存入交错的数据const positionBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// 数据交错:[X1, Y1, R1, G1, B1, X2, Y2, R2, G2, B2, ... ]// 顶点1 (红色), 顶点2 (绿色), 顶点3 (蓝色)const positionsAndColors = [0.0, 0.5, 1.0, 0.0, 0.0, // 顶点1: 坐标(0, 0.5), 颜色(R=1, G=0, B=0)-0.5, -0.5, 0.0, 1.0, 0.0, // 顶点2: 坐标(-0.5, -0.5), 颜色(R=0, G=1, B=0)0.5, -0.5, 0.0, 0.0, 1.0 // 顶点3: 坐标(0.5, -0.5), 颜色(R=0, G=0, B=1)];gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positionsAndColors), gl.STATIC_DRAW);// ... (步骤 6, 7 与上一篇相同:清空画布、启用程序) ...gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);gl.clearColor(0.1, 0.1, 0.1, 1.0);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(program);// 8. 告诉 WebGL 如何从唯一的 Buffer 中解析出两份数据// 启用两个 attributegl.enableVertexAttribArray(positionAttributeLocation);gl.enableVertexAttribArray(colorAttributeLocation);// 再次绑定 Buffer (好习惯)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);const FSIZE = (new Float32Array()).BYTES_PER_ELEMENT; // 每个浮点数占4个字节const STRIDE = 5 * FSIZE; // 一套完整顶点数据(X,Y,R,G,B)的总字节数// ---- 指示 a_position 如何解析数据 ----const positionSize = 2; // 每个顶点的位置由 2 个分量组成const positionType = gl.FLOAT;const positionNormalize = false;const positionOffset = 0 * FSIZE; // 位置数据从一套数据的开头(偏移量0)开始gl.vertexAttribPointer(positionAttributeLocation,positionSize,positionType,positionNormalize,STRIDE, // Stride: 告诉它下一组位置数据在多远之后positionOffset // Offset: 告诉它这组数据从哪里开始);// ---- 指示 a_color 如何解析数据 ----const colorSize = 3; // 每个顶点的颜色由 3 个分量组成 (RGB)const colorType = gl.FLOAT;const colorNormalize = false;const colorOffset = 2 * FSIZE; // 颜色数据从偏移量2个浮点数大小之后开始gl.vertexAttribPointer(colorAttributeLocation,colorSize,colorType,colorNormalize,STRIDE, // Stride: 同样是一套完整顶点数据的长度colorOffset // Offset: 告诉它颜色数据在位置数据之后);// 9. 绘制!const primitiveType = gl.TRIANGLES;const drawOffset = 0;const count = 3;gl.drawArrays(primitiveType, drawOffset, count);}// --- 辅助函数 (与上一篇相同) ---function createShader(gl, type, source) {const shader = gl.createShader(type);gl.shaderSource(shader, source);gl.compileShader(shader);if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;console.error("Shader compile error:", gl.getShaderInfoLog(shader));gl.deleteShader(shader);}function createProgram(gl, vertexShader, fragmentShader) {const program = gl.createProgram();gl.attachShader(program, vertexShader);gl.attachShader(program, fragmentShader);gl.linkProgram(program);if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;console.error("Program link error:", gl.getProgramInfoLog(program));gl.deleteProgram(program);}</script>
</body>
</html>
总结与展望
现在,你的浏览器里应该出现了一个色彩斑斓的三角形,从红色的顶点平滑地过渡到绿色和蓝色。非常漂亮,不是吗?
今天,我们掌握了 WebGL 中数据流转的又一关键环节:
- 学会了使用
varying
变量,在顶点着色器和片元着色器之间架起了沟通的桥梁。 - 理解了 GPU 是如何自动插值
varying
变量,从而创造出平滑的渐变效果。 - 实践了数据交错的技巧,以及如何使用
stride
和offset
参数,让 WebGL 从同一个 Buffer 中精确地解析出多种 attribute 数据。
我们的三角形已经有了形状和颜色,但它还是一个“死”的东西。在下一篇文章中,我们将学习 2D 图形学中最核心的概念之一——矩阵变换。我们将引入第三位变量家族成员 uniform
,并通过它让我们的三角形动起来:平移、旋转、缩放!
敬请期待 《第 3 篇:让图形动起来 - WebGL 2D 变换》!