之前章节我们学习了绘制单一和渐变颜色的三角形,但是在实际的建模中(游戏居多),模型表面往往都是丰富生动的图片。这就需要有一种机制,能够让我们把图片素材渲染到模型的一个或者多个表面上,这种机制叫做纹理贴图,本节我们学习如何使用 WebGL 进行纹理贴图。
什么是贴图和贴图的格式
之前章节的示例中,为图形增加色彩仅仅是用了简单的单色和渐变色,但是实际应用中往往需要一些丰富多彩的图案,我们不可能用代码来生成这些图案,费时费力,效果也不好。通常我们会借助一些图形软硬件(比如照相机、手机、PS等)准备好图片素材,然后在 WebGL 中把图片应用到图形表面。WebGL 对图片素材是有严格要求的,图片的宽度和高度必须是 2 的 N 次幂,比如 16 x 16,32 x 32,64 x 64 等。实际上,不是这个尺寸的图片也能进行贴图,但是这样会使得贴图过程更复杂,从而影响性能,所以我们在提供图片素材的时候最好参照这个规范。
纹理坐标系统
纹理也有一套自己的坐标系统,为了和顶点坐标加以区分,通常把纹理坐标称为 UV,U 代表横轴坐标,V 代表纵轴坐标。
图片坐标系统的特点是:
左上角为原点(0, 0)。
- 向右为横轴正方向,横轴最大值为 1,即横轴坐标范围【1,0】。
- 向下为纵轴正方向,纵轴最大值为 1,即纵轴坐标范围【0,1】。
- 纹理坐标系统不同于图片坐标系统,它的特点是:
左下角为原点(0, 0)。
- 向右为横轴正方向,横轴最大值为 1,即横轴坐标范围【1,0】。
- 向上为纵轴正方向,纵轴最大值为 1,即纵轴坐标范围【0,1】。
如下图所示:
纹理坐标系统可以理解为一个边长为 1 的正方形。
按照规范所讲,我们首先准备一张符合要求的图片,这里自己制作一个尺寸为宽高分别是 2 的 7 次方,即 128 x 128 的图片。本节片元着色器中,不再是接收单纯的颜色了,而是接收纹理图片对应坐标的颜色值,所以我们的着色器要能够做到如下几点:
顶点着色器接收顶点的 UV 坐标,并将UV坐标传递给片元着色器。
- 片元着色器要能够接收顶点插值后的UV坐标,同时能够在纹理资源找到对应坐标的颜色值。
- 我们看下如何修改才能满足这两点:
顶点着色器
- 首先,增加一个名为 v_Uv 的 attribute 变量,接收 JavaScript 传递过来的 UV 坐标。
- 其次,增加一个 varying 变量 v_Uv,将 UV 坐标插值化,并传递给片元着色器。
precision mediump float;
// 接收顶点坐标 (x, y)
attribute vec2 a_Position;
// 接收 canvas 尺寸(width, height)
attribute vec2 a_Screen_Size;
// 接收JavaScript传递过来的顶点 uv 坐标。
attribute vec2 a_Uv;
// 将接收的uv坐标传递给片元着色器
varying vec2 v_Uv;
void main(){
vec2 position = (a_Position / a_Screen_Size) * 2.0 - 1.0;
position = position * vec2(1.0,-1.0);
gl_Position = vec4(position, 0, 1);
// 将接收到的uv坐标传递给片元着色器
v_Uv = a_Uv;
}
片元着色器 首先,增加一个 varying 变量 v_Uv,接收顶点着色器插值过来的 UV 坐标。
其次,增加一个 sampler2D 类型的全局变量 texture,用来接收 JavaScript 传递过来的纹理资源(图片数据)。
precision mediump float;
// 接收顶点着色器传递过来的 uv 值。
varying vec2 v_Uv;
// 接收 JavaScript 传递过来的纹理
uniform sampler2D texture;
void main(){
// 提取纹理对应uv坐标上的颜色,赋值给当前片元(像素)。
gl_FragColor = texture2D(texture, vec2(v_Uv.x, v_Uv.y));
}
我们首先要将纹理图片加载到内存中:
var img = new Image();
img.onload = textureLoadedCallback;
img.src = "";
图片加载完成之后才能执行纹理的操作,我们将纹理操作放在图片加载完成后的回调函数中,即textureLoadedCallback。
需要注意的是,我们使用 canvas 读取图片数据是受浏览器跨域限制的,所以首先要解决跨域问题。
那么,针对图片跨域问题我们可以采用三种方式来解决:
第一种方法:设置允许 Chrome 跨域加载资源
在本地开发阶段,我们可以设置 Chrome 浏览器允许加载跨域资源,这样就可以使用磁盘地址来访问页面了。
第二种方法:图片资源和页面资源放在同一个域名下
除了设置 Chrome,我们还可以将图片资源和页面资源部署在同一域名下,这样就不存在跨域问题了。
第三种方法:为图片资源设置跨域响应头
实际生产环境中,图片资源往往部署在 CDN 上,图片和页面分属不同域,这种情况的跨域访问我们就需要正面解决了。
假设我们的图片资源所属域名为:https://cdn-pic.com,页面所属域名为 https://test.com。
解决方法如下:
首先:为图片资源设置跨域响应头:
Access-Control-Allow-Origin:`https://test.com`
其次:在图片加载时,为 img 设置 crossOrigin 属性。
var img = new Image();
img.crossOrigin = '';
img.src = 'https://cdn-pic.com/test.jpg'
做完这两步,我们就可以真正的加载跨域图片了。 解决了图片加载跨域问题,我们就可以开始纹理贴图了。
我们定义六个顶点,这六个顶点能够组成一个矩形,并为顶点指定纹理坐标。
var positions = [
30, 30, 0, 0, //V0
30, 300, 0, 1, //V1
300, 300, 1, 1, //V2
30, 30, 0, 0, //V0
300, 300, 1, 1, //V2
300, 30, 1, 0 //V3
]
加载图片
var img = new Image();
img.onload = textureLoadedCallback;
img.src="";
图片加载完成后,我们进行如下操作:
首先:激活 0 号纹理通道gl.TEXTURE0,0 号纹理通道是默认值,本例也可以不设置。
gl.activeTexture(gl.TEXTURE0);
//创建纹理
var texture = gl.createTexture();
// 之后将创建好的纹理对象texture绑定 到当前纹理绑定点上,即 gl.TEXTURE_2D。绑定完之后对当前纹理对象的所有操作,都将基于 texture 对象,直到重新绑定。
gl.bindTexture(gl.TEXTURE_2D, texture);
// 为片元着色器传递图片数据:
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
// 我们设置图片在放大或者缩小时采用的算法gl.LINEAR
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 之后为片元着色器传递 0 号纹理单元:
gl.uniform1i(uniformTexture, 0);
注意事项
我们总结一下贴图的注意点:
- 图片最好满足 2^m x 2^n 的尺寸要求。
- 图片数据首先加载到内存中,才能够在纹理中使用。
- 图片资源加载前要先解决跨域问题。