输入纹理资源

前言

webGL中,纹理有两种类型,一种是2d的,另一种是天空盒类型的。纹理除了用于给物体表面贴图之外,还可以用于数据传输,十分有用。

纹理数据的来源可以是图片、视频、帧缓冲、canvas甚至还可以是数组,对于着色器而言,纹理就是一类特定的数据,在着色器可以使用取样器来进行数据取样(解析)。

纹理坐标系

20200209164113

如上图所示(图片来源),纹理坐标系的原点位于纹理区域左下角,然后s轴从左往右逐渐增大,范围为[0, 1];同理,t轴从下往上逐渐增大,范围也为[0, 1]

记住纹理坐标系的特点能便于纹理数据的取样以及坐标系的变换,因为纹理坐标系和webGL的其他坐标系的特点不一样,如果需要将纹理映射到目标坐标系就需要进行坐标系变换了。

单个纹理输入

2D纹理输入的一般流程

  1. 加载纹理资源或创建纹理资源;

  2. 创建纹理缓冲

    1
    let textureBuffer = gl.createBuffer()
  3. 激活相应的纹理通道

    1
    gl.activeTexture(gl.TEXTURE0 + textureID)
  4. 将纹理缓冲绑定到相应的纹理通道上

    1
    gl.bindTexture(gl.TEXTURE_2D, textureBuffer)
  5. 将纹理通道号传递给纹理取样器属性

    1
    gl.uniform1i(pos, textureID) // pos指的是纹理取样器属性的地址
  6. 绑定纹理数据

    1
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value)

    :这里的texImage2D()方法参数仅是举例,实际上该方法有多种参数形式,需要根据具体情况来设置!

  7. 设置纹理取样参数

    1
    2
    3
    4
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) // 指定纹理S轴方向大小适应方式
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) // 指定纹理T轴方向大小适应方式
    gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) // 指定纹理缩小取样算法
    gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) // 指定纹理放大取样算法

    举的例子是比较常见的取样参数设置。

对于着色器而言,只需要设置一个纹理取样器属性用来接收纹理数据即可,然后使用texture2D()函数来进行取样:

1
2
3
4
5
6
7
uniform sampler2D u_Texture; // 纹理取样器
// ...
void main() {
//...
vec4 textureColor = texture2D(u_Texture, texturePos); // 取样
//...
}

不同类型的纹理资源的输入流程基本一致,只是在加载步骤以及绑定纹理数据的时候有所不同。

不同类型纹理资源的加载流程

图片资源

:这里的图片资源指的是<img>元素(即HTMLImageElement对象),既包DOM中本身存在的元素,也包括通过Image()构造函数手动创建的元素。

图片资源需要在加载完成后才能绑定到纹理缓冲上去,可以利用HTMLImageElement对象自带的onload方法就能监听到图片数据加载完成的事件:

1
2
3
4
5
6
7
8
let img = new Image()

img.onload = () => {
// ...
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
// ...
}
img.src = 'img_path.png'

没错,HTMLImageElement本身可以直接传入texImage2D()方法中做参数!而在DOM中获取到的<img>结点和利用base64字符串构建的图片也是一样的,需要在onload回调中才进行纹理数据的传递!

视频资源

:这里的视频资源指的是<video>元素(即HTMLVideoElement对象),既包含DOM中存在的元素也包括createElement()方法创建的<video>元素(<video>元素没有对应的构造函数!)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let video = document.createElement('video')

function videoLoaded () {
// ...
video.play() // 开始播放
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video)
// ...
video.removeEventListener('canplaythrough', videoLoaded) // 事件触发后马上解绑事件
}

video.addEventListener('canplaythrough', videoLoaded) // 监听视频缓冲是否能够进行播放了
video.muted = true // 静音
video.preload = 'auto' // 开启预加载
video.src = 'video_path.mp4' // 写入视频地址
  • 同样,HTMLVideoElement对象能够直接传入texImage2D()方法中做参数!

  • canplaythrough事件会在视频缓冲到一定长度且能够播放时进行触发,因此可以以此来作为视频资源可用的依据,在这个回调里进行纹理数据的传递等操作。不过需要注意的是,该事件在开启loop后,会在视频每一次循环的开始被触发,因此最好将回调事件设置成一次性的。

  • 设置preload属性为auto可用使得视频优先被加载(即使在未播放的状态下)。

  • 在不需要声音信息的情况下可以开启muted属性!

  • 需要注意的是,视频纹理信息并不会实时更新(其实一次传递就相当于传了当时正在播放的视频帧图像而已),因此需要在帧更新函数(如requestAnimationFrame()函数)中不停地更新当前视频资源到纹理缓冲中!更新的操作流程跟第一次传入类似,只不过不需要再重复创建纹理缓冲了!

ImageData对象

ImageData对象是一个用于描述canvas区域像素数据的数据结构。它含有以下属性:

  • data:一个Uint8ClampedArray类型的数组,数组中的元素按照RGBA的顺序排序。
  • width:像素区域的宽度。
  • height:像素区域的高度。

ImageData对象的来源有:

  • canvas上下文对象的createImageData()getImageData()方法;此外putImageData()方法可以将数据间接地写入ImageData对象。

  • ImageData()构造函数:

    new ImageData(array, width, height);

    • array:像素数据,Uint8ClampedArray类型的数组;
    • width:像素区域宽度;
    • height:像素区域高度;

从来源就可以看出,ImageData类型的纹理既可以取自canvas,也可以直接将数组转换成ImageData,即可以手动生成纹理数据

同样地,ImageData对象可以作为参数直接传入texImage2D()方法中!如:

1
2
3
4
5
6
let img = new ImageData(
new Uint8ClampedArray(new Uint8Array(pixelList), width, height),
width,
height
)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)

