將 gif 貼到 cesium 三維球上

前言

最近做項目的過程中,碰到一些不太尋常的需求——將 gif 圖貼到三維球上跨算。

首先我們分析下這一需求爆土,朝 cesium 三維球上貼圖并非難事,很多方式都能做到诸蚕。

采用多邊形步势、方形、甚至于 SingleTileImageryProvider api背犯,都能做到坏瘩。

但是問題最麻煩的地方在于,不僅僅要貼圖片漠魏,而是需要貼 gif 動圖倔矾。

如果不是動圖,而是一系列靜態(tài)圖片的話蛉幸,實現(xiàn)起來的難度反而小一些破讨。

無非是,設(shè)置一些定時器奕纫,定時將當(dāng)前加載的圖片切換為下一張。筆者之前烫沙,就做過這種輪播效果匹层。


test.gif

但是現(xiàn)在,問題的難點在于锌蓄,如何針對一張 gif 也實現(xiàn)這種播放的效果呢升筏?

一些思考

稍微梳理一下思路,便不難得出瘸爽,我們需要考慮以下幾點問題:

  1. 瀏覽器是否提供現(xiàn)成的api您访,直接讓 canvas 支持 gif 動圖的加載。
  2. 如果 canvas 不支持動圖剪决,那么前端是否有現(xiàn)成的工具灵汪,可以將 gif 里的每一幀拆分出來檀训,拆分出每一幀后,我們就可以像上面的做法享言,控制這些幀來實現(xiàn)輪播的效果了峻凫。
  3. 如果找不到現(xiàn)成的可以拆分 gif 幀的工具,那么就只有采用將 gif 處理成一系列的圖片的方式來實現(xiàn)了览露。

按照這樣的思路荧琼,我們來一條條的尋找解決方案。

第一條差牛,直接可以從原理上否決掉命锄,目前沒有任何一個瀏覽器會支持該功能。原因偏化,其實也很容易能想明白累舷。canvas 其實就是一個畫布,它被設(shè)計出來就是干類似于繪畫的工作夹孔。你要是想要在畫布上實現(xiàn)動畫效果被盈,就只有通過改變每一幀畫布上要繪制的內(nèi)容,來實現(xiàn)搭伤。

canvas 的 api 只提供較為底層只怎,較為基礎(chǔ)的繪畫功能,繪制點線面等等怜俐,webgl 也不提供動圖的支持身堡,因為無論是 canvas 還是 webgl,通俗意義上講拍鲤,都是一張畫布贴谎,它實質(zhì)是不變的,想要變化季稳,則需要人為的去控制擅这。

既然第一條思路行不通,那么第二條思路景鼠,可以解決我們的問題么仲翎?

著手解決問題

帶著這個思路,筆者在網(wǎng)上找尋了一番铛漓,果然找到了一個有力的工具:https://themadcreator.github.io/gifler/

image.png

它可以支持將 gif 解析成幀溯香,并且還封裝了一些動畫接口,方便供我們調(diào)用浓恶。

默認(rèn)情況下玫坛,直接調(diào)用 animate 方法即可為我們在目標(biāo)的 canvas 上繪制出該 gif 動畫。

https://codepen.io/wuzhiqin/pen/vYZabwy

當(dāng)然包晰,還有另外的使用方式湿镀,通過調(diào)用 frame api 或者 get api炕吸。

下面這個 demo,是使用 frame api 調(diào)用的方式肠骆。

https://codepen.io/wuzhiqin/pen/YzQjgyZ

可以看出來算途,兩種使用方式,大同小異蚀腿,結(jié)果也并無差別嘴瓤,唯一不同的是,第二種方式莉钙,方便我們?nèi)タ刂泼恳粠瑪?shù)據(jù)廓脆。

那么怎么將該工具應(yīng)用于我們 cesium 貼圖中去,實現(xiàn)動畫效果呢磁玉?

首先停忿,結(jié)合 gifler 和 cesium,不難寫出如下代碼

