THREE.js开发的应用运行在iphone5下发现有些时候会崩溃,跟了几天发现是由于Sprite太多频繁更新纹理占用显存致使的。一般解决纹理频繁更新问题就要用到one draw all方法,放到纹理上就是把全部纹理图片生成一张大图片的方式css
咱们须要一张大纹理,先将全部的内容绘制在大纹理上,须要显示局部纹理的时候经过纹理坐标控制去大纹理上取图像。那么这个时候问题来了,THREE.js内部实现方式是将Texture与图片、纹理坐标绑定,即便为全部的Texture对象设置同一张图片,THREE.js仍然会将每一个Texture中的图片上传给GPU。每次上传一张大纹理严重阻塞UI渲染进程。html
首先要解决的是让这张大纹理值上传一次。ios
这个问题须要咱们对THREE.js源码进行深刻了解,能够看到setTexture2D函数中有一个properties变量,这个变量是一个WebGLProperties类型的变量,而该类型存储各类东西:Texture、Material、RenderTarget、Object的buffers等。咱们继续深刻该类的源码,发现get方法会根据对象的uuid来获取相关WebGL属性,好比gl.createTexture、gl.createBuffer建立的各类缓冲区。git
对应Texture获得的webgl属性以下,其中__webglTexture就是对应的纹理图片建立的缓冲区对象。github
那么咱们能够来一个取巧的方法,将全部纹理的的uuid都设置惟一,那么THREE.js只会对第一个Texture的纹理进行上传,后面的texture对象取到的都是第一个的properties,这样就能避免纹理重复上传。web
咱们须要本身维护一套索引关系,经过这套索引关系获得每一个贴图在大纹理中纹理坐标。这里要为每个poi记录它的起始位置和区域范围,其中要用到canvasContext.measureText来测量文本的宽度,文本高度能够直接根据fontSize取得。canvas
同时索引创建完毕后,须要计算每一个poi区域在全局纹理中的纹理坐标范围:数组
要注意的是,这里纹理坐标的原点在左下方,有时候原点在左上方。创建索引代码以下浏览器
上述方案虽然可以避免频繁上传纹理,可是须要每次将须要绘制的内容准备好,当有内容须要更新时,仍是须要从新上传整个全局纹理,反而使得性能降低巨大。通过查阅资料后发现webgl中有一种局部纹理更新技术,简单来讲先在内存中开辟一块的纹理区域,将全部内容绘制在这张全局纹理中,每次有更新时,只须要更新它的一个局部区域便可。
可是这里要解决的问题是THREE.js并无提供局部纹理更新的方式,也没有相应的自定义接口,那么这时候就须要咱们本身来处理了。
这里自定义一个Texture的子类缓存
开辟一块内存区域
在须要的时候动态更新局部纹理,其中src这里是ImageData对象
具体代码能够参考这里,我这里也是基于它来定制的。
https://github.com/spite/THREE.UpdatableTexture
原文做者经过更改THREE.js源码的方式实现,而我是直接把下面这个函数拷贝到这个子类中
如今咱们的方案是,先在gpu中开辟一块全局纹理区域,而后绘制时将poi绘制到一张与全局纹理一样大小的canvas上,而后从canvas中调用createImageData来获取像素,将像素局部更新到gpu中。那么在pc上咱们获得的结果很完美。
然而放到移动端上后,咱们获得的结果是:
TMMD中间那块哪去了!找了大半天发现问题出如今高清屏上,挡在高清屏上绘制canvas上时,咱们一般会作一些高清处理,好比四像素绘制一像素。
咱们作高清处理的方式是利用radio*radio设备像素绘制一css像素,看起来是css像素的大小,但实际在浏览器内部,看起来css上一像素实际在canvas里的像素是radio * radio(radio表明window.devicePixelRatio)
但实际上在浏览器内部绘制canvas图像的单位是设备像素。那么若是咱们还以上面的rectW、rectH来获取像素的话,咱们获得的这部分像素并非这个poi真正占有的像素数目。
因此,问题就来了咱们须要在gpu开辟的全局纹理的单位跟canvas中获取像素的单位要保持一致,咱们统一使用设备像素。
咱们对canvas也不用使用style来设置样式宽高了。
那么获取poi图像的真正像素范围时:
因此利用getImageData取像素时候,就要当心取到真正的像素区域,(startX * radio,startY * radio)- (poiRectW * radio, poiRectH * radio);不然某些像素就会被丢弃掉,这部分像素才是浏览器真正使用的设备像素。
如今在移动设备上可以获取正确的高清label啦!
当全局纹理被占满时候,在继续绘制poi,这时候新的poi区域须要更新到gpu中,那么也就带来了新的问题,在gpu中的纹理还保持着以前的像素,而新的poi会覆盖这部分区域,但有时候每每会与以前的文字叠加起来,效果以下:
能够看到新更新的poi,在计算纹理坐标时候,有一部分像素包含了其余poi的像素。这个问题是由于新poi的区域恰好叠在了先前poi的边界上,那么咱们只要给新的poi加一点buffer,这个buffer是白素透明区域,buffer会把以前的poi像素覆盖掉,而咱们计算纹理坐标时,只取poi的边界,那么就能够解决这个问题。
那么首先绘制的时候就要保留buffer
上传的时候使用buffer
计算纹理坐标时,排除buffer
根据目前的结果,局部更新能后解决crash的问题,可是带来了严重的性能开销,与同事应用局部更新提高性能的结果相反。这个问题还要继续跟踪。
目前发现问题是由于使用了getImageData来获取数据,而后传递到gpu中,非ios设备用这种方式有时候getImageData的开销特别大,而ios设备相对好一些。
测试发现非ios设备直接上传一张大纹理的效果反而比getImageData这种方式更好。可是依然不如以前上传多个canvas的性能。而在iphone5的测试机和iphone6的机器上性能比以前直接上传多个canvas的方式好一些,且没有崩溃问题。可是在岳阳的iphone6 plus 16g内存的手机上发现用具局部纹理更新性能不好,并且常常崩溃。
后来发现缘由是由于,虽然getImageData在IOS上性能好过非IOS设备,但性能开销仍然比较大,因此当场景中POI不少时,仍然会引发主线程卡顿,甚至计算太密集引发浏览器崩溃。其中层尝试使用cesium方式,每一个poi建立新的canvas,将canvas进行局部上传,本觉得这种方式不须要getImageData会更快一些,然而实践发现每次建立canvas设置参数的过程更耗时。
最终的方案是仍然使用getImageData,可是将getImageData的过程分块处理,每50ms处理一次,分块放到场景中,这样就解决密集计算引发的崩溃问题,虽然增长了控制成本,可是可以有效解决IOS崩溃问题。有趣的是在安卓上getImageData方式开销很大,即便分块也不适合,并且安卓用一张大纹理的方式来处理,会发现不少POI绘制效果很差。
最终方案是,IOS使用getImageData局部纹理+分块加载方式绘制POI。安卓使用POI独立建立canvas+全量加载方式。(安卓不适用分块加载,是为了尽快把全部POI呈现给用户)
这个问题自始至终困扰我很久一直没找到黑边的缘由;
将原始的canvas导出后发现这是由于原始的canvas就有一层边界
曾经怀疑是minFilter的设置不对在pc端纹理使用NEARESTFilter方式取值发现的确可以消除黑边,然而移动端仍然会出现黑边,最后使用颜色混合公式解决问题。
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
在Three.js中须要设置SpriteMaterial的blending为CustomBlending
可是使用上述方式一样引来新问题,设计反映poi的icon四周被裁切掉,
看着没问题是吧,设计同窗截了图以后放大了20倍。。。。。
刚开始我确实觉得这是webgl渲染问题,后来仔细考虑了下这外圈白色的由来(遇到问题仍是得静下心分析)。
缘由是设置了blendFunc(SrcAlphaFactor,OneMinusSrcAlphaFactor)致使有些icon周围的像素alpha比较低
颜色混合后增长了target的颜色份量,致使最终这些区域的颜色范围接近255,因此泛白。从而把原来图片四周有切边的问题充分暴露出
解决方法是设置alphaTest,若是原始纹理的alpha小于这个值则直接discard。最终获得的效果是:
前面由于sprite的旋转中心只能放在sprite纹理区域的中心因此,上面作了不少冗余纹理,有不少空白区域,目前改造了Sprite加了pivot能够动态改变选中中心点,改变后IOS下纹理的使用率提高了60%,安卓下由于是单个纹理上传因此,须要保证纹理的大小是2的n次方,纹理的浪费率下降了50%
上述问题虽然解决了崩溃问题,可是实际使用中每一个poi都要getImageData和texSubImage2D这个方法,形成单个poi耗时基本在25ms(iphone5 8.4.4);虽然上面使用setTimeout 50ms分块方式上传,可是若是poi过多好比1000多的停车场,这样会致使停车场数据须要50s才能彻底显示出来。此次优化的方案是等待全部poi图片拿到后,绘制全部的poi把画布调用一次getImageData和一次texSubImage2D上传到gpu,同时下次更新时,只会增量一次性上传更新。
原来是在每一级别缩放时把全部的poi都生成好,如今的作法是只生成视锥体中能看获得的poi,而后在每次OrbitControl出发change事件时根据视锥体判断poi,作去重后增量更新
目前仍是有些问题,有时候会碰到视锥体中的poi不多,多是判断问题,后续会加入空间索引,根据索引和视锥体结合起来作增量更新
后续使用发如今停车场这种大数据的poi所有加载到地图下,使用这种方式每次都要作去重处理,性能开销很大,处理方式是使用{}作hash代替数组includes方法,结果发现性能提示很大,原来3600个节点每次去重处理在iphone 16g 10.3.3上性能基本在28帧每秒,通过优化后数据帧率达到50+(主流iPhone7fps60);iphone5 16g 8.4.1 性能在24左右优化后帧率在44+,安卓华为荣耀9优化前25帧,优化后 40+
安卓之因此不适用IOS的绘制方式,是由于这种在安卓上的绘制效果不理想,被设计挑战
安卓后面也作了一些优化,以前安卓是每次都会从新建立canvas并上传至gpu纹理中,致使使用视景体增量更新poi时,性能有所降低,后来每一层中的poi都根据icon、文字组成key缓存起来,而且缓存纹理,不但阻止canvas的重复建立,还阻止canvas重复上传至gpu纹理(three中使用同一uuid),使用该方案荣耀9的fps达到50+
该方式还有待尝试
https://webglfundamentals.org/webgl/lessons/webgl-text-glyphs.html
因为要作poi渐变出现效果,可是由于以前处理黑边问题用的是颜色混合的方式,因此当动态改变透明度时,受颜色混合影响每每是文字颜色先消失,剩下透明度部分还存在显示先过不好。因此要实现渐变效果,不能使用颜色混合方法,但不适用颜色混合就会有黑边问题,因此要从源头上解决黑边问题。(看到最后会发现有残影)
那么思考黑边究竟是怎么产生的,这与webgl中纹理插值的颜色有关,有的设备像素取纹理时有不一样的方案,但通常状况下纹理像素和设备像素都不是一一对应,因此有插值取值问题。
这是正常状况下利用canvas绘图时背景颜色不设置,那么能够看到咱们绘制出来的canvas的确有一层奇怪的黑边。当设备取到纹理中这些边界时就会产生黑边。那么就要思考怎么不让它取到这层黑边,这个问题想了很久曾经试过用opacity过滤,发现不能解决问题。
有一天忽然想到若是canvas背景为有颜色,每一个设备像素都能取到颜色,那么就不会有这个问题。因此咱们可否经过改一下canvas的背景颜色同时有经过透明度过滤掉不合格的像素?最终发现这个问题还真能够。
首先在绘制时将canvas背景设置为白色,可是有很低的透明度
这时候canvas绘制出来的效果是
能够看到已经没有黑边了,那么这时候设备像素永远不会取到黑色边界,也就完全解决了黑边问题。 那么就能够利用tween来作动画了