Skip to content

之前章节我们学习了绘制单一和渐变颜色的三角形,但是在实际的建模中(游戏居多),模型表面往往都是丰富生动的图片。这就需要有一种机制,能够让我们把图片素材渲染到模型的一个或者多个表面上,这种机制叫做纹理贴图,本节我们学习如何使用 WebGL 进行纹理贴图。

目标

本节我们的目标是要学会纹理贴图的步骤以及注意事项。

通过本节学习,你将掌握如下内容:

  • 为什么需要贴图?
  • 贴图的步骤?
  • 注意事项。

为什么我们需要贴图?

之前章节的示例中,为图形增加色彩仅仅是用了简单的单色和渐变色,但是实际应用中往往需要一些丰富多彩的图案,我们不可能用代码来生成这些图案,费时费力,效果也不好。通常我们会借助一些图形软硬件(比如照相机、手机、PS等)准备好图片素材,然后在 WebGL 中把图片应用到图形表面。

纹理图片格式

WebGL 对图片素材是有严格要求的,图片的宽度和高度必须是 2 的 N 次幂,比如 16 x 16,32 x 32,64 x 64 等。实际上,不是这个尺寸的图片也能进行贴图,但是这样会使得贴图过程更复杂,从而影响性能,所以我们在提供图片素材的时候最好参照这个规范。

纹理坐标系统

纹理也有一套自己的坐标系统,为了和顶点坐标加以区分,通常把纹理坐标称为 UVU 代表横轴坐标,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 坐标插值化,并传递给片元着色器。

glsl
    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 传递过来的纹理资源(图片数据)。
glsl
	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 部分

我们首先要将纹理图片加载到内存中:

javascript
    var img = new Image();
    img.onload = textureLoadedCallback;
    img.src = "";

图片加载完成之后才能执行纹理的操作,我们将纹理操作放在图片加载完成后的回调函数中,即textureLoadedCallback

需要注意的是,我们使用 canvas 读取图片数据是受浏览器跨域限制的,所以首先要解决跨域问题。

那么,针对图片跨域问题我们可以采用三种方式来解决:

第一种方法:设置允许 Chrome 跨域加载资源

在本地开发阶段,我们可以设置 Chrome 浏览器允许加载跨域资源,这样就可以使用磁盘地址来访问页面了。

mac 设置方法如下:

bash
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir(指定目录,例如 = /user/Documents)

第二种方法:图片资源和页面资源放在同一个域名下

除了设置 Chrome,我们还可以将图片资源和页面资源部署在同一域名下,这样就不存在跨域问题了。

第三种方法:为图片资源设置跨域响应头

实际生产环境中,图片资源往往部署在 CDN 上,图片和页面分属不同域,这种情况的跨域访问我们就需要正面解决了。

假设我们的图片资源所属域名为:https://cdn-pic.com,页面所属域名为 https://test.com

解决方法如下:

  • 首先:为图片资源设置跨域响应头:
glsl
Access-Control-Allow-Origin:`https://test.com`
  • 其次:在图片加载时,为 img 设置 crossOrigin 属性。
javascript
var img = new Image();
img.crossOrigin = '';
img.src = 'https://cdn-pic.com/test.jpg'

做完这两步,我们就可以真正的加载跨域图片了。 解决了图片加载跨域问题,我们就可以开始纹理贴图了。

我们定义六个顶点,这六个顶点能够组成一个矩形,并为顶点指定纹理坐标。

javascript
    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 的代码已经很熟悉了。

加载图片

javascript
	var img  = new Image();
	img.onload = textureLoadedCallback;
	img.src="";

您或许看到我并没有为 img 设置 crossOrigin 属性,原因是在我本地,图片和页面在同一个域名下,所以不需要额外设置。

图片加载完成后,我们进行如下操作:

首先:激活 0 号纹理通道gl.TEXTURE0,0 号纹理通道是默认值,本例也可以不设置。

javascript
	gl.activeTexture(gl.TEXTURE0);

然后创建一个纹理对象:

javascript
	var texture = gl.createTexture();

之后将创建好的纹理对象texture绑定 到当前纹理绑定点上,即 gl.TEXTURE_2D。绑定完之后对当前纹理对象的所有操作,都将基于 texture 对象,直到重新绑定。

javascript
	gl.bindTexture(gl.TEXTURE_2D, texture);

为片元着色器传递图片数据:

javascript
	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 采用最靠近象素中心的纹素,该算法可能使图像走样,但是执行效率高,不需要额外的计算。

javascript
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

之后为片元着色器传递 0 号纹理单元:

javascript
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:设置图片放大缩小时的过滤算法。

以上是截止到目前所涉及的知识点,大家可以按照自己的想法做些小例子,熟练掌握它们。

接下来我们开始学习如何使用基本图元构建 3D 模型。