let canvas = document.createElement("canvas");
let url =
  "https://media.giphy.com/media/VbEq7lhC0gVMFUX819/giphy-downsized.gif?cid=ecf05e471i9fq42unyxtjoci88jd019z2aana25ytggjay33&rid=giphy-downsized.gif&ct=g";
let gifImageLayer;
let rectangle = Cesium.Rectangle.fromDegrees(...[92.07, 27.67, 118.66, 39.45]);

function onDrawFrame(ctx, frame) {
  ctx.canvas.width = frame.width;
  ctx.canvas.height = frame.height;

  ctx.drawImage(frame.buffer, 0, 0);

  const provider = new Cesium.SingleTileImageryProvider({
    url: canvas.toDataURL(),
    rectangle
  });
  gifImageLayer = viewer.imageryLayers.addImageryProvider(provider);
}

gifler(url).frames(canvas, onDrawFrame);

基本邏輯就是蚊伞,在動圖每一幀調(diào)用的時候席赂,都將像素點繪制在 canvas 上,然后將 canvas 導(dǎo)出成圖片时迫,然后再調(diào)用 cesium 的 SingleTileImageryProvider api颅停,將該幀以圖層的方式,貼到球面上掠拳。

最終呈現(xiàn)出來的效果是這樣的:


cesium上貼gif.gif

雖然看起來還不錯癞揉,但是這段代碼,實質(zhì)上是有一些問題的溺欧。

比如喊熟,頻繁的添加新的 layer,卻沒有處理之前重復(fù)的 layer姐刁,短期內(nèi)好像看不出大問題芥牌,但是當(dāng)頁面運行時間長了,會占用大量的內(nèi)存龙填,造成頁面內(nèi)存泄漏胳泉。

那有沒有改進(jìn)的方案呢?

世界上本沒有路岩遗,走的人多了也便成了路。

對于解決問題的方案凤瘦,同樣如此一般宿礁。

方案當(dāng)然有很多,但是唯有不斷嘗試蔬芥,才能找到問題的最佳解決方案梆靖。

既然一直增加不行控汉,那么,為了防止內(nèi)存泄漏返吻,每次增加之前姑子,將上一個刪除不就行了么?

function onDrawFrame(ctx, frame) {
    // 不斷地將之前添加的 layer 從 imageryLayers 中刪除掉
  viewer.imageryLayers.remove(gifImageLayer, true);
  
  ctx.canvas.width = frame.width;
  ctx.canvas.height = frame.height;

  ctx.drawImage(frame.buffer, 0, 0);

  const provider = new Cesium.SingleTileImageryProvider({
    url: canvas.toDataURL(),
    rectangle
  });
  gifImageLayer = viewer.imageryLayers.addImageryProvider(provider);
}

如上所示测僵,改改我們的代碼街佑,看看運行以后,會出現(xiàn)什么結(jié)果:


cesium上貼gif2.gif

結(jié)果發(fā)現(xiàn)捍靠,這個效果完全不行沐旨,因為切換幀的時候,沒有過渡榨婆,會導(dǎo)致出現(xiàn)閃屏的效果磁携。

既然先移除再增加不行,那么先增加再移除是否可行呢良风?

按照思路谊迄,再改改我們的代碼:

function onDrawFrame(ctx, frame) {
  ctx.canvas.width = frame.width;
  ctx.canvas.height = frame.height;

  ctx.drawImage(frame.buffer, 0, 0);

  const provider = new Cesium.SingleTileImageryProvider({
    url: canvas.toDataURL(),
    rectangle
  });
  let layer = viewer.imageryLayers.addImageryProvider(provider);

  // 不斷地將之前添加的 layer 從 imageryLayers 中刪除掉
  viewer.imageryLayers.remove(gifImageLayer, true);
  gifImageLayer = layer;
}

沒想到,再次運行我們的代碼烟央,結(jié)果比之前反而閃爍的更嚴(yán)重了:


