阿里妹導讀:開啟盒馬app,相信你跟阿里妹一樣,很難抵抗各種美味的誘惑。顏值即正義,盒馬的圖片視訊技術逼真地還原了食物細節,並在短短數秒內呈現出食物的最佳效果。今天,我們請來阿里高階無線開發工程師萊寧,解密盒馬app裡那些“美味”視訊是如何生產的。
一、前言圖片合成視訊併產生類似PPT中每頁過渡特效的能力是目前很多短視訊軟體帶有的功能,比如抖音的影集。這個功能主要包括圖片合成視訊、轉場時間線定義和OpenGL特效等三個部分。
其中圖片轉視訊的流程直接決定了後面過渡特效的實現方案。這裡主要有兩種方案:
圖片預先合成視訊,中間不做處理,記錄每張圖片展示的時間戳位置,然後在相鄰圖片切換的時間段用OpenGL做畫面處理。圖片合成視訊的過程中,在畫面幀寫入時同時做特效處理。方案1每個流程都比較獨立,更方便實現,但是要重複處理兩次資料,一次合併一次加特效,耗時更長。
方案2的流程是相互穿插的,只需要處理一次資料,所以我們採用這個方案。
下面主要介紹下幾個重點流程,並以幾個簡單的轉場特效作為例子,演示具體效果。
二、圖片合成
1.方案圖片合成視訊有多種手段可以實現。下面談一下比較常見的幾種技術實現。
I.FFMPEG
定義輸出編碼格式和幀率,然後指定需要處理的圖片列表即可合成視訊。
II.MediaCodec
在使用Mediacodec進行視訊轉碼時,需要解碼和編碼兩個codec。解碼視訊後將原始幀資料按照時間戳順序寫入編碼器生成視訊。但是圖片本身就已經是幀資料,如果將圖片轉換成YUV資料,然後配合一個自定義的時鐘產生時間戳,不斷將資料寫入編碼器即可達到圖片轉視訊的效果。
III.MediaCodec&OpenGL
既然Mediacodec合成過程中已經有了處理圖片資料的流程,可以把這個步驟和特效生成結合起來,把圖片處理成特效序列幀後再按序寫入編碼器,就能一併生成轉場效果。
2.技術實現
首先需要定義一個時鐘,來控制圖片幀寫入的頻率和編碼器的時間戳,同時也決定了視訊最終的幀率。
這裡假設需要24fps的幀率,一秒就是1000ms,因此寫入的時間間隔是1000/24=42ms。也就是每隔42ms主動生成一幀資料,然後寫入編碼器。
時間戳需要是遞增的,從0開始,按照前面定義的間隔時間差deltaT,每寫入一次資料後就要將這個時間戳加deltaT,用作下一次寫入。
然後是設定一個EGL環境來呼叫OpenGL,在Android中一個OpenGl的執行環境是threadlocal的,所以在合成過程中需要一直保持在同一個執行緒中。Mediacodec的建構函式中有一個surface引數,在編碼器中是用作資料來源。在這個surface中輸入資料就能驅動編碼器生產視訊。通過這個surface用EGL獲取一個EGLSurface,就達到了OpenGL環境和視訊編碼器資料繫結的效果。
這裡不需要手動將圖片轉換為YUV資料,先把圖片解碼為bitmap,然後通過texImage2D上傳圖片紋理到GPU中即可。
最後就是根據圖片紋理的uv座標,根據外部時間戳來驅動紋理變化,實現特效。
三、轉場時間線
對於一個圖片列表,在合成過程中如何銜接前後序列圖片的展示和過渡時機,決定了最終的視訊效果。
假設有圖片合集{1,2,3,4},按序合成,可以有如下的時間線:
每個Stage是合成過程中的一個最小單元,首尾的兩個Stage最簡單,只是單純的顯示圖片。中間階段的Stage,包括了過渡過程中前後兩張圖片的展示和過渡動畫的時間戳定義。
假設每張圖片的展示時間為showT(ms),動畫的時間為animT(ms)。
相鄰Stage中同一張圖的靜態顯示時間的總和為一張圖的總顯示時間,則首尾兩個Stage的有效時長為showT/2,中間的過渡Stage有效時長為showT+animT。
其中過渡動畫的時間段又需要分為:
前序退場起始點enterStartT,前序動畫開始時間點。前序退場結束點enterEndT,前序動畫結束時間點。後序入場起始點exitStartT,後序動畫開始時間點。後序入場結束點exitEndT,後序動畫結束時間點。動畫時間線一般只定義為非淡入淡出外的其他特效使用。為了過渡的視覺連續性,前後序圖片的淡入和淡出是貫穿整個動畫時間的。考慮到序列的銜接性,退場完畢後會立刻入場,因此enterEndT=exitStartT。
四、OpenGL特效
1.基礎架構
按照前面時間線定義回撥介面,用於處理動畫引數:
定義幾個通用的片段著色器變數,輔助過渡動畫的處理:
前後序列的混合流程,根據動畫流程計算出的兩個紋理的UV座標混合顏色值:
解析圖片,先讀取Exif資訊獲取旋轉值,再將旋轉矩陣應用到bitmap上,保證上傳的紋理圖片與使用者在相簿中看到的旋轉角度是一致的:
在使用圖片之前,還要根據最終的視訊寬高調整OpenGL視窗尺寸。同時紋理的貼圖座標的起始(0,0)是在紋理座標系的左下角,而Android系統上canvas座標原點是在左上角,需要將圖片做一次y軸的翻轉,不然圖片上傳後是垂直映象。
上傳圖片紋理,並記錄紋理的handle:
載入第二張圖片時要開啟非0的其他紋理單元,過渡動畫需要同時操作兩個圖片紋理:
最後是實際繪製的部分,因為用到了透明度漸變,要手動開啟GL_BLEND功能,並注意切換正在操作的紋理:
2.平移覆蓋轉場
I.著色器實現
GLSL中的step函式定義如下,當x<edge是返回0,反之則返回1:
已知我們有前後兩張圖,將他們覆蓋展示。然後從一個方向逐漸修改這一條軸上的所掃過的畫素的intensity值,隱藏前圖,展示後圖。經過時鐘動畫驅動後就有了覆蓋轉場的效果。
再定義一個direction引數,控制掃描的方向,即可設定不同的轉場方向,有PPT翻頁的效果。
II.效果圖
3.畫素化轉場
I.著色器實現
首先是定義畫素塊的效果,我們需要畫素塊逐漸變大,到動畫中間值時再逐漸變小到消失。
通過對progress(0到1)取反向值1-progress,得到distFromEdges,可知這個值在progress從0到0.5時會從0到0.5,在0.5到1時會從0.5到0,即達到了我們需要的變大再變小的效果。
畫素塊就是一整個方格範圍內的畫素都是同一個顏色,視覺效果看起來就形成了明顯的畫素間隔。如果我們將一個方格範圍內的紋理座標都對映為同一個顏色,即實現了畫素塊的效果。
squareSizeFactor是影響畫素塊大小的一個引數值,設為50,即最大畫素塊為50畫素。
imageWidthFactor和imageHeightFactor是視窗高寬取倒數,即1/width和1/height。
通過dx * floor(uv.x / dx)和dy * floor(uv.y / dy)的兩次座標轉換,就把一個區間範圍內的紋理都對映為了同一個顏色。
II.效果圖
4.水波紋特效
I.數學原理
水波紋路的週期變化,實際就是三角函式的一個變種。目前業界最流行的簡易水波紋實現,Adrian的部落格中描述了基本的數學原理:
水波紋實際是Sombero函式的求值,也就是sinc函式的2D版本。
下圖的左邊是sin函式的影象,右邊是sinc函式的影象,可以看到明顯的水波紋特徵。
部落格中同時提供了一個WebGL版本的著色器實現,不過功能較簡單,只是做了效果驗證。
將其移植到OpenGLES中,並做引數調整,即可整合到圖片轉場特效中。
完整的水波紋片段著色器如下:
其中最關鍵的程式碼就是水波紋畫素座標的計算:
vTextureCoord + (curPosition/centerLength)*cos(centerLength*rippleAmplitude-rippleTime*rippleSpeed)*rippleOffset;
簡化一下即:vTextureCoord + A*cos(L*x - T*y)*rippleOffset,一個標準的餘弦函式。
vTextureCoord是當前紋理的歸一化座標(0,0)到(1,1)之間。
curPosition是(-1,-1)到(1,1)之間的當前畫素座標。
centerLength是當前點距離波紋中心的距離。
curPosition/centerLength即是線性代數中的單位向量,這個引數用來決定波紋推動的方向。
cos(centerLength*rippleAmplitude-rippleTime*rippleSpeed)通過一個外部時鐘rippleTime來驅動cos函式生成周期性的相位偏移。
rippleAmplitude是相位的擴大因子。
rippleSpeed調節函式的週期,即波紋傳遞速度。
最後將偏移值乘以一個最大偏移範圍rippleOffset(一般為0.03),限定單個畫素的偏移範圍,不然波紋會很不自然。
II.時間線動畫
設定顏色混合,在整個動畫過程中,圖1逐漸消失(1到0),圖2逐漸展現(0到1)。
設定畫布透明度,在起始時為1,逐漸變化到0.7,最後再逐漸回到1。
設定波紋的振幅,在起始時最大,過渡到動畫中間點到最小,最後逐漸變大到動畫結束。
設定波紋的速度,在起始時最大,過渡到動畫中間點到最小,最後逐漸變大到動畫結束。
設定波紋的畫素最大偏移值,在起始時最大,過渡到動畫中間點到最小,最後逐漸變大到動畫結束。
將本次動畫幀的引數更新到著色器:
其中GLClock是一個與mediacodec編碼時間戳繫結的外部時鐘,用於同步合成時間和動畫時間戳位置。
III.最終效果
圖片展示時長:3s
過渡動畫時長:1.5s
波紋中心為圖片中心點
5.隨機方格
I.噪聲函式
我們想實現的效果是前一個畫面上隨機出現很多方塊,每個方塊中展示下一張圖的畫面,當圖片上每一塊位置都形成方塊後就完成了畫面的轉換。
首先就需要解決隨機函式的問題。雖然Java上有很多現成的隨機函式,但是GLSL是個很底層的語言,基本上除了加減乘除其他的都需要自己想辦法。這個著色器裡用的rand函式是流傳已久幾乎找不到來源的一個實現,很有上古時期遊戲程式設計程式碼的風格,有魔法數,程式碼只要一行,證明要寫兩頁。
網上一個比較靠譜且簡潔的說明是StackOverflow上的,這個隨機函式實際是一個hash函式,對每一個相同的(x,y)輸入都會有相同的輸出。
II.著色器實現
首先將當前紋理座標乘以方格大小,用隨機函式轉換後獲取這個方格區域的隨機漸變值。
然後用smoothstep做一個厄米特插值,將漸變的intensity平滑化。
最後用這個intensity值mix前後影象序列。
III.效果圖