WebGL 纹理贴图

729 阅读7分钟

在 webgl 中,有一个概念叫做“纹理”,通常就是指二维的栅格图像,而纹理对象就是对栅格图像的封装。将一张图片映射到一个用 webgl 绘制的几何图形上,这张图片就称为纹理图像或纹理。webgl 会根据纹理图像,为光栅化后的每个片元涂上颜色,组成纹理图像的像素也被称为纹素。

前期准备

本篇文章前期准备好了如下黄色矩形作为待贴图图形,如果你不知道这个矩形是怎么画出来的,可以移步《绘制矩形的几种方法》了解:

1.png

另外,这不龙年到了,就以如下我去沈阳故宫拍摄的康熙款的青花海水红彩龙盘作为贴图的图像:

long.jpg

注意,上图的尺寸大小为 128*128 像素,其长宽都恰巧为 2 的整数幂,这是有意为之的,因为这样可以在 webgl 获取纹理时实现快速取值。

大体思路

webgl 实现纹理贴图的大体思路就是你准备好一张作为纹理取样的图像,然后告诉 webgl 截取这张图像的哪些区域作为纹理,然后 webgl 就会使用采样器去获取纹理,最终贴图到图形中,个中还有些诸如纹理对象、纹理单元等概念和实现细节,下文将逐一介绍。

动手实现

现在我们开始动手编写代码实现纹理贴图。

采样器

先准备个采样器 uSampler,其类型为 sampler2D,表示是二维纹理(另外还有种 samplerCube 类型,表示是立方体纹理),采样器只能是 uniform 变量,在定义时需要定义浮点数的精度。

着色器可以基于一张图像建立一个或多个采样器,不同的采样器可以定义不同的规则去获取图像中的纹素颜色,然后赋给当前片元 —— 通过 webgl 的内置函数 texture2D(),传入采样器 uSampler 和纹理坐标 vTex,赋值给 gl_FragColor

// 代码片段 1
// 片元着色器源码
const fsSource = `
  precision lowp float;
  uniform sampler2D uSampler;
  varying vec2 vTex;
  void main() {
    // 片元颜色
    gl_FragColor = texture2D(uSampler, vTex);
  }
`

varying 变量

vTex 是个 varying 变量,varyingattributeuniform 一样也是存储限定符,它可以使得在片元着色器中获取顶点着色器内的变量,方法是在顶点着色器和片元着色器中同时使用 varying 声明名称相同的变量,比如 vTex,然后在顶点着色器中对 vTex 进行赋值:

<!-- 顶点着色器源码 -->
<script type="x-shader/x-vertex" id="vsSource">
  attribute vec2 aPosition;
  attribute vec2 aTex;
  uniform vec2 uCanvasSize;
  varying vec2 vTex;
  void main() {
      // 点的坐标
      float x = aPosition.x * 2.0 / uCanvasSize.x - 1.0;
      float y = 1.0 - aPosition.y * 2.0 / uCanvasSize.y;
      gl_Position = vec4(x, y , 0, 1);
      vTex = aTex;
  }
</script>

这样我们就可以在顶点着色器中接收顶点的纹理坐标,然后在片元着色器中使用了。vTex 作为 texture2D 的第二个参数需要是个类型为 vec2 的值,其值等于 aTexaTex 指明我们要截取的图像的位置,其值来源于数据源 points 每一行的后 2 个元素(对应的是纹理坐标系):

// 创建数据源,注意坐标点的顺序
const points = new Float32Array([
  100, 50, 0, 1,
  100, 150, 0, 0, 
  300, 50, 1, 1, 
  300, 150, 1, 0
])

const BYTES = points.BYTES_PER_ELEMENT
// 获取 aTex 变量的内存地址
const aTex = gl.getAttribLocation(program, 'aTex')
// 给 aTex 变量赋值
gl.vertexAttribPointer(aTex, 2, gl.FLOAT, false, BYTES * 4, BYTES * 2)
// 激活 aTex 变量
gl.enableVertexAttribArray(aTex)

纹理坐标

纹理坐标,也称为 st 坐标或 uv 坐标,它如下所示,s 轴向右,图片宽度为 1;t 轴向上,图片高度为 1。aTex 的值相当于是取了 (0, 1)、(0, 0)、(1, 1)、(1, 0) 这 4 个点,也就是整张图像。在贴图时,纹理坐标会和 webgl 中的图形(黄色矩形)形成映射关系,从而将图像的指定区域贴到 webgl 图形中:

