纹理 (texture) 是 OpenGL 中不可或缺的一部分,虽然我们可以使用 RGB 去表达几乎所有颜色,但如果真的要亲手编写每个像素的 RGB 颜色,这实在是太繁琐了。纹理则可以大大提高效率,我们可以使用纹理去模拟真实世界,去存储数据信息等等。
在 3D 世界中,除了模型本身,最关键的就是纹理/材质了。这就涉及了一个关键技术:纹理映射(texture mapping)。简单来说,就是将一张图片根据一定规则贴在几何图形表面,这张图片就是纹理图像。光栅化之后每个像素的颜色,就取自这张图片相应的位置的颜色,组成纹理图像的像素又被称为纹素 (texels),每一个纹素的颜色都使用 RGB 或 RGBA 格式编码。
既然提到映射,自然是少不了坐标的。之前我们已经介绍过 Canvas 2D 和 WebGL 坐标的差异了,而纹理坐标和前两者又不一样:
假设我们想把这张图片铺满整个 WebGL 视窗,则需要映射坐标点:
我们依旧通过缓冲区对象同时保存顶点坐标和纹理坐标(对于 2D 平面可以只使用 xy 坐标而不是 xyz,减少传输数据量):
var verticesTexCoords = new Float32Array([
// 顶点坐标, 纹理坐标
-1.0, 1.0, 0.0, 1.0,
-1.0, -1.0, 0.0, 0.0,
1.0, 1.0, 1.0, 1.0,
1.0, -1.0, 1.0, 0.0
])
然后在着色器代码中,获取传入的纹理坐标:
<!-- 定义顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
// 定义浮点数精度
// highp:32 位浮点格式,适合用于顶点变换。性能最慢
// mediump:16 位浮点格式,适用于纹理坐标。比 highp 大约快两倍
// lowp:10 位的顶点格式,适合颜色,照明等高性能计算,速度大约是 highp 的 4 倍
precision mediump float;
attribute vec2 a_Position;
attribute vec2 a_TexCoord; // 纹理坐标
varying vec2 v_TexCoord; // 纹理坐标
void main() {
gl_Position = vec4(a_Position, 1.0, 1.0);
v_TexCoord = a_TexCoord;
}
</script>
<!-- 定义片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
varying vec2 v_TexCoord; // 获取纹理坐标
void main() {
gl_FragColor = vec4(v_TexCoord.xy, 1., 1.); // 使用 vec3(xy,1) 坐标组成 rgb 颜色
}
</script>
通过 v_TexCoord 我们可以得到 x、y 轴从 0~1 的取值,将 xyz 作为 rgb 的值,可以得到很好看的颜色渐变。
See the Pen 1. Load Texuture (part1) by ShaderFans (@shaderfans) on CodePen.
其实对于 2D 矩形纹理映射,还可以通过另外一种方式来生成坐标:
<!-- 定义顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
precision mediump float;
attribute vec2 a_Position;
void main() {
gl_Position = vec4(a_Position, 1.0, 1.0);
v_TexCoord = a_TexCoord;
}
</script>
<!-- 定义片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
vec2 resolution = vec2(500.0, 300.0);
void main() {
vec2 uv = gl_FragCoord.xy / resolution;
gl_FragColor = vec4(uv, 1., 1.);
}
</script>
不需要传递 a_TexCoord 纹理坐标,但需要传入画布的分辨率。gl_FragCoord 是 Shader 的内置变量,它的 x 和 y 值是当前窗口空间坐标 (Window-Space Coordinate)。如果我们的窗口是 800×600 的,那么 x 的范围就在 0~800,y 则是 0~600。通过除以宽高分辨率得到了 0~1 的坐标,相当于归一化。我们习惯给纹理坐标命名为 st 或者 uv。
这种方式仅限于给 2D 矩形添加纹理,假设我们要给复杂的 3D 图形贴纹理,还是需要通过纹理坐标来实现。一般的 3D 建模软件都可以输出模型对应的纹理坐标,如下所示为文本编辑器打开的 3D 模型(.obj 格式),v 表示顶点,vt 表示纹理坐标:
......
g default
v 0.556468 -3.694208 -0.180808
v 0.473360 -3.694208 -0.343916
v 0.343916 -3.694208 -0.473360
.....
vt 0.000000 0.050000
vt 0.050000 0.050000
vt 0.100000 0.050000
.....
解决了坐标映射之后,就是加载纹理和配置纹理了。我们先看着色器要怎么写:
<!-- 顶点着色器没变化 -->
<!-- 定义片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
varying vec2 v_TexCoord;
uniform sampler2D u_Sampler;
void main() {
vec4 texColor = texture2D(u_Sampler, v_TexCoord);
gl_FragColor = texColor;
}
</script>
首先看 uniform sampler2D u_Sampler
:Sampler(采样器)是 Shader 提供的可供纹理对象使用的内建数据,它通常是在片元着色器中内定义,被 uniform 修饰符修饰。Sampler2D 代表一个二维的纹理类型,同理还有 Sampler1D、Sampler3D 表示不同维度的纹理类型。
然后是vec4 texture2D(sampler2D sampler, vec2 coord)
:这个函数返回的就是纹素,即 coord 二维坐标对应的图片的颜色值。在 OpenGL ES 3.0 之后(即 WebGL 2.0),使用 texture() 替代 texture2D(),为保证向后兼容,目前的 Demo 依旧采用旧的写法。
接下来是 JavaScript 代码,首先加载一张图片:
var image = new Image()
image.crossOrigin = "Anonymous" // 解决图片跨域
image.src = 'https://shaderfans.com/images/figure1.jpg'
image.onload = function() {
// 图片加载完成后才能继续往下操作
// 可以采用 Promise 等方式解决多张图片加载的异步回调问题
}
加载完图片后,就是 WebGL 的纹理配置了:
// 创建纹理对象
var texture = gl.createTexture()
if (!texture) {
console.log('Failed to create the texture object')
return false
}
// 获取第纹理图的存储位置
var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
if (!u_Sampler) {
console.log('Failed to get the storage location of u_Sampler')
return false
}
// 翻转图片 Y 轴
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 激活纹理单元 TEXTURE0
gl.activeTexture(gl.TEXTURE0)
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture)
// 设置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
// 将纹理传递给着色器
gl.uniform1i(u_Sampler, 0)
上面代码的信息量较多,一句一句来分析:
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
:将图像进行 Y 轴反转。由于WebGL 纹理坐标系统中的 Y 轴的方向和 PNG、BMP、JPG 等格式图片的坐标系的 Y 轴方向相反(这些图片的坐标起始点都是在左上角)。因此,只有将图像的 Y 轴进行反转,才能够正确地将图像映射到图形上(可以注释这段语句看看效果)。
gl.activeTexture(gl.TEXTURE0)
:激活 TEXTURE0 指定的纹理单元。分别存在 gl.TEXTURE0~7,最后的数字表示纹理单元的编号。系统支持的纹理单元个数取决于硬件和浏览器的 WebGL 实现,但在默认情况下,WebGL 至少支持 8 个纹理单元。
gl.bindTexture(gl.TEXTURE_2D, texture)
:把 TEXTURE_2D 类型的纹理绑定到 target 上。WebGL 通过纹理单元 (texture unit) 的机制来同时使用多个纹理。每个纹理单元都有一个单元编号来管理纹理图像。激活纹理单元后将其与纹理绑定。
gl.texParameteri(target, pname, param)
:设置纹理滤镜 (Texture Filter),处理当对象出现缩放时,纹理如何处理中间点或被压缩的点。有四个参数可以设置:
通过下图可以形象地理解(蓝色表示渲染的大小),至于它们的取值则是不同的滤镜算法,这里暂时不深入:
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
:将 image 指定的图像分配给绑定到目标上的纹理对象,其他参数取值相对固定,这里暂不深入。
最终可以看到纹理被成功加载:
See the Pen 1. Load Texuture (part2) by ShaderFans (@shaderfans) on CodePen.
http://www.adriancourreges.com/blog/2017/05/09/beware-of-transparent-pixels/