输入纹理资源
前言
在webGL
中,纹理有两种类型,一种是2d
的,另一种是天空盒类型的。纹理除了用于给物体表面贴图之外,还可以用于数据传输,十分有用。
纹理数据的来源可以是图片、视频、帧缓冲、canvas
甚至还可以是数组,对于着色器而言,纹理就是一类特定的数据,在着色器可以使用取样器来进行数据取样(解析)。
纹理坐标系
如上图所示(图片来源),纹理坐标系的原点位于纹理区域的左下角,然后s
轴从左往右逐渐增大,范围为[0, 1]
;同理,t
轴从下往上逐渐增大,范围也为[0, 1]
。
记住纹理坐标系的特点能便于纹理数据的取样以及坐标系的变换,因为纹理坐标系和webGL
的其他坐标系的特点不一样,如果需要将纹理映射到目标坐标系就需要进行坐标系变换了。
单个纹理输入
2D纹理输入的一般流程
-
加载纹理资源或创建纹理资源;
-
创建纹理缓冲:
1
let textureBuffer = gl.createBuffer()
-
激活相应的纹理通道:
1
gl.activeTexture(gl.TEXTURE0 + textureID)
-
将纹理缓冲绑定到相应的纹理通道上:
1
gl.bindTexture(gl.TEXTURE_2D, textureBuffer)
-
将纹理通道号传递给纹理取样器属性:
1
gl.uniform1i(pos, textureID) // pos指的是纹理取样器属性的地址
-
绑定纹理数据:
1
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value)
注:这里的
texImage2D()
方法参数仅是举例,实际上该方法有多种参数形式,需要根据具体情况来设置! -
设置纹理取样参数:
1
2
3
4gl.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 | uniform sampler2D u_Texture; // 纹理取样器 |
不同类型的纹理资源的输入流程基本一致,只是在加载步骤以及绑定纹理数据的时候有所不同。
不同类型纹理资源的加载流程
图片资源
注:这里的图片资源指的是<img>
元素(即HTMLImageElement
对象),既包DOM
中本身存在的元素,也包括通过Image()
构造函数手动创建的元素。
图片资源需要在加载完成后才能绑定到纹理缓冲上去,可以利用HTMLImageElement
对象自带的onload
方法就能监听到图片数据加载完成的事件:
1 | let img = new Image() |
没错,HTMLImageElement
本身可以直接传入texImage2D()
方法中做参数!而在DOM
中获取到的<img>
结点和利用base64
字符串构建的图片也是一样的,需要在onload
回调中才进行纹理数据的传递!
视频资源
注:这里的视频资源指的是<video>
元素(即HTMLVideoElement
对象),既包含DOM
中存在的元素也包括createElement()
方法创建的<video>
元素(<video>
元素没有对应的构造函数!)。
1 | let video = document.createElement('video') |
-
同样,
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 | let img = new ImageData( |
帧缓冲
帧缓冲可以看做等同于屏幕画面的帧,具有显示帧的一切绘制操作,除了不能直接显示。而通过将纹理缓冲绑定到帧缓冲上,可以直接利用webGL
的绘制API
(包括着色器)来绘制出想要的纹理,然后传递该纹理数据!
详细步骤可以参考——利用FBO传送粒子数据 | snowdream。
天空盒纹理输入的一般流程
不同于2D
纹理,天空盒(skybox
)纹理实际上是一个立方体纹理,即包含六个正方形的2D
纹理,而取样则是从立方体内部(中心点)通过射线方向来取样。
因此,天空盒纹理实际上就是由6
张2D
纹理组成的,因此输入的一般流程跟2D
类似:
-
加载纹理资源或创建纹理资源;
-
创建纹理缓冲:
1
let textureBuffer = gl.createBuffer()
-
激活相应的纹理通道:
1
gl.activeTexture(gl.TEXTURE0 + textureID)
-
将纹理缓冲绑定到相应的纹理通道上:
1
gl.bindTexture(gl.TEXTURE_CUBE_MAP, textureBuffer)
注意:这里的纹理类型为TEXTURE_CUBE_MAP!
-
将纹理通道号传递给纹理取样器属性:
1
gl.uniform1i(pos, textureID) // pos指的是纹理取样器属性的地址
-
绑定纹理数据:
1
2
3
4
5
6gl.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]) // 后可以看到,天空盒纹理在绑定纹理数据的时候要分别绑定
6
个2D
纹理数据,每个纹理数据分别对应立方体的一个面,所以千万注意不要把面的对应关系绑定错了!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
-
设置纹理取样参数:
1
2
3
4gl.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 | let skybox = [ |
上面这种方法是通过闭包的方式来判断图片是否已全部加载完成;当然,也可以将一个图片加载完成过程封装成一个Promise
对象,然后利用Promise.all()
方法来执行所有图片加载完成的回调。
其它类型的纹理资源也是同理的,不再赘述。
多个纹理输入
输入多个纹理资源,实际上每个纹理资源的加载和传递等步骤同单个纹理资源的是一样的,不同的是多个纹理资源需要额外注意纹理通道的分配和纹理缓冲的对应等问题。
注意事项
-
纹理通道的分配:因为每个着色器内的取样器连接的地址就是纹理通道,当多个纹理缓冲共用一个通道时,那么就相当于这个通道连接的所有取样器实际上都是共用一个纹理缓冲(最近更新数据那个纹理缓冲);这显然是不可行的。因此需要给同一个
webGL
上下文内的所有纹理缓冲进行编号,可以设定一个类似于ID
的自增属性(从0
开始),在TEXTURE0
常量上加上这个自增属性即可得到对应的纹理通道。1
2
3
4
5
6
7
8let textureID = 0 // 纹理通道的编号
//
let textureBuffer = gl.createBuffer()
gl.activeTexture(gl.TEXTURE0 + textureID)
gl.bindTexture(gl.TEXTURE_2D)
gl.uniform1i(pos, textureID)
// ...
textureID++ // 创建并绑定完相应的纹理缓冲后编号自增TEXTURE0
常量指的就是0
号纹理通道的地址,后面的纹理通道地址都是在此基础上自增的;上面只是举了一个最简单的例子,实际上可以声明一个纹理资源信息的类,直接将得到纹理编号绑定到纹理资源上。 -
纹理缓冲的绑定:当需要对多个纹理资源中的某个纹理数据进行更新时,最好是不要重复再创建纹理缓冲,应该使用首次数据传递时用的那个缓冲对象;因此建议同上面类似,在第一次创建好纹理缓冲后将缓冲对象绑定到纹理资源信息上。