2.png

接下来无非就是在 js 中去获取并修改采样器 uSampler了,但在此之前,还有一些对象及概念需要创建与介绍。

纹理对象

先准备好图片,确保它已经加载完成。因为图像的坐标系的 y 轴和纹理坐标系的 t 轴方向是相反的,所以需要使用 pixelStorei 对图像进行预处理,gl.UNPACK_FLIP_Y_WEBGLtrue 表示上下对称翻转坐标轴:

const img = new Image()
img.src = './imgs/long.jpg'

img.onload = () => {
  // 翻转图像的 y 轴坐标
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
  // 创建纹理对象
  const texture = gl.createTexture()
  // 接下去的代码都写在这
}

然后创建用于存储纹理图像数据的纹理对象 texture。由于纹理对象是由 js 创建的,着色器是肯定不能直接拿来用的,所以 webgl 在浏览器底层又为纹理对象创建了一块缓冲区,用于将纹理对象编译成着色器可以读懂的语言。

纹理单元

这个存储纹理对象的空间就是纹理单元,它由 webgl 创建,并且默认情况下只支持 8 个(比如 gl.TEXTURE0,gl.TEXTURE1,gl.TEXTURE2 等),每个纹理单元有一个单元编号来管理一张纹理图像,使用时需要激活:

gl.activeTexture(gl.TEXTURE0)

接着就是使用 bindTexture() 绑定纹理对象到纹理单元,第 1 个参数为绑定点,因为是二维纹理,所以传 gl.TEXTURE_2D,如果是立方体映射纹理,则传递 gl.TEXTURE_CUBE_MAP;第 2 个 参数就是要绑定的纹理对象:

gl.bindTexture(gl.TEXTURE_2D, texture)

图示如下:

3.png

指定二维纹理图像

现在,通过 gl.texImage2D() 来指定二维纹理图像:

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img)
  • 第 1 个参数指定纹理类型,值与 bindTexture 时一致;
  • 第 2 个参数先直接写 0,它指定详细级别,0 表示是基本图像;
  • 第 3个参数指定纹理中的颜色组件,或者说是纹素数据的格式,这里使用 gl.RGB,其它的可选值有 gl.RGBA 等;
  • 第 4 个参数需与第 3 个参数一致,指定纹理的内部格式;
  • 第 5 个参数指定纹理数据的数据类型,gl.UNSIGNED_BYTE 表示无符号字节,每个颜色分量占据一个字节,也就是 8 位;
  • 最后一个参数就是用作纹理的像素源的图片了。

设置纹理参数

需要贴图的矩形的尺寸是 200*100,而我们选取的纹理图像的尺寸是 128*128。 如何让两个不同尺寸进行匹配,就需要通过 texParameteri() 设置一些纹理参数,方法名中的 i 表示接收的参数是整型(int),如果是 f 则表示是浮点数(float):

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

第 1 个参数都是纹理的类型,均为 gl.TEXTURE_2D 二维纹理,后两个参数其实是键值对关系:

  • gl.TEXTURE_MAG_FILTER 用于描述纹理放大滤波器。就是当纹理的绘制范围比纹理本身更大时,指明如何去获取纹素颜色;
  • gl.TEXTURE_MIN_FILTER 用于描述纹理缩小滤波器。当纹理的绘制范围比纹理本身小时,就需要剔除纹理图像中的部分像素,此参数表示如何剔除像素;
  • gl.TEXTURE_WRAP_S 表示如何对纹理图像左侧或右侧的区域进行填充;
  • gl.TEXTURE_WRAP_T 表示如何对纹理图像上方或下方的区域进行填充;
  • gl.LINEAR 可以赋给 gl.TEXTURE_MAG_FILTERgl.TEXTURE_MIN_FILTER。表示使用距离新像素中心最近的四个像素的颜色值的加权平均,作为新像素的值。
  • gl.CLAMP_TO_EDGE 可以赋给 gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T。表示使用纹理图像边缘值。

更多细节可查看 MDN

修改采样器

现在就可以去获取采样器 uSampler 并通过 uniform1i 赋值了,唯一能赋的值是纹理单元编号。方法名中的 1i 表示接收的参数里有 1 个分量为整型:

const uSampler = gl.getUniformLocation(program, 'uSampler')
gl.uniform1i(uSampler, 0)

因为我们激活的纹理单元是 gl.TEXTURE0,所以 uniform1i 的第 2 个参数传 0。

最终效果

感谢.gif 点赞.png