目录
顶点坐标 --> 图形装配 --> 光栅化 --> 执行片元着色器
第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)来决定如何装配。本例使用三个顶点来装配出一个三角形。
gl.drawingBufferWidth / gl.drawingBufferHeight
前言
在 WebGL 或 OpenGL 中,“varying” 是一种用于在顶点着色器和片元着色器之间传递数据的特殊类型的变量。它允许在顶点着色器对数据进行处理后,在片元着色器中使用该处理后的数据进行进一步计算。
彩色三个点
彩色三个点示例代码
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'varying vec4 v_Color;\n' + // varying 变量
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
' v_Color = a_Color;\n' + // 将数据传给片元着色器
'}\n';
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' + // 设置varing精度
'varying vec4 v_Color;\n' + // 从顶点着色器接受数据
'void main() {\n' +
' gl_FragColor = v_Color;\n' +
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
// 设置顶点的坐标和颜色
var n = initVertexBuffers(gl);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, n);
}
function initVertexBuffers(gl) {
var verticesColors = new Float32Array([
// 顶点坐标和颜色
0.0, 0.5, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0,
]);
var n = 3; // 顶点数量
// 创建缓冲区对象
var vertexColorBuffer = gl.createBuffer();
if (!vertexColorBuffer) {
console.log('Failed to create the buffer object');
return false;
}
// 将顶点坐标和颜色写入缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
var FSIZE = verticesColors.BYTES_PER_ELEMENT;
// 获取a_Position的存储位置,分配缓冲区并开启
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
gl.enableVertexAttribArray(a_Position); // 开启变量
// 获取a_Color的存储位置,分配缓冲区并开启
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
gl.enableVertexAttribArray(a_Color); // 开启缓冲区分配
return n;
}
varying变量使用规范
在顶点着色器中,我们声明了attribute变量a_Color用以接收颜色数据(第4行),然后声明了新的varying变量v_Color,该变量负责将颜色值将被传给片元着色器(第5行)。注意,varying变量只能是float(以及相关的vec2,vec3,vec4,mat2,mat3和mat4)类型的。
我们将a_Color变量的值直接赋给之前声明的v_Color变量(第9行)。
那么,片元着色器该如何接收这个变量呢?答案很简单,只需要在片元着色器中也声明一个(与顶点着色器中的那个varying 变量同名)varying变量就可以了:
在WebGL中,如果顶点着色器与片元着色器中有类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值就会被自动地传入片元着色器,如下图所示。
所以,顶点着色器赋给v_Color变量的值(第9行)被传递给了片元着色器中的v_Color变量,然后片元着色器将v_Color赋值给gl_FragColor,这样每个顶点的颜色将被修改(第17行)。
数组verticesColor中有两种不同类型的数据(坐标和颜色)。现在的颜色有3个分量值,所以每个顶点所占字节数是FSIZE*5,需要修改相应的gl.vertexAttribPointer()函数的stride参数和offset参数(第53和58行)。
最后,执行绘图命令(第27行),在浏览器中绘制了红、蓝、绿三个点。
彩色三角形
让我们来看看将gl.drawArrays()函数的第一个参数改成gl.TRIANGLES后会怎样(第27行)
程序的结果如下图所示,程序绘制了一个颜色平滑过渡的、三个角各是红、绿、蓝颜色的三角形。
我们只改变了一个参数,程序的运行结果却从三个不同颜色的孤立的点变成了一个颜色平滑过渡的三角形。到底发生了什么?
几何形状的装配和光栅化
为了简单起见,这里拿一个红色三角形的代码来解释
红色三角形示例代码
我们在initVertexBuffers()函数中将顶点坐标写入了缓冲区对象(第50行和第52行),然后将缓冲区对象分配给a_Position变量(第74行)。最后调用gl.drawArrays()执行顶点着色器(第46行)。当顶点着色器执行时,缓冲区中的三个顶点坐标依次传给了a_Position变量(第4行),再赋值给gl_Position(第6行),这样WebGL系统就可以根据顶点坐标进行绘制。在片元着色器中,我们将红色的RGBA值(1.0,0.0,0.0,1.0)赋给gl_FragColor,这样就画出了一个红色的三角形。
可是直到现在,你还是不明白这究竟是如何做到的?在你向gl_Position给出了三角形的三个顶点的坐标时,片元着色器又怎样才能进行所谓的逐片元操作呢?
如下图显示了问题所在,程序向gl_Position给出了三个顶点的坐标,谁来确定这三个点就是三角形的三个顶点?最终,为了填充三角形内部,谁来确定哪些像素需要被着色?谁来负责调用片元着色器,片元着色器又是怎样处理每个片元的?
顶点坐标 --> 图形装配 --> 光栅化 --> 执行片元着色器
在顶点着色器和片元着色器,有这样两个步骤:
-
图元装配过程
这一步的任务是,将孤立的顶点坐标装配成几何图形。几何图形的类别由gl.drawArrays()函数的第一个参数决定。
-
光栅化过程
这一步的任务是,将装配好的几何图形转换为片元。
上图所示,gl_Position实际上是几何图形装配(geometric shape assembly)阶段的输入数据。注意,几何图形装配过程又被称为图元装配过程(primitive assembly process),因为被装配出的基本图形(点、线、面)又被称为图元(primitives)。
顶点着色器和片元着色器之间图形装配和光栅化的过程详解
第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步:显示在屏幕上的三角形是由片元(像素)组成的,所以还需要将图形转化为片元,这个过程被称为光栅化(rasterization)。光栅化之后,我们就得到了组成这个三角形的所有片元。在上图中的最后一步,你可以看到光栅化后得到的组成三角形的片元。
上图为了示意,只显示了10个片元。实际上,片元数目就是这个三角形最终在屏幕上所覆盖的像素数。如果修改了gl.drawArrays()的第1个参数,那么第4步的图形装配、第5步的片元数目和位置就会相应地变化。比如说,如果这个参数是gl.LINE,程序就会使用前两个点装配出一条线段,舍弃第3个点;如果是gl.LINE_LOOP,程序就会将三个点装配成为首尾相接的折线段,并光栅化出一个空心的的三角形(不产生中间的像素)。
调用片元着色器
一旦光栅化过程结束后,程序就开始逐片元调用片元着色器。在下图中,片元着色器被调用了10次,每调用一次,就处理一个片元(为了整洁,下图省略了中间步骤)。对于每个片元,片元着色器计算出该片元的颜色,并写入颜色缓冲区。直到第15步最后一个片元被处理完成,浏览器就会显示出最终的结果。
红色三角形代码中的片元着色器将每个片元的颜色都指定为红色,如下所示。因此,浏览器就绘制出了一个红色的三角形。
做个试验:根据片元的位置来确定片元颜色
这样可以证明片元着色器对每个片元都执行了一次。光栅化过程生成的片元都是带有坐标信息的,调用片元着色器时这些坐标信息也随着片元传了进去,我们可以通过片元着色器中的内置变量来访问片元的坐标(如下表)。
为了证明片元着色器是逐片元执行的,我们修改了原红色三角形程序的第12行(gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0)),如下所示:
从片元着色器的程序代码中可见,三角形中每个片元的颜色,其红色分量和蓝色分量都是根据片元的位置计算得到的。注意,canvas中的Y轴方向和WebGL系统中的Y轴方向是相反的,而且WebGL中的颜色分量值区间为0.0到1.0,所以你需要将Y轴坐标除以<canvas>元素的高度(400像素)以将其压缩到0.0到1.0之间。我们将gl.drawingBufferWidth(颜色缓冲区的宽度)和gl.drawingBufferHeight(颜色缓冲区的高度)的值传给uniform变量u_Width和u_Height。下面彩色三角形显示了程序的运行结果:一个三角形,像素颜色由像素的位置决定,从左上方到右下方呈现一个渐变效果。
gl.drawingBufferWidth / gl.drawingBufferHeight
gl.uniform1f(u_Width, gl.drawingBufferWidth); // 颜色缓冲区的宽度,下面同理 高度
gl.uniform1f(u_Height, gl.drawingBufferHeight);
显示效果
由于片元颜色取决于它的坐标位置,所以很自然地,片元颜色会随着片元位置逐渐变化,三角形呈现平滑的颜色渐变效果。
varying变量的作用和内插过程
现在,我们已经了解了顶点着色器与片元着色器之间的几何图形装配和光栅化过程,明白了WebGL系统是怎样逐片元执行片元着色器的了。
回到上面彩色三角形程序,这个程序也可以用刚学到的知识来解释为什么在顶点着色器中只是指定了每个顶点的颜色,最后得到了一个具有渐变色彩效果的三角形呢?事实上,我们把顶点的颜色赋值给了顶点着色器中的varying变量v_Color,它的值被传给片元着色器中的同名、同类型变量(即片元着色器中的varying变量v_Color),如下图所示。但是,更准确地说,顶点着色器中的v_Color变量在传入片元着色器之前经过了内插过程。所以,片元着色器中的v_Color变量和顶点着色器中的v_Color变量实际上并不是一回事,这也正是我们将这种变量称为“varying”(变化的)变量的原因。
varying变量的行为
varying变量的内插
更准确地说,在红色三角形中,我们在varying变量中为三角形的3个不同顶点指定了3种不同颜色,而三角形表面上这些片元的颜色值都是WebGL系统用这3个顶点的颜色内插出来的。
例如,考虑一条两个端点的颜色不同的线段。一个端点的颜色为红色(1.0,0.0,0.0),而另一个端点的颜色为蓝色(0.0,0.0,1.0)。我们在顶点着色器中向varying变量v_Color赋上这两个颜色(红色和蓝色),那么WebGL就会自动地计算出线段上的所有点(片元)的颜色,并赋值给片元着色器中的varying变量v_Color(如下图所示)。
颜色值的内插
在这个例子中RGBA中的R值从1.0降低为0.0,而B值则从0.0上升至1.0,线段上的所有片元的颜色值都会被恰当地计算出来——这个过程就被称为内插过程(interpolation process)。一旦两点之间每个片元的新颜色都通过这种方式被计算出来后,它们就会被传给片元着色器中的v_Color变量。
再来看红色三角形的程序代码。在顶点着色器中,我们将三角形的3个顶点的颜色赋给了varying变量v_Color(第9行),然后片元着色器中的varying变量 v_Color就接收到了内插之后的片元颜色。在片元着色器中,我们把片元的颜色赋值给gl_FragColor变量(第19行),这样就绘制出了一个彩色的三角形,同理,每一个varying变量都会经过这样的内插过程。
总结
顶点着色器和片元着色器之间的过程非常重要。光栅化也是三维图形学的关键技术之一,它负责将矢量的几何图形转变为栅格化的片元(像素)。图形被转化为片元之后,我们就可以在片元着色器内做更多的事情,如为每个片元指定不同的颜色。颜色可以内插出来,也可以直接编程指定。