乾貨 | 移動應用中使用OpenGL生成轉場特效

語言: CN / TW / HK

作者簡介

jzg,攜程資深前端開發工程師,專注Android開發;

zcc,攜程高階前端開發工程師,專注iOS開發。

一、前言

隨著移動端短視訊的火熱,音視訊編輯工具在做內容類APP上的地位舉足輕重。豐富的轉場方式可以給短視訊帶來更多炫酷的效果,從而更好地贏得使用者青睞。本議題主要包含了對OpenGL的簡單介紹及相關API使用,GLSL著色器語言的基本使用,以及如何通過編寫自定義的著色器程式來實現圖片的轉場效果。

二、為什麼使用OpenGL以及使用的難點

2.1 為什麼使用OpenGL

視訊的轉場效果離不開圖形的處理,移動裝置在處理3D圖形相關的計算時一般都會選擇使用GPU。相較於CPU,GPU在影象動畫處理時具有更高效的效能。移動裝置以android為例,GPU處理提供了兩套不同的API,分別是 Vulkan OpenGL ES 。其中 VulKan 只支援 Android 7.0 以上的裝置, OpenGL ES  則支援所有的 Android 版本,而iOS並沒有對vulkan的官方支援。同時  OpenGL ES  作為  OpenGL  的子集,針對手機、PDA 和遊戲主機等嵌入式裝置去除了 glBegin/glEnd,四邊形、多邊形等複雜圖元等許多非絕對必要的特性,消除它的冗餘功能,從而提供了更容易學習和易於在移動圖形硬體中實現的庫。

目前,在短視訊影象處理中,  OpenGL ES  憑藉良好的系統支援性和功能的高度精簡性,成為了最廣泛的 GPU 處理 API 之一。為了方便,本文中提到的  OpenGL  即表示  OpenGL ES

2.2 使用OpenGL處理視訊轉場的難點

使用 OpenGL 處理視訊轉場的難點是如何編寫轉場效果的著色器,關於這一點,我們可以參考開源的 GLTransitions 網站。該網站有很多開源的轉場效果,我們可以借鑑並學習,下文會有較為詳細的介紹。

三、OpenGL的基本介紹和轉場應用

3.1 OpenGL的基本介紹

OpenGL 是一種開放式的圖形庫,用於渲染2D、3D向量圖形的跨語言,跨平臺的應用程式程式設計介面。OpenGL 可以⽤來做什麼?

  • 視訊,圖形,圖⽚處理

  • 2D/3D 遊戲引擎開發

  • 科學視覺化

  • 醫學軟體開發

  • CAD(計算機輔助技術)

  • 虛擬實境(AR,VR)

  • AI ⼈⼯智慧

我們使用 OpenGL 來處理視訊轉場,就是上面提到的用 OpenGL 來對視訊、圖形、圖片進行處理。

3.1.1 OpenGL渲染流程

在使用OpenGL進行繪製時,我們主要關注的是 頂點著色器 片元著色器 頂點著色器 用來確定繪製圖形的頂點位置, 片元著色器 負責給圖形新增顏色。主要繪製流程如下圖:

渲染的流程有以下幾步:

1)頂點資料的輸入:

頂點資料用來為後面的頂點著色器等階段提供處理的資料。

2)頂點著色器:

頂點著色器主要功能是進行座標變換。

3)幾何著色器:

與頂點著色器不同,幾何著色器的輸入是完整的圖元(比如,點),輸出可以是一個或多個其他的圖元(比如,三角面),或者不輸出任何的圖元,幾何著色器是可選的。

4)圖元組裝、光柵化:

圖元組裝將輸入的頂點組裝成指定的圖元,經過圖元組裝以及螢幕對映階段後,我們將物體座標變換到了視窗座標,光柵化是個離散化的過程,將3D連續的物體轉化為離散螢幕畫素點的過程。

5)片元著色器(片段著色器):

片元著色器用來決定螢幕上畫素的最終顏色。

6)混合測試:

渲染的最後一個階段是測試混合階段。測試包括裁切測試、Alpha測試、模板測試和深度測試。沒有經過測試的片段會被丟棄,不需要進行混合階段,經過測試的片段會進入混合階段。

