WebGL不完全踩坑指北
鉴于现在各大社区已经有了很详细的WebGL入门指南,本篇就不再赘述WebGL的介绍和一些用法了。本篇就具体介绍一下使用WebGL中的一些实践。
透明度问题
才接触WebGL的新手可能会遇到一个令人困惑的问题,我在shader中设置了透明度,然后渲染出来的结果居然是这个鸟样??? (这里我们用了一张蔬菜水果的图作为背景)
下面这张才是我想要的结果啊~!
通过查询资料,我发现有这样的问题
WebGL是被浏览器混合在页面中,默认使用的是预乘阿尔法通道
什么叫预乘阿尔法通道?
预乘阿尔法通道就是将透明度与RGB依次相乘后的颜色表示, 例如: rgba(255, 0, 0, 0.5) 使用预乘阿尔法通道,则颜色为rgb(127, 0, 0);
要解决这个问题,我们可以在获取webgl上下文的时候加一个选项,告诉浏览器我们不想使用预乘阿尔法通道
let gl = canvas.getContext('webgl', {
premultipliedAlpha: false
});
这样就解决了上述的问题
传入纹理导致的性能问题
在WebGL中,要使用图片就必须使用纹理(Texture)来进行处理。具体就是通过gl.texImage2D这个API将我们的图片/视频作为一个纹理传入到WebGL中。我先描述下具体的场景:
在某个产品中,需要使用Canvas对视频和图片进行渲染,换而言之,就是不能直接使用video和img标签来直接展示。一般才入门WebGL的新手可能会写出以下的代码:
// 先将video传入GPU中作为纹理并绘制
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
// 再将img传入GPU中作为纹理并绘制
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
这样乍一看没什么问题,的确,如果canvas仅仅只绘制一次的或者绘制的频率很低,这样做的问题不大。但是由于有video的存在,video每一帧的画面都是不同的,所以需要不停的调用上面的代码来向GPU中传入video的画面,但是由于video和img使用了同一个纹理单元,所以video向GPU传递了纹理过后,之前传过的img也需要重新传递一次。
我们运行程序可以发现操作变得卡顿起来了,打开FPS Meter可以发现我们的刷新率只有23.9FPS!这是为什么呢?
对比不渲染图片的情况下:
我们可以打开performance来记录以下当前程序中比较耗时的操作,具体如下:
通过上图我们可以很清晰的看到,在一帧中,我们只调用了draw这个函数,在这个函数中花费时间最多的就是texImage2D这个函数了。为什么只渲染视频的时候texImage2D只需要0.1ms,这里却需要这么久? 我们仔细观察发现,再渲染有图片的情况时,texImage2D中还多了一个函数Image Decode,这又是干什么用的呢? 顾名思义,这就是将图片格式解码为裸RGBA数据。而且图片越大越清晰,花费的时间也就越多。 那么,如果我们传入GPU中的就是解码好的RGBA数据,效果会不会好很多呢?
通过查询资料,我发现createImageBitmap这样的一个API,可以将img标签解码为ImageBitmap也就是图片解码后的数据了,这样我们试试
createImageBitmap(image).then(bitmap => {
image = bitmap;
})
因为我的图片是静态的,它不会像视频那么每帧都需要更新,我能不能只往GPU中传递一次然后保存在GPU中呢? 答案肯定是能的,我们只需要创建多个Texture对象,分别保存不同的纹理,然后在使用的时候绑定它们即可。
gl.bindTexture这个API来选择绑定相应的纹理即可。我们试试:
let imgTexture = util.createTexture(gl);
image.onload = function () {
createImageBitmap(this).then(img => {
image = img;
});
// 先绑定,再传递纹理
gl.bindTexture(gl.TEXTURE_2D, imgTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
}
gl.bindTexture(gl.TEXTURE_2D, texture);
// 由于视频每一帧都需要更新,所以每帧都需要调用texImage2D
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// 这里直接绑定之前已经设好的纹理
gl.bindTexture(gl.TEXTURE_2D, imgTexture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
记住静态图片只需要往GPU中传递一次即可。使用GPU来保存图片纹理,不必每次都调用texImage2D。
多层级图像绘制问题
在canvas2D的最佳实践中有这样一条:
如果需要绘制多个不同层级的对象,建议使用多个canvas分别进行绘制,而不要将其都绘制在一张canvas上。
然而这条canvas2D的最佳实践并不适用于WebGL 我们可以运行以下代码
for (let i = 0; i < 50; i++) {
let canvas = document.createElement('canvas');
let context = canvas.getContext('webgl');
console.log(context);
}
可以看到控制台中有这样一行报错:
WARNING: Too many active WebGL contexts. Oldest context will be lost.
有太多激活的WebGL上下文,最老的上下文将会被丢弃
这就意味着在同一个页面中,WebGL同时存在的上下文是有数量限制的,当超过了这个数量限制时,之前最先创建的那个将失效。 经过测试,最多的同时激活的WebGL只有16个。那么我们就不能绘制超过16层的图像了吗?答案是否定的。WebGL中也有类似与离屏canvas的概念,只不过不是使用canvas来进行绘制了,而是使用的WebGL中内置的FrameBuffer,由于涉及篇幅过多,留于下次讲解。敬请期待。