OpenGL 编程/Glescraft 3
之前两个教程中的体素看起来很无聊,而且很难区分它们,因为它们具有统一的颜色,并且没有尝试使用光照效果。在本教程中,我们将了解如何仅使用我们拥有的一个纹理坐标为体素赋予不同的纹理。此外,我们将以微小的方式调整片段着色器,以在场景照明方面获得很大的改进。
当我们想在体素上放置纹理时,我们不想给一个块中的所有体素赋予相同的纹理。但是,如果我们一次性渲染块中的所有三角形,我们就无法在体素之间切换纹理。因此,我们将不得不使用 纹理图集 将我们想要在体素上绘制的所有图像存储在一个 OpenGL 纹理中。我们只有一个纹理坐标可用,它只能有 256 个可能的值。如果我们可以使用它来指向纹理图集中的 256 个可能的子图像之一,那就太好了。
假设我们有一个包含 16 个子图像的纹理图集。所有子图像都具有相同的大小(SW x SH),但确切的大小并不重要。纹理图集将在一行中包含所有子图像,因此纹理图集将具有 (SW * 16) x SH 个像素。Theblk[x][y][z]数组现在应包含 0 到 15 范围内的值。在片段着色器中,我们现在必须从varying vec4 texcoord创建真实的纹理坐标,这些坐标是从顶点着色器获得的。显然,我们从 0 到 15 的整数值是不够的。但是,我们可以使用 x、y 和 z 坐标。由于这些是“变化的”,它们将不包含来自 VBO 的整数值,而是可以在整数值之间具有任何值,具体取决于片段在顶点之间的距离。特别是,如果我们从 (0, 0, 0) 到 (1, 1, 0) 绘制一个四边形,则 z 坐标将始终为 0,但 x 和 y 坐标可以在其之间取任何值。为了纹理化这个四边形,我们可以使用以下片段着色器
#version 120
varying vec4 texcoord;
uniform sampler2D texture;
void main(void) {
gl_FragColor = texture2D((texcoord.x + texcoord.w) / 16.0, texcoord.y);
}
除以 16 是因为我们的纹理图集在同一行中包含 16 个子图像。此外,由于 w 坐标对于四边形中的所有顶点都是相同的,因此它将在片段着色器中具有恒定值。请记住,texcoord只是制服的副本coord在应用 MVP 矩阵之前,因此纹理不会受到 MVP 任何变化的影响。
当然,您可能已经注意到此着色器不适用于大多数其他可能坐标的四边形。首先,为了解决任何角落为 (x, y, *) 到 (x + 1, y + 1, *) 的四边形,其中 x 和 y 是整数坐标,* 表示任何可能的 z,我们可以使用fract()函数将 x 坐标映射回 0 到 1 的范围
gl_FragColor = texture2D((fract(texcoord.x) + texcoord.w) / 16.0, texcoord.y);
我们不必使用fract()ontexcoord.y,因为我们的纹理图集只有一行图像,OpenGL 将负责在垂直方向上进行纹理环绕。此着色器适用于指向正向或负向 z 方向的任何体素面。但是,对于这些面,面的所有顶点都具有相同的 /整数/ z 坐标值。因此,在应用之前,我们可以安全地将其添加到 x 坐标中fract()function
gl_FragColor = texture2D((fract(texcoord.x + texcoord.z) + texcoord.w) / 16.0, texcoord.y);
相同的参数适用于指向正向或负向 y 方向的体素面,因此您可以对这些面使用相同的着色器!但是,我们不能在其中也放入 y 坐标。正确渲染指向正向或负向 y 方向的面唯一方法是,要么使用两个片段着色器,分别渲染 x 和 z 面,要么向顶点添加一些额外信息,以便能够在片段着色器中区分这两种情况。由于我们只在纹理中使用 16 个子图像,因此我们只使用 w 坐标的 4 位。事实上,我们可以使用另一个位作为非常基本的“法线”向量。我们将使用负 w 坐标来指示指向 y 方向的面
if(texcoord.w < 0)
gl_FragColor = texture2D((fract(texcoord.x) + texcoord.w) / 16.0, texcoord.z);
else
gl_FragColor = texture2D((fract(texcoord.x + texcoord.z) + texcoord.w) / 16.0, texcoord.y);
我们不必担心 w 坐标的负值,只要在创建 VBO 时,我们从blk[x][y][z]的值中减去 16 的倍数,使其变为负数。这样,纹理坐标将超出 0..1 的范围,但会由 OpenGL 正确地包裹到纹理图集中的正确位置。
练习
- 在之前的教程中,我们已经看到可以合并相邻面以减少需要绘制的三角形数量。上面的着色器在这种情况下仍然有效吗?
即使使用了纹理,也很难区分体素。为了使场景看起来更自然,并更容易区分我们正在查看体素的哪一面,我们将使用上一节中介绍的“法线”位使体素的侧面略微变暗。这模拟了正午时太阳直射时的真实世界阴影。
if(texcoord.w < 0)
gl_FragColor = texture2D((fract(texcoord.x) + texcoord.w) / 16.0, texcoord.z);
else
gl_FragColor = texture2D((fract(texcoord.x + texcoord.z) + texcoord.w) / 16.0, texcoord.y) * 0.85;
在现实世界中,远处物体看起来比您面前的物体更暗淡,颜色也更少。这是由于大气对光线的散射造成的。这几乎与雾相同,主要区别在于强度。我们可以在片段着色器中实现这一点,如下所示
#version 120
varying vec4 texcoord;
uniform sampler2D texture;
const vec4 fogcolor = vec4(0.6, 0.8, 1.0, 1.0);
const float fogdensity = .00003;
void main(void) {
vec4 color;
if(texcoord.w < 0)
color = texture2D((fract(texcoord.x) + texcoord.w) / 16.0, texcoord.z);
else
color = texture2D((fract(texcoord.x + texcoord.z) + texcoord.w) / 16.0, texcoord.y) * 0.85;
float z = gl_FragCoord.z / gl_FragCoord.w;
float fog = clamp(exp(-fogdensity * z * z), 0.2, 1);
gl_FragColor = mix(fogcolor, color, fog);
}
在片段着色器顶部,我们定义了两个常量。雾的颜色是物体在很远的地方时会呈现的颜色。雾的密度控制雾的效果强度。非常小的值代表大气散射或轻微的薄雾,较大的值代表浓雾。
片段到摄像机的距离可以通过将gl_FragCoord.z除以gl_FragCoord.w来计算。雾的效果随距离呈指数增长。变量fog表示应用雾后“剩余”的真实颜色的比例。最终的片段颜色是原始颜色和雾颜色的混合。
最后,您可以制作具有透明像素的纹理。虽然您可以使用混合来应用透明纹理,但也可以在片段着色器中模拟完全透明的像素。在计算color之后,您可以根据 alpha 值丢弃片段
if(color.a < 0.5)
discard;
Thediscard关键字会导致片段程序停止进一步处理(它类似于 C 中的“return”语句)。与混合相比,这种方法的优势在于,z 缓冲区的值不会为透明像素更新。使用混合,如果在靠近摄像机的位置渲染一个透明三角形,然后在它后面渲染一个不透明三角形,则不透明三角形将不会被绘制,因为它将无法通过深度测试。
练习
- 向一些子图像添加透明度,并查看结果。
- 改变chunk::update()来自之前教程的函数,以正确处理部分透明的面。
- 尝试使用混合而不是丢弃关键字。