初级入门 --- 纹理贴图:为形体穿上外衣

之前章节我们学习了绘制单一和渐变颜色的三角形,但是在实际的建模中(游戏居多),模型表面往往都是丰富生动的图片。这就需要有一种机制,能够让我们把图片素材渲染到模型的一个或者多个表面上,这种机制叫做纹理贴图,本节我们学习如何使用 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 的尺寸要求。
  • 图片数据首先加载到内存中,才能够在纹理中使用。
  • 图片资源加载前要先解决跨域问题。
posted @ 2023-01-30 10:18  自在一方  阅读(89)  评论(0编辑  收藏  举报