經過以上幾個步驟, OpenGL 就能將最終的圖形顯示到螢幕上。

OpenGL 繪製流程中,我們能夠編碼的就是 Vertex Shader (頂點著色器) 和  Fragment Shader (片元著色器)。這也是渲染過程中必備的2個著色器。

Vertex Shader 處理從客戶端輸入的資料、應用變換、進行其他的型別的數學運算來計算光照效果、位移、顏色值等。比如為了渲染共有3個頂點的三角形, Vertex Shader 將執行3次,也就是為了每個頂點執行一次。

圖中的3個頂點已經組合在一起,而三角形也已經逐個片段的進行了光柵化。每個片段通過執行 Fragment Shader 進行填充。 Fragment Shader 會輸出我們螢幕上看到的最終顏色值。

在繪製圖形的時候,我們會使用到 OpenGL 的多種狀態變數,例如當前的顏色,控制當前檢視和投影變換、直線和多邊形點畫模式、多邊形繪圖模式、畫素包裝約定、光照的位置和特徵以及被繪製物體的材料屬性等。可以設定它的各種狀態(或模式),然後讓這些狀態一直生效,直到再次修改它們。

以把當前顏色設定為白色、紅色或其他任何顏色,在此之後繪製的所有物體都將使用這種顏色,直到再次把當前顏色設定為其他顏色。許多表示模式的狀態變數可以用 glEnable() glDisable() 。所以我們說 OpenGL 是一個狀態機。

因為 OpenGL 在渲染處理過程中會順序執行一系列操作,就如流水線作業一樣,所以我們將 OpenGL 繪製的流程稱為渲染管線,包括固定管線和可程式設計管線。我們使用的是可程式設計管線,在可程式設計管線裡,頂點的位置、顏色、貼圖座標、貼圖傳進來之後,如何對資料進行改動,產生的片元如何生成結果,可以很自由地控制。

下面就簡單介紹一下管線和在可變程式設計管線中必不可少的GLSL(著色器語言)。

3.1.2 管線

管線:渲染管線可以理解為渲染流水線。指的是輸入需要渲染的3D物體的相關描述資訊資料(例:頂點座標、頂點顏色、頂點紋理等),經過渲染管線一系列的變化和渲染過程,輸出一幀最終的影象。簡單理解就是一堆原始圖形資料經過一個輸送管道,期間經過各種變化處理最終出現展示到螢幕的過程。管線又分為固定管線和可程式設計管線兩種。

固定管線:在渲染影象的過程,我們只能通過呼叫GLShaderManager類的固定管線效果實現一系列的著色器處理。

可程式設計管線:在渲染影象的過程,我們能夠使用自定義頂點著色器和片元著色器的去處理資料的過程。由於OpenGL的使用場景非常豐富,固定管線或者儲存著色器無法完成的任務,這時我們可以使用可程式設計管線去處理。

3.1.3 GLSL(OpenGL Shading Language)

OpenGL著色語言(OpenGL Shading Language)是用來在OpenGL中著色編碼的語言,也即開發人員寫的短小的自定義程式,他們是在GPU(Graphic Processor Unit圖形處理單元)上執行的,代替了固定的渲染管線的一部分,使渲染管線中不同層次具有可程式設計性。它可以得到當前OpenGL 中的狀態,GLSL內建變數進行傳遞。GLSL其使用C語言作為基礎高階著色語言,避免了使用匯編語言或硬體規格語言的複雜性。

GLSL的著色器程式碼分成2個部分:VertexShader(頂點著色器) 和 Fragment Shader(片元著色器)。

著色器Shader

著色器(Shader)是用來實現影象渲染的,用來替代固定渲染管線的可編輯程式。其中Vertex Shader(頂點著色器)主要負責頂點的幾何關係等的運算,Pixel Shader(畫素著色器)主要負責片源顏色等的計算。

頂點著色器VertexShader