帧缓冲

帧缓冲可以看做等同于屏幕画面的帧,具有显示帧的一切绘制操作,除了不能直接显示。而通过将纹理缓冲绑定到帧缓冲上,可以直接利用webGL的绘制API(包括着色器)来绘制出想要的纹理,然后传递该纹理数据!

详细步骤可以参考——利用FBO传送粒子数据 | snowdream

天空盒纹理输入的一般流程

不同于2D纹理,天空盒(skybox)纹理实际上是一个立方体纹理,即包含六个正方形的2D纹理,而取样则是从立方体内部(中心点)通过射线方向来取样。

因此,天空盒纹理实际上就是由62D纹理组成的,因此输入的一般流程跟2D类似:

  1. 加载纹理资源或创建纹理资源;

  2. 创建纹理缓冲

    1
    let textureBuffer = gl.createBuffer()
  3. 激活相应的纹理通道

    1
    gl.activeTexture(gl.TEXTURE0 + textureID)
  4. 将纹理缓冲绑定到相应的纹理通道上

    1
    gl.bindTexture(gl.TEXTURE_CUBE_MAP, textureBuffer)

    注意:这里的纹理类型为TEXTURE_CUBE_MAP

  5. 将纹理通道号传递给纹理取样器属性

    1
    gl.uniform1i(pos, textureID) // pos指的是纹理取样器属性的地址
  6. 绑定纹理数据

    1
    2
    3
    4
    5
    6
    gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[0]) // 右
    gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[1]) // 左
    gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[2]) // 上
    gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[3]) // 下
    gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[4]) // 前
    gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[5]) // 后

    可以看到,天空盒纹理在绑定纹理数据的时候要分别绑定62D纹理数据,每个纹理数据分别对应立方体的一个面,所以千万注意不要把面的对应关系绑定错了

    • TEXTURE_CUBE_MAP_POSITIVE_X
    • TEXTURE_CUBE_MAP_NEGATIVE_X
    • TEXTURE_CUBE_MAP_POSITIVE_Y
    • TEXTURE_CUBE_MAP_NEGATIVE_Y
    • TEXTURE_CUBE_MAP_POSITIVE_Z
    • TEXTURE_CUBE_MAP_NEGATIVE_Z
  7. 设置纹理取样参数

    1
    2
    3
    4
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) // 指定纹理S轴方向大小适应方式
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) // 指定纹理T轴方向大小适应方式
    gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) // 指定纹理缩小取样算法
    gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) // 指定纹理放大取样算法

    举的例子是比较常见的取样参数设置。

从上面流程可以看到,除了绑定纹理缓冲和绑定纹理数据和2D纹理略有不同之外,其它基本一致。

天空盒纹理资源的加载

纹理资源的来源和类型同上面一致;而加载方式也一样,只是要多注意多个不同资源加载时需要等待所有资源全都加载完后才进行数据传递等操作!

以六张不同的纹理图片资源为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let skybox = [
'1.png',
'2.png',
'3.png',
'4.png',
'5.png',
'6.png'
] // 6张纹理图片地址
// 加载纹理图片
function loadImages () {
let i = 0
let skyList = [] // 已加载成功的图片资源
skybox.forEach(url => {
let img = new Image()
skyList.push(img)
img.onload = () => {
i++
if (i === 6) { // 所有纹理图片加载完后进行绘制
// 在这里进行加载完成后的操作
}
}
img.src = url
})
}

上面这种方法是通过闭包的方式来判断图片是否已全部加载完成;当然,也可以将一个图片加载完成过程封装成一个Promise对象,然后利用Promise.all()方法来执行所有图片加载完成的回调。

其它类型的纹理资源也是同理的,不再赘述。

多个纹理输入

输入多个纹理资源,实际上每个纹理资源的加载和传递等步骤同单个纹理资源的是一样的,不同的是多个纹理资源需要额外注意纹理通道的分配和纹理缓冲的对应等问题。

注意事项

  1. 纹理通道的分配:因为每个着色器内的取样器连接的地址就是纹理通道,当多个纹理缓冲共用一个通道时,那么就相当于这个通道连接的所有取样器实际上都是共用一个纹理缓冲(最近更新数据那个纹理缓冲);这显然是不可行的。因此需要给同一个webGL上下文内的所有纹理缓冲进行编号,可以设定一个类似于ID自增属性(从0开始),在TEXTURE0常量上加上这个自增属性即可得到对应的纹理通道。

    1
    2
    3
    4
    5
    6
    7
    8
    let textureID = 0 // 纹理通道的编号
    //
    let textureBuffer = gl.createBuffer()
    gl.activeTexture(gl.TEXTURE0 + textureID)
    gl.bindTexture(gl.TEXTURE_2D)
    gl.uniform1i(pos, textureID)
    // ...
    textureID++ // 创建并绑定完相应的纹理缓冲后编号自增

    TEXTURE0常量指的就是0号纹理通道的地址,后面的纹理通道地址都是在此基础上自增的;上面只是举了一个最简单的例子,实际上可以声明一个纹理资源信息的类,直接将得到纹理编号绑定到纹理资源上。

  2. 纹理缓冲的绑定:当需要对多个纹理资源中的某个纹理数据进行更新时,最好是不要重复再创建纹理缓冲,应该使用首次数据传递时用的那个缓冲对象;因此建议同上面类似,在第一次创建好纹理缓冲后将缓冲对象绑定到纹理资源信息上

参考文档

  1. WebGLRenderingContext.texImage2D() - Web API 接口参考 | MDN
  2. 纹理 - LearnOpenGL-CN
  3. ImageData() - Web API 接口参考 | MDN
  4. WebGL Using 2 or More Textures