此文上接 WebGL 系列文章,第一个是基础概念, 这篇文章讲纹理映射的透视纠正,要理解它你可能需要先看看透视投影 和纹理,你也需要知道可变量以及它的用处,但是我还会简短的介绍一下。
在"工作原理"中我们讲过了可变量的工作原理, 顶点着色器可以声明可变量并给它赋值,一旦顶点着色器被引用 3 次就会画一个三角形。 绘制这个三角形的每个像素都会调用片断着色器获得像素颜色, 在三个顶点之间的点会得到差之后的可变量。
回到我们的第一篇文章,在裁剪空间中绘制一个三角形, 没有数学运算,只是传入裁剪空间坐标到一个简单的顶点着色器,像这样
  #version 300 es
  // 输入到顶点着色器的属性
  // 从缓冲中获取数据
  in vec4 a_position;
  // 所有的着色器都有一个 main 函数
  void main() {
    // gl_Position 是着色器需要设置的一个特殊的变量
    gl_Position = a_position;
  }
还有一个简单的片断着色器提供固定的颜色
  #version 300 es
  // 片断着色器没有默认的精度,
  // 高等精度是个不错的默认值
  precision highp float;
  // 定义一个传递到片段着色器的颜色变量
  out vec4 outColor;
  void main() {
    // 设置颜色变量为红紫色
    outColor = vec4(1, 0, 0.5, 1);
  }
让我们在裁剪空间绘制两个矩形,为每个顶点传递X, Y, Z, 和 W。
var positions = [
  -.8, -.8, 0, 1,  // 第一个矩形的第一个三角形
   .8, -.8, 0, 1,
  -.8, -.2, 0, 1,
  -.8, -.2, 0, 1,  // 第一个矩形的第二个三角形
   .8, -.8, 0, 1,
   .8, -.2, 0, 1,
  -.8,  .2, 0, 1,  // 第二个矩形的第一个三角形
   .8,  .2, 0, 1,
  -.8,  .8, 0, 1,
  -.8,  .8, 0, 1,  // 第二个矩形的第二个三角形
   .8,  .2, 0, 1,
   .8,  .8, 0, 1,
];
这是结果
再添加一个浮点型可变量,并把它从顶点着色器直接传递到片断着色器
  #version 300 es
  in vec4 a_position;
+  in float a_brightness;
+  out float v_brightness;
  void main() {
    gl_Position = a_position;
+    // 直接传递亮度到片断着色器
+    v_brightness = a_brightness;
  }
在片断着色器中使用可变量设置颜色
  #version 300 es
  precision highp float;
+  // 顶点着色器的值插值后传入这里
+  in float v_brightness;
   // 定义一个传递到片段着色器的颜色变量
   out vec4 outColor;
  void main() {
*    outColor = vec4(v_brightness, 0, 0, 1);  // 红色
  }
我们需要给可变量提供数据,创建一个缓冲放入一些数据,每个顶点一个值, 我们将设置左边亮度为 0,右边亮度为 1。
  // 创建缓冲并放入 12 个亮度值
  var brightnessBuffer = gl.createBuffer();
  // 绑定到 ARRAY_BUFFER
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);
  var brightness = [
    0,  // 第一个矩形的第一个三角形
    1,
    0,
    0,  // 第一个矩形的第二个三角形
    1,
    1,
    0,  // 第二个矩形的第一个三角形
    1,
    0,
    0,  // 第二个矩形的第二个三角形
    1,
    1,
  ];
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(brightness), gl.STATIC_DRAW);
还需要在初始化阶段找到 a_brightness 的位置
  // 找到顶点数据的输入位置
  var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
+  var brightnessAttributeLocation = gl.getAttribLocation(program, "a_brightness");
然后在渲染阶段设置属性
  // 启用属性
  gl.enableVertexAttribArray(brightnessAttributeLocation);
  // 绑定位置缓冲
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);
  // 告诉属性如何从缓冲中读取数据
  var size = 1;          // 每次迭代读取一个单位数据
  var type = gl.FLOAT;   // 数据类型是 32 位浮点型
  var normalize = false; // 不用单位化
  var stride = 0;        // 每次迭代需要移动的距离
  var offset = 0;        // 从缓冲的起始处开始
  gl.vertexAttribPointer(
      brightnessAttributeLocation, size, type, normalize, stride, offset);