頂點著色器是一個可程式設計的處理單元,一般用來處理圖形每個頂點變換(旋轉/平移/投影等)、光照、材質的應用與計算等頂點的相關操作。頂點著色器是逐頂點運算的程式,每個頂點資料都會執行一次。替代了原有固定管線的頂點變換、光照計算,採用GLSL進行開發 。我們可以根據自己的需求採用著色語言自行開發頂點變換、光照等功能,大大增加了程式的靈活性。

頂點著色器工作過程為將原始的頂點幾何資訊(頂點座標、顏色、紋理)及其他屬性傳送到頂點著色器中,經過自定義的頂點著色程式處理產生變化後的頂點位置資訊,將變化後的頂點位置資訊傳遞給後續圖元裝配階段,對應的頂點紋理、顏色等資訊則經光柵化後傳遞到片元著色器。

頂點著色器的輸入主要為待處理頂點相應的 attribute uniform 取樣器以及臨時變數 ,輸出主要為經過頂點著色器後生成的 varying 及一些 內建輸出變數

頂點著色器示例程式碼:

//頂點位置
attribute vec4 Position;
//紋理座標
attribute vec2 TextureCoord;
//紋理座標 用於接收和傳遞給片元著色器的紋理座標
varying vec2 varyTextureCoord;
void main() {
gl_Position = Position;
varyTextureCoord = TextureCoord;
}

元著色器FragmentShader

片元著色器是一個可程式設計的處理單元,一般用來處理圖形中每個畫素點顏色計算和填充、紋理的取樣等操作。片元著色器是逐畫素運算的程式,也就說每個畫素都會執行一次片元著色器。

片元著色器是替換了OpenGL固定渲染管線階段中紋理顏色求和、霧以及Alpha測試等階段,採用GLSL進行開發 ,我們可以根據自己的需求採用著色語言自行開發。

片元著色器示例程式碼:

//高精度
precision highp float;
//用於接收頂點著色器的紋理座標
varying vec2 varyTextureCoord;
//圖片紋理
uniform sampler2D Texture;
//圖片紋理
uniform sampler2D Texture2;
const vec2 direction = vec2(0.0, 1.0);
void main(){
vec2 p = varyTextureCoord.xy/vec2(1.0).xy;
vec4 color = mix(texture2D(Texture, varyTextureCoord), texture2D(Texture2, varyTextureCoord), step(1.0-p.y,progress));
gl_FragColor = vec4(color);
}

3.1.4 三種向OpenGL著⾊器傳遞資料的⽅法

上面的頂點著色器和片元著色器裡出現了attribute,varying,uniform等型別定義,下面就簡單介紹一下這三種類型。

attribute

attribute :attribute變數是隻能在頂點著色器中使用的變數,一般用attribute變數來表示一些頂點的資料,如:頂點座標,法線,紋理座標,頂點顏色等。

uniform

uniform :uniform變數是外部application程式傳遞給著色器的變數,uniform變數就像是C語言裡面的常量,也就是說著色器只能用而不能修改uniform變數。

varying

varying :從頂點著色器傳遞到片元著色器的量,如用於傳遞到片元著色器中的頂點顏色,可以使用varying(易變變數)。

注意點: Attributes 不能夠直接傳遞給 Fragment Shader ,如果需要傳遞給 Fragment Shader ,則需要通過 Vertex Shader 間接的傳遞過去。而  Unifrom Texture Data 可以直接傳遞給 Vertex Shader Fragment Shader, 具體怎麼傳遞,依需求而定。

3.1.5 如何使用OpenGL來繪製一張圖片

上面介紹了頂點著色器和片元著色器,以及如何向 OpenGL 程式傳遞資料的方法。

現在我們就利用剛剛介紹的一些知識點,通過OpenGL程式將圖片繪製到螢幕上,這也是製作圖片輪播轉場特效的前提。圖片的繪製對於 OpenGL 來說就是紋理的繪製,這裡只為了展示效果,不使用變換矩陣來處理圖片的寬高比例,直接鋪滿整個視窗。

首先定義一個頂點著色器:

