Android相機OpenGL特效渲染

你們好,今天給你們介紹Android上如何利用OpenGL進行相機特效渲染。git

相機特效渲染是什麼呢?所謂特效是一個比較寬泛的概念,對相機採集到的畫面作必定的修改,加上必定的效果再展現出來,均可以叫特效,好比咱們有時候會用一些app來進行自拍,有美顏、提亮等效果,還能夠在畫面上添加各類貼紙,或者是讓畫面放大、縮小、抖動等,都是特效。github

要實現相機特效渲染,總的來講有3步,首先須要採集到相機圖像,而後對它進行特效處理,最後顯示出來。數組

咱們先來看看若是從相機採集數據,在Android上,相機有2種返回幀數據的方式,一種是以byte數組的方式返回,一種是以texture的方式返回。app

前一種方式返回的byte數組能夠直接在CPU上操做,處理後可轉成bitmap最終顯示到ImageView上,但這種方式效率相對來講比較低,由於對於圖像處理和渲染,CPU遠沒有GPU效率高,可是這種方式學習門檻低,不須要學習OpenGLide

後一種方式由於直接經過texture返回,所以從返回數據,到特效處理,到最後顯示,所有能夠經過OpenGLGPU上作,效率很是高,如今的商業app都是用這種方式,這篇文章也將介紹這種方式,若是你對OpenGL還不瞭解,能夠參考個人Android OpenGL ES 2.0 手把手教學系列文章及OpenGL ES 高級進階系列文章。oop

在使用相機時,首先須要打開相機,固然,相機權限必不可少,這裏不哆嗦了,有不少文章講解相機權限:post

...
val cameraId = getCameraId(Camera.CameraInfo.CAMERA_FACING_BACK)
camera = Camera.open(cameraId)
...
private fun getCameraId(facing : Int) : Int {
    val numberOfCameras = Camera.getNumberOfCameras()
    for (i in 0 until numberOfCameras) {
        val info = Camera.CameraInfo()
        Camera.getCameraInfo(i, info)
        if (info.facing == facing) {
            return i
        }
    }
    return -1
}
複製代碼

首先須要獲取要打開的相機的id,通常來講,手機上有前置和後置兩個相機,這裏獲取了後置相機的id並打開了它,注意這裏的打開並非說相機就開始工做取景了,而僅僅獲得這個相機對像而已,此時相機並無開始工做,雖然沒有開始工做,但若是不釋放的話,別的應用再去打開會失敗。學習

獲得相機對象後,下面須要設置一些參數,有不少參數能夠設置,如預覽分辨率、對焦方式、拍照分辨率等等,這裏咱們只設置預覽分辨率和顯示角度。ui

相機的預覽分辨率只能從支持的列表中選一個設置,不能設置任意值,實際使用時,通常會作些篩選邏輯,好比一個720P的屏幕,選了一個1080P的分辨率,其實沒什麼意義,會浪費資源,這裏簡單起見,我就直接取支持列表中的第0個。this

private fun setPreviewSize(parameters: Camera.Parameters) {
    parameters.setPreviewSize(
        parameters.supportedPreviewSizes[0].width,					   
        parameters.supportedPreviewSizes[0].height
    )    
}
複製代碼

下面是設置旋轉角度:

val info = Camera.CameraInfo()
Camera.getCameraInfo(cameraId, info)
camera.setDisplayOrientation(info.orientation)
複製代碼

這個旋轉角度會影響到咱們看到的圖像的旋轉,通常狀況下這樣設置就能夠了,不過有些機型會有兼容性問題,須要特殊機型特殊設置。

前面說到,咱們會讓相機採集到的幀圖像經過texture返回,這是經過setPreviewTexture方法設置的:

camera.setPreviewTexture(surfaceTexture)
複製代碼

注意這裏的surfaceTexture它不是texturetexture是一個int類的值,這裏的surfaceTextureSurfaceTexture類的一個對象,它是經過texture建立出來的,能夠認爲它是將texture包了一層,這裏的texturesurfaceTexture都是咱們本身建立的,而後設置給camera

既然要建立texture,那就須要OpenGL環境,關於OpenGL環境能夠參考個人一篇文章《OpenGL ES 高級進階:EGL及GL線程》,作OpenGL渲染,通常會用GLSurfaceView,它自帶了OpenGL環境,不須要咱們再去建立,這裏我用TextureView,它是不帶OpenGL環境的,我本身封裝了一個OpenGL環境,使它的功能和GLSurfaceView同樣,TextureView相比於GLSurfaceView來講還有一大好處就是,它同時有和普通view同樣的功能,好比咱們能夠將它像一個普通的view同樣放到一個RecyclerView中的item中顯示,不會有問題,而若是把GLSurfaceView這樣作,會有問題,相似的,若是你去移動一個GLSurfaceView,也會發現有這樣那樣的問題,根源就在於GLSurfaceView它不是在view樹上的,它和普通的view是不同的。所以,我這裏封裝了一個帶有OpenGL環境的TextureView,叫GLTextureView,它比GLSurfaceView的功能更強大,而用於顯示相機特效渲染的View繼承了GLTextureView,叫GLCameraView,它能夠綁定一個實現了ICamera接口的自定義camera類,用於向GLCameraView提供一些操做camera的方法以及一些獲取心要信息的方法。

咱們的texture也就是從這裏的OpenGL環境建立出來的,注意這裏建立的texture,不是普通的texture,是OES類型的texture,相機和視頻硬解碼出來的內容,都須要用OES類型的texture承載,不然會報錯。

OES類型texture建立:

fun createOESTexture(): Int {
    val textures = IntArray(1)
    GLES30.glGenTextures(textures.size, textures, 0)
    GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0])
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_WRAP_S, 
        GLES30.GL_CLAMP_TO_EDGE
    )
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE
    )
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR
    )
    GLES30.glTexParameteri(
        GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 
        GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR
    )
    GLES30.glBindTexture(GLES30.GL_TEXTURE_EXTERNAL_OES, 0)
    return textures[0]
}
複製代碼

設置好SurfaceTexture以後,咱們還須要給SurfaceTexture設置回調來感知相機給咱們返回數據了:

...
st?.setOnFrameAvailableListener(this)
...
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    ...
    surfaceTexture?.updateTexImage()
    ...
}
複製代碼

當相機給咱們返回一幀數據時,onFrameAvailable就會回調一次告訴咱們有數據了,注意這只是告訴咱們有數據了,並非告訴咱們已經把數據放到texture上了,此時咱們須要調用updateTexImage,相機幀數據纔會更新到texture上,因爲這一步是對texture進行操做,因此updateTexImage須要在GL線程調用。

onFrameAvailable是從GL線程回調過來嗎?不必定,這處決於你在哪裏建立SurfaceTexture,若是建立SurfaceTexture的線程有Looper,它就會持有這個Looper,以後回調onFrameAvailable就會經過這個Looper建立一個handler來在這個線程回調,若是建立SurfaceTexture的線程有沒有Looper,它就會在主線程中回調過來。所以,若是建立SurfaceTexture的線程有Looper而且是GL線程,那麼onFrameAvailable回調過來就是GL線程,不然就不是,此時就不能在onFrameAvailable裏調用updateTexImage

在個人代碼中,我確保onFrameAvailable回調過來就是GL線程,所以我直接在裏面調用updateTexImage

這些都設置好以後,咱們就調用startPreview,這時相機就真正開始工做了:

camera.startPreview()
複製代碼

此至,咱們就完成了將相機幀數據採集到一個texture上,那麼特效處理就以這個texture作爲輸入,利用OpenGL進行渲染,這裏我使用個人一個庫FunRenderer進行渲染,它將OpenGL進行了封裝,使用起來很方便。

前面提到咱們封裝了一個GLCameraView用於顯示渲染的結果,這裏我經過callback回調3個方法,分別用於初始化、渲染、釋放:

interface RenderCallback {

    fun onInit()
    fun onRenderFrame(oesTexture: Int, stMatrix: FloatArray, cameraPreviewSize: Size, surfaceSize: Size)
    fun onRelease()

}
複製代碼

咱們也就是在這三個方法中使用FunRenderer

val cameraWrapper = CameraWrapper()
cameraView.bindCamera(cameraWrapper)
cameraView.renderCallback = object : GLCameraView.RenderCallback {

    private val oes2RGBARenderer = OES2RGBARenderer()
    private val cropRenderer = CropRenderer()
    private val effectRenderer = TestEffectRenderer()
    private val screenRenderer = ScreenRenderer()
    private lateinit var renderChain: RenderChain

    override fun onInit() {
        renderChain = RenderChain.create()
        .addRenderer(oes2RGBARenderer)
        .addRenderer(cropRenderer)
        .addRenderer(effectRenderer)
        .addRenderer(screenRenderer)
        renderChain.init()
    }

    override fun onRenderFrame(oesTexture: Int, stMatrix: FloatArray, cameraPreviewSize: Size, surfaceSize: Size) {
        GLES30.glClearColor(0f, 0f, 0f, 1f)
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        val input = Texture(oesTexture, cameraPreviewSize.height, cameraPreviewSize.width, false)
        val data = mutableMapOf<String, Any>()
        data[Keys.ST_MATRIX] = stMatrix
        data[Keys.CROP_RATIO] = surfaceSize.width.toFloat() / surfaceSize.height
        data[Keys.SURFACE_WIDTH] = surfaceSize.width
        data[Keys.SURFACE_HEIGHT] = surfaceSize.height
        renderChain.render(input, data)
    }

    override fun onRelease() {
        renderChain.release()
    }

}
複製代碼

這裏每幀作的操做有OESRGBA、裁剪、一個簡單的特效、上屏。

前面提到,相機返回的幀數據不是用普通的texture承載的,而是用OES類型的texture承載的,所以第一步須要經過OES2RGBARenderer將它轉換成RGBAtexture

而後再經過CropRenderer進行一步裁剪,由於咱們選的預覽分辨率,可能和咱們實現顯示的區域的比例是不同的,若是不裁剪,強行徹底填充,就會變形。

而後咱們作一個簡單的特效,TestEffectRenderer繼承於SimpleRenderer,我用一個簡單的shader實現一個簡單的特效:

#version 300 es
precision mediump float;
in vec2 v_textureCoordinate;
layout(location = 0) out vec4 fragColor;
uniform sampler2D u_texture;
void main() {
    vec4 c = texture(u_texture, v_textureCoordinate);
    c.b = 0.5;
    fragColor = c;
}
複製代碼

這裏我簡單地將藍色經過的值設爲0.5,獲得的效果就是有點偏藍,就像作了一個濾鏡了同樣。固然,實際使用的顏色效果濾鏡比這個要複雜得多,通常用LUT實現,這裏只是作一個簡單的示例。

最後經過ScreenRenderer上屏,這樣咱們就完成了相機特效渲染,實際中你們看到的相機特效,無非就是本身寫各類各樣的shader,組合各類渲染步驟獲得的。

咱們來看下效果:

我封裝了一個庫HiCamera(github.com/kenneycode/…),能夠方便地綁定相機進行特效渲染,demo裏我用個人FunRenderer(github.com/kenneycode/…)庫來渲染,能夠本身繼承FunRendererSimpleRenderer進行擴展,或者用別的,本文中的代碼也都在HiCamerademo中。

感謝閱讀!

相關文章
相關標籤/搜索