前言
在WebGL
中有一项很重要的技术 —— 纹理映射。所谓纹理映射,就是将一张图片映射到一个几何图形的表面上去(就像孩童时喜欢在胳膊、手背上贴贴纸一样) 将“贴纸”贴到一个矩形上之后,这个矩形表面看上去就像是一张图片,而此时,这张图片又可以称为纹理图像或纹理。
纹理映射的作用,就是根据纹理图像为之前光栅化后的每个片元涂上适当的颜色,组成纹理图像的像素又被称为纹素,每一个纹素的颜色都使用RGB
或RGBA
格式编码。如图:
图中的每个小方块都是一个纹素(图片来源)。
纹理映射
问:在WebGL
中进行纹理映射,分为几步?
答:4步。
第一步 - 准备纹理图像
作为一名龙珠的爱好者,在此我就准备了一张悟空的图片(图片来源):
第二步 - 为几何图形配置映射方式
指定映射方式就是确定“几何图形的某个片元”的颜色如何决定。我们利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标来确定纹理图像的哪部分将覆盖到几何图形上。纹理坐标是一套新的坐标系统,下面将会对纹理坐标进行简单的介绍。
纹理坐标
纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。WebGL
系统中的纹理坐标系统是二维的,为了将纹理坐标和我们平时使用的坐标系统区分开来,WebGL
中使用s
和t
命名纹理坐标系统(st
坐标系统):
如图,在纹理坐标系中,纹理图像的左下角为(0.0, 0.0)
,右上角为(1.0, 1.0)
。不要与WebGL
的坐标系统搞混哦!
将纹理映射到几何图形
来看看这张图:
这张图是将纹理图像的顶点映射到WebGL
坐标系统中的四个顶点处,有小伙伴可能会想到“将这个长方形的图片映射到一个正方形的区域,图片岂不是会变形”,要注意在WebGL
坐标系统中我们使用的(0.5, 0.5, 0.0)
这种坐标是一个相对的坐标值,如果我们的canvas
是个正方形,那么上图中对应的映射区域就是个正方形,如果是长方形,同理映射区域就是个长方形。下面来看看我们的着色器如何编写:
// 顶点着色器
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
顶点着色器中多声明了一个vec2
变量,用来接收纹理图像的坐标,而在片元着色器会在稍后介绍。再修改一下initVertexBuffers
方法:
function initVertexBuffers (gl) {
const verticesTexCoords = new Float32Array([
// 顶点坐标 纹理坐标
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0,
]);
const n = 4;
// 创建缓冲区对象
const vertexTexCoordBuffer = gl.createBuffer();
// ...
// 将顶点坐标写入缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
// ...
// 将纹理坐标分配给a_TexCoord并开启它
const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
// ...
return n;
}
上面代码在之前的文章中写过很多遍,主要是添加了纹理坐标,就不再赘述。这样就在顶点着色器中接收到了纹理坐标,并光栅化后传给片元着色器;随后,片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元,并设置顶点的纹理坐标(initVertexBuffers()
)。
第三步 - 加载纹理图像
加载纹理图像要使用我们的Image
对象来完成:
function initTexture (gl, n) {
const texture = gl.createTexture(); // 创建纹理对象
// 获取 u_Sampler 的存储位置(会在第四步中介绍)
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
const image = new Image();
// 注册图像加载事件响应函数
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image);
};
image.src = '...';
return true;
}
function loadTexture (gl, n, u_Sampler, image) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); //翻转纹理图像的 y 轴
gl.activeTexture(gl.TEXTURE0); // 开启 0 号纹理单元
gl.bindTexture(gl.TEXTURE_2D, texture); // 向 target 绑定纹理对象
// 配置纹理参数
gl.texParameteri(gl.TEXTRUE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 配置纹理图像
gl.texImage2D(gl.TEXTURE_2D, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
gl.uniform1i(u_Sampler, 0); // 将 0 号纹理传递给着色器
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // 绘制矩形
}
initTexture
函数中应该比较好理解,下面将直接介绍loadTexture
函数。首先在我们的WebGL
系统中有8个纹理单元分别是gl.TEXTURE0
到gl.TEXTURE7
,这每一个纹理单元都与gl.TEXTURE_2D
相关联,而后者就是绑定纹理时的纹理目标:
当调用gl.createTexture
后,WebGL
系统中就会存在一个纹理对象:
坐标轴翻转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
函数是WebGL
中的图像预处理函数,第一个参数是处理方式,第二个参数为处理方式的参数。
WebGL
中的纹理坐标系统的t
轴方向与PNG/BMP/JPG等格式图片的坐标系统的y
轴方向是相反的。所以只有先将图像的y
轴进行反转,才能将图像正确地映射到图形上:
激活纹理单元
WebGL
通过一种叫做纹理单元的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像,一些其他系统支持的个数更多。内置变量gl.TEXTURE0
到gl.TEXTURE7
各代表一个纹理单元。
在使用纹理单元之前,需要调用gl.activeTexture(gl.TEXTURE0)
来激活它(下图中激活的是TEXTURE0
):
绑定纹理对象
接下来,我们还要告诉WebGL
系统纹理对象使用的是哪种类型的纹理。在对纹理对象操作之前,我们需要绑定纹理对象,这里会发现这一系列的操作和缓冲区很相似:在对缓冲区对象进行操作之前,也需要绑定缓冲区对象。WebGL
支持两种类型的纹理:gl.TEXTURE_2D
和gl.TEXTURE_CUBE_MAP
,分别为二维纹理和立方体纹理。当调用gl.bindTexture
后:
这样我们就指定了纹理对象的类型(gl.TEXTURE_2D
)。
配置纹理对象参数
配置纹理对象的参数的目标主要是设置:如何根据纹理坐标获取纹素颜色、以及按哪种方式重复填充纹理。对于gl.texParameteri()
方法的参数含义如下图:
gl.TEXTURE_MAG_FILTER
和gl.TEXTURE_MIN_FILTER
的非金字塔纹理类型常量:
可以赋值给gl.TEXTURE_WRAP_S
和gl.TEXTURE_WRAP_T
的常量(可以想象一下以往在Windows系统中设置桌面壁纸时的平铺/拉伸等选项):
将纹理图像分配给纹理对象
使用gl.texImage2D
方法将纹理图像分配给纹理对象,同时该函数还允许告诉WebGL
系统关于该图像的一些特性。此API参数比较复杂,详细了解请参考MDN texImage2D。
第四步 - 在FS中抽取纹素并赋给片元
将纹理单元传递给片元着色器
首先让我们来看一下片元着色器代码:
// 片元着色器
#ifdef GL_ES
precision mediump float;
#endif
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
我们在示例程序中使用了gl.TEXTURE_2D
这种二维纹理,所以在片元着色器中定义的uniform
变量的数据类型应该为sampler2D
,除此之外还有samplerCube
(这种数据类型对应gl.TEXTURE_CUBE_MAP
)。
在initTexture
函数中,我们获取到了uniform
变量u_Sampler
的存储地址,并将其作为参数传给loadTexture
函数。我们必须通过指定纹理单元编号(即gl.TEXTUREn
中的n
)将纹理传给u_Sampler
。因为我们绑定到了gl.TEXTURE0
上,所以调用gl.uniform1i
时,第二个参数设为0:
从顶点着色器向片元着色器传输纹理坐标
我们是通过attribute
变量a_TexCoord
接收顶点的纹理坐标,所以将数据赋值给varying
变量v_TexCoord
并将纹理坐标传入片元着色器是行得通的。
剩下的工作就是,根据片元的纹理坐标,从纹理图像上抽取出纹素的颜色,然后涂到当前的片元上。
在片元着色器中获取纹理像素颜色
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
我们使用GLSL ES
内置函数texture2D()
来抽取纹素颜色,该函数使用起来十分方便,只需要传入两个参数——纹理单元编号和纹理坐标,就可以获取到纹理上的像素颜色。
纹理放大和缩小方法的参数将决定WebGL
系统将以何种方式内插出片元。我们将texture2D()
函数的返回值赋给了gl_FragColor
变量,然后片元着色器就将当前片元涂成这个颜色。最后,纹理图像就被映射到了图形上,并最终被画了出来。
下面让我们打开页面看一下效果(因为跨域原因,大家需在本地启用http
服务器):
怎么漆黑一片呢?
别急,先来仔细看一下console
信息:
会发现warning
中有说到我们的纹理图像无法渲染,可能因为图片尺寸不是2的整数次方,那么让我们把图片裁剪成256 x 256
大小的再试一下呢?
完美 我们目前使用的都是WebGL1 .0
的特性,在WebGL 2.0
中支持了非2的整数次方大小的纹理图像!
我们已经成功展示出一张图片了,但是在WebGL
系统中有多个纹理单元,所以我们可以展示多张图片,比如我给悟空图片上再加一张图片:
这里就不详细描述了,给一点提示:片元着色器中texture2D
内置函数返回的是vec4
类型的color
,而对于两张图片的重叠部分:
gl_FragColor = color0 * color1;
可以通过以上方式计算得出!
结束语
纹理部分内容较多,大家可以慢慢学习一下,再次总结一下主要分为四步:
- 准备纹理图像;
- 为几何图形配置映射方式;
- 加载纹理图像:
- 翻转坐标轴(
gl.pixelStorei
); - 激活纹理单元(
gl.activeTexture
); - 绑定纹理对象(
gl.bindTexture
); - 配置纹理参数(
gl.texParameteri
); - 配置纹理图像(
gl.texImage2D
); - 将纹理单元传给着色器。
- 在FS中抽取纹素并赋给片元(
texture2D
)。
有趣的纹理映射介绍到这里啦,后续会出更多好玩并且有用的文章分享给大家,欢迎关注公众号:Refactor,感谢阅读!