attribute vec4 a_position;//傳入的頂點座標
attribute vec2 a_texCoord;//傳入的紋理座標
varying vec2 v_texCoord;//傳遞給片元著色器的紋理座標
void main()
{
gl_Position = a_position;//將頂點座標賦值給OpenGL的內建變數
v_texCoord = a_texCoord;//將傳入的紋理座標傳遞給片元著色器
}
再定義一個片元著色器:
precision mediump float;//定義float精度,紋理座標使用的是一個float型別的二維向量vec2
uniform sampler2D u_texture;//紋理
varying vec2 v_texCoord;//紋理座標
void main(){
gl_FragColor = texture2D(u_texture, v_texCoord);//2D紋理取樣,將顏色賦值給OpenGL的內建變數gl_FragColor
}

再給出Android端使用這兩個著色器繪製一個圖片紋理的程式碼:

class SimpleImageRender(private val context: Context) : GLSurfaceView.Renderer {
//頂點座標
private val vCoordinates = floatArrayOf(
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
)
//紋理座標
private val textureCoordinates = floatArrayOf(
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
)
//OpenGL程式id
var programId = 0
//頂點座標控制代碼
var vCoordinateHandle = 0
//紋理座標控制代碼
var textureCoordinateHandle = 0
//紋理id
var textureId = 0
private val vertexBuffer =
ByteBuffer.allocateDirect(vCoordinates.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vCoordinates)


private val textureBuffer =
ByteBuffer.allocateDirect(textureCoordinates.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureCoordinates)


override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
vertexBuffer.position(0)
textureBuffer.position(0)
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
//根據頂點著色器和片元著色器編輯連結OpenGL程式
programId =
loadShaderWithResource(context, R.raw.simple_image_vs, R.raw.simple_image_fs)
//獲取頂點座標的控制代碼
vCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_position")
//獲取紋理座標的控制代碼
textureCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_texCoord")
//生成紋理
val textureIds = IntArray(1)
GLES20.glGenTextures(1, textureIds, 0)
if (textureIds[0] == 0) {
return
}
textureId = textureIds[0]
//繫結紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
//環繞方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT)
//過濾方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)


val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.scene1)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle()
}


override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}


override fun onDrawFrame(gl: GL10?) {
//清屏,清理掉顏色的緩衝區
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
//設定清屏的顏色,這裡是float顏色的取值範圍的[0,1]
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)


//使用program
GLES20.glUseProgram(programId)


//設定為可用的狀態
GLES20.glEnableVertexAttribArray(vCoordinateHandle)
//size 指定每個頂點屬性的元件數量。必須為1、2、3或者4。初始值為4。(如position是由3個(x,y,z)組成,而顏色是4個(r,g,b,a))
//stride 指定連續頂點屬性之間的偏移量。如果為0,那麼頂點屬性會被理解為:它們是緊密排列在一起的。初始值為0。
//size 2 代表(x,y),stride 8 代表跨度 (2個點為一組,2個float有8個位元組)
GLES20.glVertexAttribPointer(vCoordinateHandle, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer)


GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
GLES20.glVertexAttribPointer(
textureCoordinateHandle,
2,
GLES20.GL_FLOAT,
false,
8,
textureBuffer
)


GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)


}
}

這樣就完成了一個圖片的繪製:

3.2 OpenGL的轉場特效應用

3.2.1 移植開源的轉場效果

什麼是轉場效果?一般來說,就是兩個視訊畫面之間的過渡銜接效果。在opengl中,圖片的轉場,其實就是兩個紋理的過渡切換。在這裡推薦一個開源專案,該專案主要用來收集各種GL轉場特效及其 GLSL 實現程式碼,開發者可以很方便地移植到自己的專案中。

GLTransitions 專案網站地址

GLTransitions 專案有接近大概70種轉場特效,能夠非常方便的使用在圖片或者視訊的轉場中,很多轉場特效包含了混合、邊緣檢測、腐蝕膨脹等常見的影象處理方法,由易到難。

對於想學習 GLSL 的同學,既能快速上手,又能學習到一些高階影象處理方法 GLSL 實現,強烈推薦。

由於glsl程式碼在各個平臺都是通用的,所以將GLTransitions的效果移植到移動端也是比較簡單的。現在我們以該網站的第一個轉場效果為例,介紹一下移植的大致流程。

