WebGL 变量的内插过程

549 阅读8分钟

上一篇文章中讲了在WebGL中使用齐次坐标对三角形进行变换,目前也可以通过gl.drawArrays方法绘制更加复杂的图形,这次让我们更进一步,给你的“女神”挑选一件漂亮的衣服吧👗挑选衣服之前,先让我们回归到最初的那三个点。

🎬又回到最初的起点

还记不记得这熟悉的三个点?在绘制三个点的例子中,我们在顶点着色器中使用了gl_PointSize=10.0这种方式将每个点的大小设置为10像素。但是如果我们想要3个大小不同的点呢?我们需要在顶点着色器中分别定义3个attribute变量接收不同的点的size吗🧐

不必如此,还记不记得我们将顶点坐标信息存入缓冲区对象,并写入attribute变量?定义不同大小的顶点坐标也可以使用这种方式:

const VertexShader = `
  // ...
  attribute float a_PointSize;
  void main() {
    // ...
    gl_PointSize = a_PointSize;
  }
`;

// ...

function initVertexBuffers (gl) {
  // ...
  const sizes = new Float32Array([
    10.0, 20.0, 30.0,
  ]);
  
  // 将顶点大小数据写入缓冲区
  const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
  const sizeBuffer = gl.createBuffer();	// create -> 你来到这个世上
  gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer); // bind -> 找到喜欢的行当
  gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW); // data -> 有车有房
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0); // pointer -> 爱上对方(attribute 变量)
  gl.enableVertexAttribArray(a_PointSize); // enable -> 谱写新的篇章
}

这样就可以看到三个不同大小的点了!还能不能再方便一些?按理说顶点大小与顶点坐标都属于顶点的一个属性,我们可不可以把这些信息放在一起呢?说到这儿,还记不记得之前说过gl.vertexAttribPointer方法中可以传入offset👏

/**
 * @param index 			指定要修改的顶点属性的索引。
 * @param size 				指定每个顶点属性的组成数量,必须是1,2,3或4。
 * @param type 				指定数组中每个元素的数据类型:
 * 											gl.BYTE: 有符号的8位整数,范围[-128, 127];
 *              				gl.SHORT: 有符号的16位整数,范围[-32768, 32767];
 *     									gl.UNSIGNED_BYTE: 无符号的8位整数,范围[0, 255];
 * 											gl.UNSIGNED_SHORT: 无符号的16位整数,范围[0, 65535];
 * 											gl.FLOAT: 32位IEEE标准的浮点数;
 * @param normalized	当转换为浮点数时是否应该将整数数值归一化到特定的范围。
 * @param stride			以字节为单位指定连续顶点属性开始之间的偏移量(即数组中一行长度)。不能大于255。
 *										如果stride为0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块
 *										中,下一个顶点的属性紧跟当前顶点之后。
 * @param offset			指定顶点属性数组中第一部分的字节偏移量,必须是类型的字节长度的倍数。
 */
gl.vertexAttribPointer(index, size, type, normalized, stride, offset);

先将顶点坐标与大小写入一个变量中:

const verticesSizes = new Float32Array([
  0.0, 0.5, 10.0,
  -0.5, -0.5, 20.0,
  0.5, -0.5, 30.0,
]);

看一下上面每个参数的含义,前面的就不做过多介绍,重点介绍一下最后两个参数strideoffsetstride可以理解为每个顶点的属性占有的字节数,我们假设每个数据占的字节大小为FSIZE,比如我们定义的verticesSizes中每个顶点有坐标位置及大小两个属性,而在这里stride则是这个顶点的坐标和大小两个属性所占的字节数,即FSIZE * 3offset是属性的偏移量,比如顶点的大小的偏移量就是FSIZE * 2

我们可以通过verticesSizes.BYTES_PER_ELEMENT获取到Float32Array中每个元素所占的字节数,到此我们之前设置顶点坐标数据缓冲区和顶点大小数据缓冲区的vertexAttribPointer就可以改造为:

// 顶点坐标
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 6, 0);
// 顶点大小
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 6, FSIZE * 2);

这样就可以通过一个Float32Array对象传入两种我们需要的属性值。同理还可以传入顶点颜色,在之前的文章中说过attribute变量只能在定点着色器中使用,所以我们无法在片元着色器中定义一个attribute变量接收颜色的值。之前我们使用过uniform变量将颜色信息传入片元着色器,然而uniform变量是不变的,所以我们无法为每个顶点都准备一个值,所以之前我们的顶点颜色是一样的。在此就要隆重介绍一下我们的新角色闪亮登场:varying,它的作用就是从顶点着色器向片元着色器传输数据🎉

