Android OpenGLES濾鏡開發之仿抖音靈魂出竅
前言
前幾篇寫的濾鏡效果比如美顏、大眼、貼紙效果都是在錄製視訊之前,這個靈魂出竅的效果是在錄製視訊之後,可以對視訊新增效果。
思路
可以觀察到靈魂出竅的效果,其實其主影象本沒有什麼變化,只是新增了一張進行縮放的紋理,跟主影象的alpha進行線性融合的。
怎麼去取靈魂呢,靈魂是跟著視訊所播放的內容不斷更新的,不可能一直只是同一個影象。所以這裡的思路是每X幀拷貝一幀作為靈魂,然後將靈魂按比例放大,最後將靈魂與主影象進行混合。
實現
片元著色器
因為視訊錄製出來的是YUV格式的資料,但是在OpenGL中是需要的RGB顏色,所以需要將YUV格式的資料轉化成RGB格式的,這裡需要將YUV格式的資料進行分離之後,再轉換。轉換是有公式的,用公式進行計算就好啦。
uniform sampler2D sampler_y; //yuv
uniform sampler2D sampler_u;
uniform sampler2D sampler_v;
//透明度
uniform float alpha;
void main(){
//4個float資料 y、u、v儲存在向量中的第一個
// 0.5是128資料歸一後的
float y = texture2D(sampler_y,aCoord).r;
float u = texture2D(sampler_u,aCoord).r - 0.5;
float v = texture2D (sampler_v,aCoord).r - 0.5;
// yuv轉rgb的公式
//R = Y + 1.402 (v-128)
//G = Y - 0.34414 (u - 128) - 0.71414 (v-128)
//B = Y + 1.772 (u- 128)
vec3 rgb;
//u - 128
//1、glsl中 不能直接將int與float進行計算
//2、rgba取值都是:0-1 (128是0-255 歸一化為0-1 128就是0.5)
rgb.r = y + 1.402 * v;
rgb.g = y - 0.34414 * u - 0.71414 * v;
rgb.b = y + 1.772 * u;
//rgba
gl_FragColor = vec4(rgb,alpha);
渲染主影象
首先就是獲取索引,然後建立Y、U、V三個紋理,分別進行傳值,然後先畫主影象,不進行任何的縮放平移。
//分離yuv資料,然後傳值
bodyImage.initData(yuv);
if(!bodyImage.hasImage()){
return;
}
GLES20.glUseProgram(mGLProgramId);
//不進行任何的縮放平移
Matrix.setIdentityM(matrix,0);
GLES20.glUniformMatrix4fv(vMatrix,1,false,matrix,0);
//主影象不透明
GLES20.glUniform1f(mAlpha,1);
//傳值
onDrawBody(bodyImage);
上面程式碼就是首先將主影象的YUV資料進行分離,分離YUV就不介紹了,比較簡單,然後設定setIdentityM設定一個單位矩陣,這個矩陣是沒有任何縮放平移效果的。這個方法的原始碼是
/** 最終得出的矩陣
* 1 0 0 0
* 0 1 0 0
* 0 0 1 0
* 0 0 0 1
*/
public static void setIdentityM(float[] sm, int smOffset) {
for (int i=0 ; i<16 ; i++) {
sm[smOffset + i] = 0;
}
for(int i = 0; i < 16; i += 5) {
sm[smOffset + i] = 1.0f;
}
}
這個矩陣需要個傳入的4個頂點座標進行運算,為什麼這樣的單位矩陣就是沒有任何效果的呢,可以來進行矩陣運算一下,輸入頂點座標為(1,1,0,0)的話,與矩陣運算之後的結果還是(1,1,0,0)
設定完需要的矩陣,YUV三個紋理,就需要將這些頂點,YUV紋理傳遞給著色器就可以了。這個裡就看一下Y資料的傳遞,UV資料傳遞也是一樣的。
//傳遞yuv資料
//啟用紋理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//繫結mTextures[0]紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,mTextures[0]);
// glTexImage2D方法是指定一個二維紋理影象 這裡就是將Y資料與mTextures[0]這個紋理相繫結
//第一個引數target 是指定紋理單元的目標紋理,二維就是GLES20.GL_TEXTURE_2D是有取值範圍的
//第二個引數是level,是指定詳細編號,0表示基本影象級別,n就是第n個縮圖
//第三個引數internalformat ,指定紋理的內部格式,必須是下列符號常量之一:GL_ALPHA,GL_LUMINANCE,GL_LUMINANCE_ALPHA,GL_RGB,GL_RGBA。因為我們這次傳遞的是YUV格式資料,所以需要給GL_LUMINANCE,意思是量度模型
//第四個引數width,紋理影象的寬,第五個就是紋理影象的高,第六個就是邊框的寬度border必須為0
//第七個引數format,指定紋理資料的格式,和internalformat是相匹配的
//第八個引數type,是指紋理資料的型別,這裡給的是無符號的byte型別
//第九個引數java.nio.Buffer pixels,畫素值,這個給的是Y資料,如果是RGA格式的,一般就是0畫素
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_LUMINANCE,
mOutputWidth,mOutputHeight,0,GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,bodyImage.getY());
GLES20.glUniform1i(mSamplerY,0);
渲染靈魂
因為靈魂出竅這個效果,靈魂是不斷的更新放大,所以我們這裡是採用了間隔X幀拷貝一幀作為靈魂。
interval++ ;
//沒有儲存靈魂或者 使用次數已經達到上限了
// 靈魂只能使用X次,因為需要不斷的更新靈魂
if (!soulImage.hasImage() || interval > mFps){
interval = 1;
// 記錄新靈魂
soulImage.initData(yuv);
}
if (!soulImage.hasImage()){
return;
}
//畫靈魂
//需要和主影象進行混合的,所以需要開啟混合模式
GLES20.glEnable(GLES20.GL_BLEND);
//這兩個引數之前也說過了,這次的源是靈魂,目標是主影象
//這次是主影象不變,也就是目標不變
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA,GLES20.GL_ONE);
//設定matrix為單位矩陣
Matrix.setIdentityM(matrix,0);
//設定縮放大小
//本次放大為 1+ 當前靈魂次數佔總次數 * 2的比例
//這個是自己設定的慢慢放大的效果
float scale = 1.0f + interval/(mFps * 2.f);
//矩陣的縮放函式,結果儲存在martix中
Matrix.scaleM(matrix,0,scale,scale,0);
GLES20.glUniformMatrix4fv(vMatrix,1,false,matrix,0);
GLES20.glUniform1f(mAlpha,0.1f + (mFps -interval) / 100.f);
吐槽
這樣就差不多可以實現了靈魂出竅的效果了,我本來是使用MediaCodec編解碼來進行錄製和播放的,但是錄製沒問題,播放的時候就有問題了,MediaCodec的相容性真滴太差了,它在個別機型上面播放的時候,可能相容不了,就會播放出黑白視訊,木有顏色~~所以這裡就不放效果圖了,改天我用FFmeg來解碼播放吧,還是FFmeg比較好用些。