首先我們來看一下轉場所需的片元著色器的程式碼,這是實現轉場的關鍵。其中sign函式,mix函式,fract函式,step函式是glsl的內建函式。這裡只為了展示效果,不使用變換矩陣來處理圖片的寬高比例,直接鋪滿整個視窗。

uniform vec2 direction; // = vec2(0.0, 1.0)


vec4 transition (vec2 uv) {
vec2 p = uv + progress * sign(direction);
vec2 f = fract(p);
return mix(
getToColor(f),
getFromColor(f),
step(0.0, p.y) * step(p.y, 1.0) * step(0.0, p.x) * step(p.x, 1.0)
);
}

我們可以看到,從GLTransitions的片元著色器程式碼已經提供了轉場效果,但是還需要使用者進行一些修改。以上面的程式碼為例,需要我們自己定義一個轉場進度的變數progress(取值為0到1的浮點數)。還有轉場最基本的兩個要素,即圖片紋理,一個轉場需要兩個圖片紋理,從紋理1過渡到紋理2,getToColor和getFromColor就是對紋理1和紋理2取色的函式。當然還有必不可少的main函式,將我們程式計算的顏色賦值給gl_FragColor,所以我們要將上面的片元著色器程式碼修改一下。如下:

precision mediump float;
uniform vec2 direction;// = vec2(0.0, 1.0)
uniform float progress;//轉場的進度
uniform sampler2D u_texture0;//紋理1
uniform sampler2D u_texture1;//紋理2
varying vec2 v_texCoord;//紋理座標
vec4 transition (vec2 uv) {
vec2 p = uv + progress * sign(direction);
vec2 f = fract(p);
return mix(
texture2D(u_texture1, f),
texture2D(u_texture0, f),
step(0.0, p.y) * step(p.y, 1.0) * step(0.0, p.x) * step(p.x, 1.0)
);
}


void main(){
gl_FragColor = transition(v_texCoord);
}

這裡也順便給出頂點著色器的程式碼,主要就是設定頂點座標和紋理座標,關於這兩個座標上文已經介紹過了,這裡就不贅述了。程式碼如下:

attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
}

現在頂點著色器和片元著色器這兩個關鍵的著色器程式都有了,一個基本的轉場就實現了。只要在我們的程式中使用這兩個著色器,在繪製的時候根據當前的幀數不停地更新兩個紋理和轉場的進度就可以了。

下面給出繪製時的程式碼邏輯,以安卓為例:        

frameIndex++ //每次繪製修並記錄繪製的幀數
//使用program
GLES20.glUseProgram(programId)


//設定為可用的狀態
GLES20.glEnableVertexAttribArray(vCoordinateHandle)
//size 指定每個頂點屬性的元件數量。必須為1、2、3或者4。初始值為4。(如position是由3個(x,y,z)組成,而顏色是4個(r,g,b,a))
//stride 指定連續頂點屬性之間的偏移量。如果為0,那麼頂點屬性會被理解為:它們是緊密排列在一起的。初始值為0。
//size 2 代表(x,y),stride 8 代表跨度 (2個點為一組,2個float有8個位元組)
GLES20.glVertexAttribPointer(vCoordinateHandle, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer)


GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
GLES20.glVertexAttribPointer(
textureCoordinateHandle,
2,
GLES20.GL_FLOAT,
false,
8,
textureBuffer
)


val uTexture0Handle = GLES20.glGetUniformLocation(programId, "u_texture0")
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(
GLES20.GL_TEXTURE_2D,
imageTextureIds[(frameIndex / transitionFrameCount) % imageNum]
)
GLES20.glUniform1i(uTexture0Handle, 0)


val uTexture1Handle = GLES20.glGetUniformLocation(programId, "u_texture1")
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
GLES20.glBindTexture(
GLES20.GL_TEXTURE_2D,
imageTextureIds[(frameIndex / transitionFrameCount + 1) % imageNum]
)
GLES20.glUniform1i(uTexture1Handle, 1)