先在顶点着色器和片元着色器中定义varying变量:

const VertexShader = `
  attribute vec4 a_Position;
  attribute vec4 a_Color;
  varying vec4 v_Color;
	void main() {
    gl_Position = a_Position;
    v_Color = a_Color;
  }
`;
const FragmentShader = `
  precision lowp float;
  varying vec4 v_Color;
  void main() {
  	gl_FragColor = v_Color;
  }
`;

如果浏览器报出:No precision specified for (float) 的错误提示,那么需要加上 precision lowp float,此语句是将精度定义为低精度。

下面的步骤就是向顶点着色器中的attribute变量a_Color传值了,在此不再赘述,与之前向缓冲区对象中传入数据的步骤相同。我将三个点分别设置为红、黄、蓝,在浏览器中的展示效果如图🤩

这种效果有没有出乎你意料呢🙊我明明就指定了3个顶点的颜色,为什么会出现这种渐变的效果呢?并且我们没有任何步骤将顶点着色器和片元着色器中的varying变量建立任何联系啊!?别急,让我来给你慢慢解释。

📠着色器中传递变量

首先,片元着色器接收varying变量很简单,只需要在片元着色器中也声明一个与顶点着色器中varying变量同名的变量即可。在WebGL中,如果顶点着色器与片元着色器中有类型和命名都相同的varying变量,那么顶点着色器赋给该变量的值就会自动地传入片元着色器中。

📐几何图形的装配

我们给gl_Position指明了3个顶点,但是谁来确定这三个点就是三角形的三个顶点呢?最终,为了填充三角形内部,谁来确定哪些像素需要被着色呢?谁来负责调用片元着色器,片元着色器又是怎么处理每个片元的呢?请看下图:

图形装配一步是将孤立的顶点装配成几何图形,几何图形的类别由gl.drawArrays的第一个参数决定;光栅化则是将装配好的几何图形转化为片元;转化之后的片元就会传给片元着色器,gl_Position实际上是图形装配阶段的数据输入💻

因为被装配出的基本图形(点、线、面)又被称为图元,故几何图形装配又被称为图元装配过程

我们在这个程序中gl.drawArrays的参数n为3,所以顶点着色器被执行了3次,每一次都会遵循以下步骤:

  • 第1步:执行顶点着色器,缓冲区对象中第一个坐标被传递给attribute变量a_Position。一旦一个顶点的坐标被赋值给了gl_Position,它就进入了图形装配区域,并将暂时储存在那里。我们只显式地给a_Position赋值了xy分量的值,所以zw分量会被默认赋值为0.01.0。之后将其余顶点信息也按照这种方式传入并储存在装配区。
  • 第2步:开始装配图形。使用传入的坐标,根据gl.drawArrays的第一个参数信息来决定如何装配。上面我们使用了gl.TRIANGLES装配出一个三角形。
  • 第3步:显示在屏幕上的三角形是由片元(像素)组成的,所以还需要将图形转化为片元,这个过程被称为光栅化。光栅化之后,我们就得到了组成这个三角形的所有片元。
  • 第4步:一旦光栅化过程结束后,程序就开始逐个片元调用片元着色器。每一次调用就处理一个片元,对于每个片元,片元着色器计算出该片元的颜色,并写入颜色缓冲区。直到最后一个片元被处理完成,浏览器就会显示出最终的结果。

⎀varying 变量的内插过程

上面我们说到顶点着色器中的varying变量被传递给了片元着色器中同名、同类型的varying变量。其实这种说法并不太准确,实际上顶点着色器中的v_Color变量在传入片元着色器之前经历了内插过程。所以,片元着色器中的v_Color变量和顶点着色器中的v_Color变量并不是一回事。

我们可以思考一条两个端点颜色不同的线段,一个端点为红色(1.0, 0.0, 0.0),另一个端点为绿色(0.0, 1.0, 0.0)(红配绿,时尚时尚最时尚😏)。我们在顶点着色器中向varying变量v_Color赋上两个颜色,那么WebGL就会自动地计算出线段上的所有点(片元)的颜色,并赋值给片元着色器中的varying变量v_Color

R值从1.0降到0.0,G值则从0.0上升到1.0,线段上的所有片元的颜色值都会被恰当的算出来——这个过程就是内插过程

结束语

有趣的varying变量内插过程就介绍到这里啦,后续会出更多好玩并且有用的文章分享给大家,预告下篇文章会介绍如何给“女神”穿新衣👘

欢迎大家关注公众号:Refactor,后续会出更多好玩并且有用的文章分享给大家,感谢阅读~