cesium上貼gif3.gif

不要氣餒统诺,按照道理來說,這個思路并沒有問題吊档,問題篙议,應(yīng)該出現(xiàn)在,增加上一個圖層和刪除下一個圖層這兩步操作怠硼,應(yīng)該并不是同步操作鬼贱,而是異步的。

對于這個問題香璃,不要著急这难,我們也有好的解決方案,每次增加一幀后,不再刪除上一幀了,而是改成梁肿,刪除倒數(shù)第二幀富岳、倒數(shù)第三幀......

一個個試下去,不愁試不出比較理想的方案脏毯。

實際在操作的過程中,你會發(fā)現(xiàn),其實剪个,在每次刪除倒數(shù)第二幀的時候,過渡就已經(jīng)很自然了版确,但是為了效果更好扣囊,我們每次刪除倒數(shù)第三幀:

function onDrawFrame(ctx, frame) {
  ctx.canvas.width = frame.width;
  ctx.canvas.height = frame.height;

  ctx.drawImage(frame.buffer, 0, 0);

  const provider = new Cesium.SingleTileImageryProvider({
    url: canvas.toDataURL(),
    rectangle
  });
  let layer = viewer.imageryLayers.addImageryProvider(provider);

  // 不斷地將之前添加的 layer 從 imageryLayers 中刪除掉
  viewer.imageryLayers.remove(gifImageLayer3, true);
  gifImageLayer3 = gifImageLayer2;
  gifImageLayer2 = gifImageLayer;
  gifImageLayer = layer;
}

改進(jìn)以后乎折,最終的效果是這樣的:


cesium上貼gif4.gif

可以看出來,效果已經(jīng)很自然了侵歇。

下面是完整的 demo:
https://codepen.io/wuzhiqin/pen/xxrJBgV

精益求精

一般情況下骂澄,按照套路來說,童鞋們可能會覺得我這篇文章這就寫完了惕虑。

但是坟冲,其實細(xì)細(xì)的思索一番,還有一種方式枷遂,可以把代碼寫的更優(yōu)雅一些樱衷。

為什么要不斷的移除之前的圖層,再重復(fù)創(chuàng)建新的圖層酒唉,而不將之前的圖層緩存起來呢矩桂?

我們知道的是,js 里面不能手動釋放內(nèi)存痪伦,即使將對象設(shè)置為 null侄榴,它也會在之后的某次垃圾回收的時候才會被清除掉。如果加一層緩存网沾,就可以減少創(chuàng)建新的對象癞蚕,使我們的代碼性能更高效。

所以辉哥,我們可以再次修改下我們的代碼桦山,將每個新圖層做一個緩存:

function onDrawFrame(ctx, frame) {
  let { data_offset } = frame;

  // 如果有緩存,則直接將緩存的圖層頂?shù)阶钌厦娲椎环駝t恒水,將創(chuàng)建新的圖層,并加入緩存
  if (gifImageLayerList[data_offset]) {
    viewer.imageryLayers.raiseToTop(gifImageLayerList[data_offset]);
  } else {
    ctx.canvas.width = frame.width;
    ctx.canvas.height = frame.height;

    ctx.drawImage(frame.buffer, 0, 0);

    const provider = new Cesium.SingleTileImageryProvider({
      url: canvas.toDataURL(),
      rectangle
    });
    let layer = viewer.imageryLayers.addImageryProvider(provider);
    gifImageLayerList[data_offset] = layer;
  }
}

可以看到饲齐,最后的效果其實跟之前的一般無二钉凌,但是效率比之前肯定要高一些。
https://codepen.io/wuzhiqin/pen/oNwPboM

后記

既然我們采用第二種方式捂人,實現(xiàn)了我們想要的效果御雕,那么前面的第三點思考到這里,就不用再深入下去了滥搭。