val directionHandle = GLES20.glGetUniformLocation(programId, "direction")
GLES20.glUniform2f(directionHandle, 0f, 1f)


val uOffsetHandle = GLES20.glGetUniformLocation(programId, "u_offset")
val offset = (frameIndex % transitionFrameCount) * 1f / transitionFrameCount
GLES20.glUniform1f(uOffsetHandle, offset)
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

以上就是將一個GLTransitions網站中的轉場特效移植到Android端的基本流程。iOS的也是類似的,非常方便。

3.2.2 實現複雜轉場效果

通過上面的介紹,我們已經對如何使用opengl來處理圖片轉場有了一個簡單的瞭解。但是剛剛的操作只能讓多張圖片都使用同一種轉場,這樣比較單調乏味。 下面介紹一個思路, 在用多張圖片合成轉場效果時, 將不同的轉場效果組合起來使用。

回想一下,剛剛做轉場移植的時候,只是使用了一個opengl程式。現在咱們來載入多個opengl程式,然後在不同的時間段使用對應的opengl程式,這樣就能比較方便地實現多個轉場效果的組合使用了。

首先定義一個IDrawer介面,表示一個使用opengl程式的物件:

interface IDrawer {
//準備階段,準備程式,資源
fun onPrepare()
//繪製
fun onDraw(frameIndex:Int){}


fun onSurfaceChanged(p0: GL10?, width: Int, height: Int){


}
}

然後定義一個render,來控制如何使用這些IDrawer:

class ComposeRender : GLSurfaceView.Renderer {
private var frameIndex = 0//當前繪製了多少幀
private var drawersFrames = 0 //所有的drawer繪製一遍需要的幀數,目前每一個drawer佔用200幀
private val framesPerDrawer = 200//每一個IDrawer繪製所需要的幀數,這裡暫時固定為200


//使用的IDrawer集合
private val drawers = mutableListOf(
HelloWorldTransitionDrawer(),
SimpleTransitionDrawer(),
PerlinTransitionDrawer(),
)


init {
drawersFrames = drawers.size.times(framesPerDrawer)
}


override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
//設定清屏的顏色,這裡是float顏色的取值範圍的[0,1]
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
//清屏,清理掉顏色的緩衝區
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
drawers.forEach {
it.onPrepare()
}
}


override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {
GLES20.glViewport(0, 0, p1, p2)
drawers.forEach {
it.onSurfaceChanged(p0, p1, p2)
}
}


override fun onDrawFrame(p0: GL10?) {
frameIndex++
//清屏,清理掉顏色的緩衝區
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
val offset = frameIndex % drawersFrames
val logicFrame = if (offset == 0) 1 else offset
//計算當前的幀數輪到哪個IDrawer的繪製,讓對應的IDrawer進行繪製
drawers.forEachIndexed { index, iDrawer ->
if (logicFrame <= (index + 1).times(framesPerDrawer) && logicFrame >= index.times(
framesPerDrawer
)
) {
iDrawer.onDraw(logicFrame - index.times(framesPerDrawer))
}
}
}
}

這裡為了方便展示流程,先將紋理和每個轉場的耗時(即使用的幀數)的使用固定值寫在程式碼裡。比如現在有四張圖片編號為1,2,3,4,我們就定義三個IDrawer A,B,C。A使用圖片1和圖片2,B使用圖片2和圖片3,C使用圖片3和圖片4,然後每個轉場都耗時200幀,這樣就能實現三個opengl程式的組合轉場了。

下面給出其中一個IDrawer的實現類:

