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來作動畫了