雖然酸纲,看起來,這篇文章寫下來瑟匆,實現(xiàn)這個功能好像很輕松福青,但是事實上,在解決這個問題的過程中脓诡,還是碰到了一些特別需要思考的問題无午。

  1. gif 貼上去,動畫速度太快祝谚,怎么控制
  2. gif 圖片太大的時候宪迟,加載速度很慢,該怎么處理

第二個問題交惯,其實沒有太好的解決方案次泽,畢竟資源擺在那兒,靠前端不太好整出合理的解決方案席爽。

而第一個問題其實挺有意思的意荤,一開始,博主以為是 gifler 框架里面沒有處理時間間隔的問題只锻,于是想了很多方案玖像,比如將 onDrawFrame 改成 async/await 函數(shù),里面插入一個自己封裝的 sleep 函數(shù)齐饮,開始 sleep 的時候捐寥,暫停動畫,sleep 結(jié)束以后再恢復(fù)執(zhí)行動畫祖驱。這一過程握恳,就達(dá)到了將后面的進(jìn)程阻塞一段時間再切換 layer,似乎能達(dá)到比較理想的效果捺僻。

但是這個解決方案乡洼,在需要不斷切換 gif 圖片的時候,就失效了匕坯。切換的時候束昵,直接就沒有辦法阻塞住進(jìn)程,這個問題困擾了我很久醒颖,最后還是沒找到太好的解決方案妻怎。

就在我以為進(jìn)行不下去的時候,沒想到抓到了問題的癥結(jié)所在泞歉。

看了 gifler 工具的源碼逼侦,才發(fā)現(xiàn),gif 播放的太快腰耙,本質(zhì)上是 gif 圖片本身的問題榛丢。

可以看到,在源碼里面挺庞,作者其實想到了這個問題晰赞,做了細(xì)致的處理的。


image.png

所以,這個問題最方便的解決方案掖鱼,其實是然走,調(diào)整下 gif, 將 gif 每一幀播放的間隔時間拉長一些就行了戏挡。

當(dāng)然芍瑞,上面我采用的方案是使用 layer 的方式來解決的,其實用 polygon 或者 rectangle 也能實現(xiàn)褐墅。

亦或者推廣一下拆檬,不用在 cesium 里面,而用在其他 webgl 框架或者 canvas 框架中妥凳,采用上面的思路竟贯,都能實現(xiàn)這樣的效果。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逝钥,一起剝皮案震驚了整個濱河市屑那,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晌缘,老刑警劉巖齐莲,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異磷箕,居然都是意外死亡选酗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門岳枷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芒填,“玉大人,你說我怎么就攤上這事空繁〉钏ィ” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵盛泡,是天一觀的道長闷祥。 經(jīng)常有香客問我,道長傲诵,這世上最難降的妖魔是什么凯砍? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮拴竹,結(jié)果婚禮上悟衩,老公的妹妹穿的比我還像新娘。我一直安慰自己栓拜,他們只是感情好座泳,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布惠昔。 她就那樣靜靜地躺著,像睡著了一般挑势。 火紅的嫁衣襯著肌膚如雪镇防。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天薛耻,我揣著相機(jī)與錄音营罢,去河邊找鬼。 笑死饼齿,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蝙搔。 我是一名探鬼主播缕溉,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吃型!你這毒婦竟也來了证鸥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤勤晚,失蹤者是張志新(化名)和其女友劉穎枉层,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赐写,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鸟蜡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挺邀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揉忘。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖端铛,靈堂內(nèi)的尸體忽然破棺而出泣矛,到底是詐尸還是另有隱情,我是刑警寧澤禾蚕,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布您朽,位于F島的核電站,受9級特大地震影響换淆,放射性物質(zhì)發(fā)生泄漏哗总。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一产舞、第九天 我趴在偏房一處隱蔽的房頂上張望魂奥。 院中可真熱鬧,春花似錦易猫、人聲如沸耻煤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哈蝇。三九已至棺妓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間炮赦,已是汗流浹背怜跑。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留吠勘,地道東北人性芬。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像剧防,于是被迫代替她去往敵國和親植锉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容