现在渲染后得到会得到两个矩形,左边 brightness 是 0 右边是 1,
中间的部分是插值(或可变量)。
在透视投影的文章中我们知道 WebGL 会将放入 gl_Position
的值除以gl_Position.w。
在上方的顶点中我们提供的 W 值为 1,但是我们知道 WebGL 会除以 W,
所以做如下修改应该结果不变。
  var mult = 20;
  var positions = [
      -.8,  .8, 0, 1,  // 第一个矩形的第一个三角形
       .8,  .8, 0, 1,
      -.8,  .2, 0, 1,
      -.8,  .2, 0, 1,  // 第一个矩形的第二个三角形
       .8,  .8, 0, 1,
       .8,  .2, 0, 1,
      -.8       , -.2       , 0,    1,  // 第二个矩形的第一个三角形
       .8 * mult, -.2 * mult, 0, mult,
      -.8       , -.8       , 0,    1,
      -.8       , -.8       , 0,    1,  // 第二个矩形的第二个三角形
       .8 * mult, -.2 * mult, 0, mult,
       .8 * mult, -.8 * mult, 0, mult,
  ];
如上所示我们将第二个矩形右边的点的 X 和 Y 都乘以 mult,
同时设置 W 为 mult。由于 WebGL 会除以 W 所以我们应该得到相同的结果对么?
这是结果
注意两个矩形和上一个例子位置相同,事实是 X * MULT / MULT(W) 还是 X,
Y 也一样,但颜色却不同,为什么?
事实是 WebGL 使用 W 实现纹理映射或者可变量插值的透视投影。
如果将片断着色器改成这样就更容以看出效果
outColor = vec4(fract(v_brightness * 10.), 0, 0, 1);  // 红色
将 v_brightness 乘以 10 就会是值的范围为 0 到 10,fract 会保留小数部分所以结果还是 0 到 1 之间,但是总共是 10 次 0 到 1 了。
线性插值应该是这样的公式
 result = (1 - t) * a + t * b
t 的范围是 0 到 1,表示 a 到 b 之间的位置,0 表示在 a 点,1 表示在 b 点。
可变量经过 WebGL 的插值时使用这个公式
          (1 - t) * a / aW + t * b / bW
result =  -----------------------------
             (1 - t) / aW + t / bW
aW 表示 a 点的 W 值,也就是 gl_Position.w 的值,而不一定等于缓冲中的值,
同理 bW 是设置在 b 点的 gl_Position.w。
为什么这个很重要?这有一个简单的纹理,使用纹理文章的例子, 并调整了 UV,每个面使用整张纹理,是一个 4×4 像素的纹理。
现在将例子中的顶点着色器做一些修改,手工除以 W,只增加了一行代码
#version 300 es
in vec4 a_position;
in vec2 a_texcoord;
uniform mat4 u_matrix;
out vec2 v_texcoord;
void main() {
  // 将位置和矩阵相乘
  gl_Position = u_matrix * a_position;
+  // 手工除以 W
+  gl_Position /= gl_Position.w;
  // 将纹理坐标传到片断着色器
  v_texcoord = a_texcoord;
}
除以 W 意味值 gl_Position.w 始终为 1,X, Y, 和 Z
不会有什么影响,因为 WebGL 也会默认做除法,这是结果。
我们还是得到了一个立方体,但是纹理变得扭曲了,这是因为没有传入 W
WebGL 就不能正确的实现纹理的透视纠正,或者更准确地说,
WebGL 就不能正确的对可变量的插值实现透视。
如果你还记得的话 W 就是透视矩阵中的 Z,
当 W 始终为 1 时 WebGL 做的就是一个简单的线性插值,
事实上如果你拿着上述的等式
          (1 - t) * a / aW + t * b / bW
 result = -----------------------------
             (1 - t) / aW + t / bW
设置所有的 W 为 1 就会得到
          (1 - t) * a / 1 + t * b / 1
result =  ---------------------------
             (1 - t) / 1 + t / 1
除以 1 等于什么也没做,所以简化成这样
          (1 - t) * a + t * b
result =  -------------------
             (1 - t) + t
(1 - t) + t 当 t 从 0 变到 1 跟 1 结果是一样的. 例如
t 等于 .7 则 (1 - .7) + .7 等于 .3 + .7 等于 1。
换句话说,我们可以移除底部,这样我们就剩下
 result = (1 - t) * a + t * b
就和上方提到的线性插值一样了。
现在就清楚为什么 WebGL 要使用 4x4 的矩阵和包含 X, Y, Z, 和 W 四个值的向量了吧。
X 和 Y 除以 W 得到裁剪空间坐标,Z 除以 W 也得到裁剪空间的 Z 坐标,W 同时还为纹理映射的透视纠正提供了帮助。