class HelloWorldTransitionDrawer() : IDrawer {
private val imageNum = 2//需要使用兩個圖片紋理


//轉場需要耗費的幀數,這裡固定寫200幀
private val transitionFrameCount = 200
private val vCoordinates = floatArrayOf(
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
)
private val textureCoordinates = floatArrayOf(
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
)
var programId = 0
var vCoordinateHandle = 0
var textureCoordinateHandle = 0
var imageTextureIds = IntArray(imageNum)
private val vertexBuffer =
ByteBuffer.allocateDirect(vCoordinates.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vCoordinates).position(0)


private val textureBuffer =
ByteBuffer.allocateDirect(textureCoordinates.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureCoordinates).position(0)


override fun onPrepare() {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
programId =
loadShaderWithResource(
MyApplication.getApp(),
R.raw.helloworld_transition_vs,
R.raw.helloworld_transition_fs
)
vCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_position")
textureCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_texCoord")
//生成紋理
val textureIds = IntArray(1)
GLES20.glGenTextures(1, textureIds, 0)
if (textureIds[0] == 0) {
return
}
loadTextures(intArrayOf(R.drawable.scene1, R.drawable.scene2))
}


override fun onDraw(frameIndex:Int) {
//使用program
GLES20.glUseProgram(programId)


//設定為可用的狀態
GLES20.glEnableVertexAttribArray(vCoordinateHandle)
//size 指定每個頂點屬性的元件數量。必須為1、2、3或者4。初始值為4。(如position是由3個(x,y,z)組成,而顏色是4個(r,g,b,a))
//stride 指定連續頂點屬性之間的偏移量。如果為0,那麼頂點屬性會被理解為:它們是緊密排列在一起的。初始值為0。
//size 2 代表(x,y),stride 8 代表跨度 (2個點為一組,2個float有8個位元組)
GLES20.glVertexAttribPointer(vCoordinateHandle, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer)


GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
GLES20.glVertexAttribPointer(
textureCoordinateHandle,
2,
GLES20.GL_FLOAT,
false,
8,
textureBuffer
)


val uTexture0Handle = GLES20.glGetUniformLocation(programId, "u_texture0")
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(
GLES20.GL_TEXTURE_2D,
imageTextureIds[0]
)
GLES20.glUniform1i(uTexture0Handle, 0)


val uTexture1Handle = GLES20.glGetUniformLocation(programId, "u_texture1")
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
GLES20.glBindTexture(
GLES20.GL_TEXTURE_2D,
imageTextureIds[1]
)
GLES20.glUniform1i(uTexture1Handle, 1)


val directionHandle = GLES20.glGetUniformLocation(programId, "direction")
GLES20.glUniform2f(directionHandle, 0f, 1f)


val uOffsetHandle = GLES20.glGetUniformLocation(programId, "u_offset")
val offset = (frameIndex % transitionFrameCount) * 1f / transitionFrameCount
GLES20.glUniform1f(uOffsetHandle, offset)
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}


private fun loadTextures(resIds: IntArray) {
if (resIds.isEmpty()) return
//直接生成兩個紋理
GLES20.glGenTextures(2, imageTextureIds, 0)
resIds.forEachIndexed { index, resId ->
if (imageTextureIds.indexOfFirst {
it == 0


} == 0) return
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index)
//繫結紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, imageTextureIds[index])
//環繞方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT)
//過濾方式
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR
)
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR
)


val bitmap = BitmapFactory.decodeResource(MyApplication.getApp().resources, resId)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle()
}
}
}

這樣就可以達到將多個轉場組合使用的目的。

四、總結

在移動端進行圖形處理時, OpenGL 憑藉其效率高,相容性好的優勢,得到了大家的青睞。

本文對 OpenGL 的基本概念和繪製流程進行了簡單介紹,讓大家對 OpenGL 的繪製流程有了一個初步的認識。在繪製流程中,對我們開發者比較重要的是使用GLSL來編寫頂點著色器和片元著色器。在使用 OpenGL 處理圖片輪播轉場時,關鍵點是編寫轉場所需的著色器,我們可以參考GLTransitions網站的開源轉場效果。該網站提供豐富的轉場效果和著色器程式碼,可以很方便的移植到客戶端中。

對於實現複雜轉場,即將多個轉場效果組合使用,本文也提供了一個思路,就是組合使用多個 OpenGL 程式,在對應的時間點載入並使用對應的 OpenGL 程式。

鑑於篇幅原因,本文分享了部分我們基於 OpenGL 開發視訊轉場特效的思考與實踐,希望對大家有所幫助,歡迎更多關於音視訊編輯的實踐和交流。

【推薦閱讀】

“攜程技術”公眾號

分享,交流,成長