之前章节我们学习了绘制单一和渐变颜色的三角形,但是在实际的建模中(游戏居多),模型表面往往都是丰富生动的图片。这就需要有一种机制,能够让我们把图片素材渲染到模型的一个或者多个表面上,这种机制叫做纹理贴图
,本节我们学习如何使用 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));
}
JavaScript 部分
我们首先要将纹理图片加载到内存中:
var img = new Image();
img.onload = textureLoadedCallback;
img.src = "";
图片加载完成之后才能执行纹理的操作,我们将纹理操作放在图片加载完成后的回调函数中,即textureLoadedCallback
。
需要注意的是,我们使用 canvas 读取图片数据是受浏览器跨域限制的,所以首先要解决跨域问题。
那么,针对图片跨域问题我们可以采用三种方式来解决:
第一种方法:设置允许 Chrome 跨域加载资源
在本地开发阶段,我们可以设置 Chrome 浏览器允许加载跨域资源,这样就可以使用磁盘地址来访问页面了。
mac 设置方法如下:
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir(指定目录,例如 = /user/Documents)
第二种方法:图片资源和页面资源放在同一个域名下
除了设置 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
]
按照惯例,我们该为着色器传递数据了。
经历过前面几个小节的练习,相信大家对操作 WebGL 的代码已经很熟悉了。
加载图片
var img = new Image();
img.onload = textureLoadedCallback;
img.src="";
您或许看到我并没有为 img 设置
crossOrigin
属性,原因是在我本地,图片和页面在同一个域名下,所以不需要额外设置。
图片加载完成后,我们进行如下操作:
首先:激活 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.texImage2D 方法是一个重载方法,其中有一些参数可以省略:
glTexImage2D(GLenum target, GLint level, GLint components, GLsizei width, glsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
参数 | 含义 |
---|---|
target | 纹理类型,TEXTURE_2D代表2维纹理 |
level | 表示多级分辨率的纹理图像的级数,若只有一种分辨率,则 level 设为 0,通常我们使用一种分辨率 |
components | 纹理通道数,通常我们使用 RGBA 和 RGB 两种通道 |
width | 纹理宽度,可省略 |
height | 纹理高度,可省略 |
border | 边框,通常设置为0,可省略 |
format | 纹理映射的格式 |
type | 纹理映射的数据类型 |
pixels | 纹理图像的数据 |
上面这段代码的意思是,我们将 img 变量指向的图片数据传递给片元着色器,取对应纹理坐标的 RGBA 四个通道值,赋给片元,每个通道的数据格式是无符号单字节整数。
接下来,我们设置图片在放大或者缩小时采用的算法gl.LINEAR
。
gl.LINEAR 代表采用最靠近象素中心的四个象素的加权平均值,这种效果表现的更加平滑自然。 gl.NEAREST 采用最靠近象素中心的纹素,该算法可能使图像走样,但是执行效率高,不需要额外的计算。
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);
这里,我们为片元着色器的 texture 属性传递 0,此处应该与激活纹理时的通道值保持一致。
图片作为纹理的渲染效果如下:
可以看到,我们绘制的矩形表面贴上了纹理。
您或许有疑问,为什么我只是指定了三角形的顶点对应的 UV 坐标,GPU 就能够将纹理图片的其他坐标的颜色贴到三角形表面呢?
这其实,就回归到了渲染管线
这个概念上,在第一节我画了个图,大致阐述了渲染管线的工作方式,但其实在光栅化环节上有些细节没有说到。 在光栅化阶段,GPU 处理两件事情:
- 计算图元覆盖了哪些像素。
- 根据顶点着色器的顶点位置计算每个像素的纹理坐标的插值。
注:片元可以理解为像素。
光栅化结束后,来到片元着色器,片元着色器此时知道每个像素对应的 UV
坐标,根据当前像素的 UV
坐标,找到纹理资源对应坐标的颜色信息,赋值给当前像素,从而能够为图元表面的每个像素贴上正确的纹理颜色。
注意事项
我们总结一下贴图的注意点:
- 图片最好满足 2^m x 2^n 的尺寸要求。
- 图片数据首先加载到内存中,才能够在纹理中使用。
- 图片资源加载前要先解决跨域问题。
回顾
至此,我们使用 WebGL 绘制平面的课程就结束了,总结一下之前章节所学的知识点:
- GLSL:着色器
- 数据类型
- vec2:2 维向量容器。
- vec4:4 维向量容器。
- 运算法则:向量与向量、向量与浮点数的运算法则。
- 修饰符
- attribute:属性修饰符。
- uniform:全局变量修饰符。
- varying:顶点着色器传递给片元着色器的属性修饰符。
- precision:设置精度
- highp:高精度。
- mediump:中等精度。
- lowp:低精度。
- 内置变量
- gl_Position:顶点坐标。
- gl_FragColor:片元颜色。
- gl_PointSize:顶点大小。
- 屏幕坐标系到设备坐标系的转换。
- 屏幕坐标系左上角为原点,X 轴坐标向右为正,Y 轴坐标向下为正。
- 坐标范围:
- X轴:【0, canvas.width】
- Y轴:【0, canvas.height】
- 设备坐标系以屏幕中心为原点,X 轴坐标向右为正,Y 轴向上为正。
- 坐标范围是
- X轴:【-1, 1】。
- Y轴:【-1, 1】。
- 数据类型
- WebGL API
- shader:着色器对象
- gl.createShader:创建着色器。
- gl.shaderSource:指定着色器源码。
- gl.compileShader:编译着色器。
- program:着色器程序
- gl.createProgram:创建着色器程序。
- gl.attachShader:链接着色器对象。
- gl.linkProgram:链接着色器程序。
- gl.useProgram:使用着色器程序。
- attribute:着色器属性
- gl.getAttribLocation:获取顶点着色器中的属性位置。
- gl.enableVertexAttribArray:启用着色器属性。
- gl.vertexAttribPointer:设置着色器属性读取 buffer 的方式。
- gl.vertexAttrib2f:给着色器属性赋值,值为两个浮点数。
- gl.vertexAttrib3f:给着色器属性赋值,值为三个浮点数。
- uniform:着色器全局属性
- gl.getUniformLocation:获取全局变量位置。
- gl.uniform4f:给全局变量赋值 4 个浮点数。
- gl.uniform1i:给全局变量赋值 1 个整数。
- buffer:缓冲区
- gl.createBuffer:创建缓冲区对象。
- gl.bindBuffer:将缓冲区对象设置为当前缓冲。
- gl.bufferData:向当前缓冲对象复制数据。
- clear:清屏
- gl.clearColor:设置清除屏幕的背景色。
- gl.clear:清除屏幕。
- draw:绘制
- gl.drawArrays:数组绘制方式。
- gl.drawElements:索引绘制方式。
- 图元
- gl.POINTS:点。
- gl.LINE:基本线段。
- gl.LINE_STRIP:连续线段。
- gl.LINE_LOOP:闭合线段。
- gl.TRIANGLES:基本三角形。
- gl.TRIANGLE_STRIP:三角带。
- gl.TRIANGLE_FAN:三角扇。
- 纹理
- gl.createTexture:创建纹理对象。
- gl.activeTexture:激活纹理单元。
- gl.bindTexture:绑定纹理对象到当前纹理。
- gl.texImage2D:将图片数据传递给 GPU。
- gl.texParameterf:设置图片放大缩小时的过滤算法。
- shader:着色器对象
以上是截止到目前所涉及的知识点,大家可以按照自己的想法做些小例子,熟练掌握它们。
接下来我们开始学习如何使用基本图元构建 3D 模型。