今日牛牛:
在第3章中我们曾绘制过了一个单色三角形。而这一节,我们将为三角形的每个顶点指定一个颜色,然后WebGL会自动在三角形表面产生颜色平滑过渡的效果。
通过这一节的学习,你会了解到顶点着色器和片元着色器之间的数据传输细节,这里也就是 varying 变量起作用的地方。
几何形状的装配和光栅化
为了简单起见,我们使用第3章中的示例程序 HelloTriangle.js 来解释,这个程序画了一个红色的三角形。
HelloTriangle.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'void main(){'+
'gl_Position=a_Position;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the <canvas> element");
return;
}
//获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if(!gl){
console.log("Failed to get the rendering context for WebGL");
return;
}
//初始化着色器
if(!initShaders(gl,VSHADER_SOURCE,FSHADER_SOURCE)){
console.log("Failed to initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//指定清空<canvas>颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n=3; //点的个数
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//将缓冲区对象保存到目标上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
//向缓存对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if(a_Position < 0){
console.log("Failed to get the storage location of a_Position");
return -1;
}
//将缓冲区对象分配给a_Postion变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//连接a_Postion变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
我们在 initVertexBuffers() 函数将顶点坐标写入了缓冲区对象,然后将缓冲区对象分配给 a_Position 变量。最后调用 gl.drawArrays()执行顶点着色器。当顶点着色器执行时,缓冲区的三个顶点坐标依次传给了 a_Position 变量,再赋值给 gl_Position,这样 WebGL 系统就可以根据顶点坐标进行绘制。在片元着色器中,我们将红色的RGBA值(1.0, 0.0, 0.0, 1.0)赋给 gl_FragColor,这样就画出了一个红色的三角形。
可是直到现在,你还是不明白这究竟是如何做到的?在你向 gl_Position 给出了三角形的三个顶点的坐标时,片元着色器又怎样才能进行所谓的逐偏远操作呢?
下图显示了问题所在,程序向 gl_Position 给出了三个顶点的坐标,谁来确定这三个点就是三角形的三个顶点?最终,为了填充三角形内部,谁来确定哪些元素需要被着色?谁来负责调用片元着色器,片元着色器又是怎样处理每个片元的?
本书在之前的示例程序的解释中掩饰了这些细节。实际上,在顶点着色器和片元着色器之间,有这样的步骤,如下图所示。
- 图形装配过程:这一步的任务是,将孤立的顶点坐标装配成几何图形。几何图形的类别由 gl.drawArrays()函数的第一个参数决定。
- 光栅化过程:这一步的任务是,将装配好的几何图形转化为片元。
通过上图你就会理解,gl_Position 实际上是几何图形装配阶段的输入数据。注意,几个图形装配过程又被称为图元装配过程,因为被装配出的基本图形(点、线、面)又被称为图元。
下图显示了在 HelloTriangle.js 中,顶点着色器和片元着色器之间图形装配与光栅化的过程。
gl.drawArrays()的参数 n 为3,顶点着色器将被执行3次。
第1步:执行顶点着色器,缓冲区对象中的第1个坐标(0.0, 0.5)传递给 attribute 变量 a_Position。一旦一个顶点的坐标被赋值给了 gl_Position,它就进入了图形装配区域,并暂时存储在那里。你应该还记得,我们仅仅显失地向 a_Position 赋了 x 分量和 y 分量,所以向 z 分量 和 w 分量赋的是默认值,进入图形装配区域的坐标其实是(0.0, 0.5, 0.0, 1.0)。
第2步:再次执行顶点着色器,类似地,将第2个坐标(-0.5, -0.5, 0.0, 1.0)传入并存储在装配区。
第3步:第3次执行顶点着色器,将第3个坐标(0.5, -0.5, 0.0, 1.0)传入并存储在装配区。现在,顶点着色器执行完毕,三个顶点坐标都已经处在装配区了。
第4步:开始装配图形。使用传入的点坐标,根据 gl.drawArrays()的第一个参数信息(gl.TRIANGLES)来决定如何装配。本例使用三个顶点来装配出一个三角形。
第5步:显示在屏幕上的三角形是由片元(像素)组成的,所以还需要将图形转化为片元,这个过程被称为光栅化。光栅化之后,我们就得到了组成这个三角形的所有片元。在下图的最后一步,你可以看到光栅化后得到的组成三角形的片元。
上图为了示意,只显示了10个片元。实际上,片元数目就是这个三角形最终在屏幕上所覆盖的像素数。如果修改了gl.drawArrays()的第1个参数,那么第4步的图形装配、第5步的片元数目和位置就会相应的变化。比如说,如果这个参数是 gl.LINE,程序就会使用前两个点装配出一条线段,舍弃第3个点里如果是gl.LINE_LOOP,程序就会将三个点装配成为首尾相接的折线段,并光栅化出一个空心的三角形(不产生中间的像素)。
调用片元着色器
一旦光栅化过程结束后,程序就开始逐元调用片元着色器。在上图中,片元着色器被涌动了10次,没调用一次,就处理一个片元。对于每个片元,片元着色器计算出该片元的颜色,并写入颜色缓冲区。知道第15步最后一个片元倍处理完成,浏览器就会显示出最终的结果。
HelloTriangle.js 中的片元着色器将每个片元的颜色都指定为红色,如下所示。因此,浏览器就绘制除了一个红色的三角形。
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
varying 变量的作用和内插过程
现在,我们已经了解了顶点着色器和片元着色器之间的几何图形装配和光栅化过程,明白了 WebGL 系统是怎样逐偏远执行片元着色器的了。
回到 ColoredTriangle 程序,这个程序也可以用刚学到的只是来解释为什么在顶点着色器中只是指定了每个顶点的颜色,最后得到了一个具有渐变色彩效果的三角形呢?事实上,我们把顶点的颜色赋值给了顶点着色器中的 varying 变量 v_Color,它的值被传给片元着色器中的同名、同类型变量(即片元着色器的 varying 变量 v_Color),如下图所示。但是,更准确地说,顶点着色器中的 v_Color 变量在传入片元着色器之前经过了内插过程。所以,片元着色器中的 v_Color 变量和顶点着色器中的 v_Color 变量实际上并不是一回事,这也正是我们将这种变量称为”varying”变量的原因。
更准确地说,在 ColoredTriangle 中,我们在 varying 变量中为三角形的3个不同定点指定了3种不同颜色,而三角形表面上这些片元的颜色值都是 WebGL 系统用这三个顶点的颜色内插出来的。
例如,考虑一条两个端点的颜色不同的线段。一个端点的颜色为红色,而另一个端点的颜色为蓝色。我们在顶点着色器中向 varying 变量 v_Color 赋上着两个颜色(红色和蓝色),那么 WebGL 就会自动地计算出线段上的所有点(片元)的颜色,并赋值给片元着色器中的 varying 变量 v_Color。
在这个例子中 RGBA 中的 R值从1.0降为0.0,而B值从0.0上升为1.0,线段上的所有片元的颜色值都会被恰当地计算出来——这个过程就被称为内插过程。一旦两点之间每个片元的新颜色都通过这种方式被计算出来后,它们就会被传给片元着色器中的 v_Color 变量。