此文上接 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
同时还为纹理映射的透视纠正提供了帮助。