Three.js使用局部纹理更新

时间:2022-03-11 05:51:26

THREE.js开发的应用运行在iphone5下发现有些时候会崩溃,跟了几天发现是因为Sprite太多频繁更新纹理占用显存导致的。通常解决纹理频繁更新问题就要用到one draw all方法,放到纹理上就是把所有纹理图片生成一张大图片的方式。

一、阻止纹理重复上传

我们需要一张大纹理,先将所有的内容绘制在大纹理上,需要显示局部纹理的时候通过纹理坐标控制去大纹理上取图像。那么这个时候问题来了,THREE.js内部实现方式是将Texture与图片、纹理坐标绑定,即使为所有的Texture对象设置同一张图片,THREE.js仍然会将每个Texture中的图片上传给GPU。每次上传一张大纹理严重阻塞UI渲染进程。

Three.js使用局部纹理更新

Three.js使用局部纹理更新

首先要解决的是让这张大纹理值上传一次。

这个问题需要我们对THREE.js源码进行深入了解,可以看到setTexture2D函数中有一个properties变量,这个变量是一个WebGLProperties类型的变量,而该类型存储各种东西:Texture、Material、RenderTarget、Object的buffers等。我们继续深入该类的源码,发现get方法会根据对象的uuid来获取相关WebGL属性,比如gl.createTexture、gl.createBuffer创建的各种缓冲区。

Three.js使用局部纹理更新

对应Texture得到的webgl属性如下,其中__webglTexture就是对应的纹理图片创建的缓冲区对象。

Three.js使用局部纹理更新

那么我们可以来一个取巧的方法,将所有纹理的的uuid都设置唯一,那么THREE.js只会对第一个Texture的纹理进行上传,后面的texture对象取到的都是第一个的properties,这样就能避免纹理重复上传。

Three.js使用局部纹理更新

二、建立纹理索引

我们需要自己维护一套索引关系,通过这套索引关系得到每个贴图在大纹理中纹理坐标。这里要为每一个poi记录它的起始位置和区域范围,其中要用到canvasContext.measureText来测量文本的宽度,文本高度可以直接根据fontSize取得。

Three.js使用局部纹理更新

同时索引建立完毕后,需要计算每个poi区域在全局纹理中的纹理坐标范围:

Three.js使用局部纹理更新

要注意的是,这里纹理坐标的原点在左下方,有时候原点在左上方。建立索引代码如下

Three.js使用局部纹理更新

三、局部更新

上述方案虽然能够避免频繁上传纹理,但是需要每次将需要绘制的内容准备好,当有内容需要更新时,还是需要重新上传整个全局纹理,反而使得性能下降巨大。经过查阅资料后发现webgl中有一种局部纹理更新技术,简单来说先在内存中开辟一块的纹理区域,将所有内容绘制在这张全局纹理中,每次有更新时,只需要更新它的一个局部区域即可。

但是这里要解决的问题是THREE.js并没有提供局部纹理更新的方式,也没有相应的自定义接口,那么这时候就需要我们自己来处理了。

这里自定义一个Texture的子类

Three.js使用局部纹理更新

开辟一块内存区域

Three.js使用局部纹理更新

在需要的时候动态更新局部纹理,其中src这里是ImageData对象

Three.js使用局部纹理更新

具体代码可以参考这里,我这里也是基于它来定制的。

https://github.com/spite/THREE.UpdatableTexture

原文作者通过更改THREE.js源码的方式实现,而我是直接把下面这个函数拷贝到这个子类中

Three.js使用局部纹理更新

四、高清屏的大坑

现在我们的方案是,先在gpu中开辟一块全局纹理区域,然后绘制时将poi绘制到一张与全局纹理同样大小的canvas上,然后从canvas中调用createImageData来获取像素,将像素局部更新到gpu中。那么在pc上我们得到的结果很完美。

Three.js使用局部纹理更新

然而放到移动端上后,我们得到的结果是:

Three.js使用局部纹理更新

Three.js使用局部纹理更新

TMMD中间那块哪去了!找了大半天发现问题出现在高清屏上,挡在高清屏上绘制canvas上时,我们通常会做一些高清处理,比如四像素绘制一像素。

我们做高清处理的方式是利用radio*radio设备像素绘制一css像素,看起来是css像素的大小,但实际在浏览器内部,看起来css上一像素实际在canvas里的像素是radio * radio(radio代表window.devicePixelRatio)

Three.js使用局部纹理更新

但实际上在浏览器内部绘制canvas图像的单位是设备像素。那么如果我们还以上面的rectW、rectH来获取像素的话,我们得到的这部分像素并不是这个poi真正占有的像素数目。

Three.js使用局部纹理更新

所以,问题就来了我们需要在gpu开辟的全局纹理的单位跟canvas中获取像素的单位要保持一致,我们统一使用设备像素。

Three.js使用局部纹理更新

我们对canvas也不用使用style来设置样式宽高了。

Three.js使用局部纹理更新

那么获取poi图像的真正像素范围时:

Three.js使用局部纹理更新

所以利用getImageData取像素时候,就要小心取到真正的像素区域,(startX * radio,startY * radio)- (poiRectW * radio, poiRectH * radio);否则某些像素就会被丢弃掉,这部分像素才是浏览器真正使用的设备像素。

现在在移动设备上能够获取正确的高清label啦!

Three.js使用局部纹理更新

五、局部更新引起的新问题

当全局纹理被占满时候,在继续绘制poi,这时候新的poi区域需要更新到gpu中,那么也就带来了新的问题,在gpu中的纹理还保持着之前的像素,而新的poi会覆盖这部分区域,但有时候往往会与之前的文字叠加起来,效果如下:

Three.js使用局部纹理更新

可以看到新更新的poi,在计算纹理坐标时候,有一部分像素包含了其他poi的像素。这个问题是因为新poi的区域刚好叠在了先前poi的边界上,那么我们只要给新的poi加一点buffer,这个buffer是白素透明区域,buffer会把之前的poi像素覆盖掉,而我们计算纹理坐标时,只取poi的边界,那么就可以解决这个问题。

Three.js使用局部纹理更新

那么首先绘制的时候就要保留buffer

Three.js使用局部纹理更新

上传的时候使用buffer

Three.js使用局部纹理更新

计算纹理坐标时,排除buffer

Three.js使用局部纹理更新

六、局部更新带来的性能为

根据目前的结果,局部更新能后解决crash的问题,但是带来了严重的性能开销,与同事应用局部更新提升性能的结果相反。这个问题还要继续跟踪。