NDK51_OpenGL:FBO

魚包子Ray發表於2020-12-09

NDK開發彙總

一 FBO

​ 幀緩衝物件:FBO(Frame Buffer Object)。預設情況下,我們在GLSurfaceView中繪製的結果是顯示到螢幕上,然而實際中有很多情況並不需要渲染到螢幕上,這個時候使用FBO就可以很方便的實現這類需求。FBO可以讓我們的渲染不渲染到螢幕上,而是渲染到離屏Buffer中。

​ 前面建立了一個ScreenFilter類用來封裝將攝像頭資料顯示當螢幕上,然而我們需要在顯示之前增加各種**“效果”,如果我們只存在一個ScreenFilter,那麼所有的"效果"**都會積壓在這個類中,同時也需要大量的if else來判斷是否開啟效果。

在這裡插入圖片描述

​ 我們可以將每種效果寫到單獨的一個Filter中去,並且在ScreenFilter之前的所有Filter都不需要顯示到螢幕中,所以在ScreenFilter之前都將其使用FBO進行快取。

需要注意的是: 攝像頭畫面經過FBO的快取時候,我們再從FBO繪製到螢幕,這時候就不需要再使用samplerExternalOES與變換矩陣了。這意味著ScreenFilter,使用的取樣器就是正常的sampler2D,也不需要#extension GL_OES_EGL_image_external : require

然而在最原始的狀態下是沒有開啟任何效果的,所以ScreenFilter就比較尷尬。

1、開啟效果: 使用sampler2D

2、未開啟效果: 使用samplerExternalOES

那麼就需要在ScreenFilter中使用if else來進行判斷,但這個判斷稍顯麻煩,所以這裡我選擇使用:

在這裡插入圖片描述

從攝像頭使用的紋理首先繪製到CameraFilterFBO中,這樣無論是否開啟效果ScreenFilter都是以sampler2D來進行取樣。

二 FBO簡單使用

1 建立View和Renderer

public class DouyinView extends GLSurfaceView {

    private DouyinRenderer douyinRenderer;

    public DouyinView(Context context) {
        this(context,null);
    }

    public DouyinView(Context context, AttributeSet attrs) {
        super(context, attrs);
        /**
         * 配置GLSurfaceView
         */
        //設定EGL版本
        setEGLContextClientVersion(2);
        douyinRenderer = new DouyinRenderer(this);
        setRenderer(douyinRenderer);
        //設定按需渲染 當我們呼叫 requestRender 請求GLThread 回撥一次 onDrawFrame
        // 連續渲染 就是自動的回撥onDrawFrame
        setRenderMode(RENDERMODE_WHEN_DIRTY);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        super.surfaceDestroyed(holder);
        douyinRenderer.onSurfaceDestroyed();
    }
}

DouyinRenderer

public class DouyinRenderer implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {

    private ScreenFilter mScreenFilter;
    private DouyinView mView;
    private CameraHelper mCameraHelper;
    private SurfaceTexture mSurfaceTexture;
    private float[] mtx = new float[16];
    private int[] mTextures;
    private CameraFilter mCameraFilter;

    public DouyinRenderer(DouyinView douyinView) {
        mView = douyinView;
    }

    /**
     * 畫布建立好啦
     *
     * @param gl
     * @param config
     */
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //初始化的操作
        mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);
        //準備好攝像頭繪製的畫布
        //通過opengl建立一個紋理id
        mTextures = new int[1];
        //偷懶 這裡可以不配置 (當然 配置了也可以)
        GLES20.glGenTextures(mTextures.length, mTextures, 0);
        mSurfaceTexture = new SurfaceTexture(mTextures[0]);
        //
        mSurfaceTexture.setOnFrameAvailableListener(this);
        //注意:必須在gl執行緒操作opengl
        mCameraFilter = new CameraFilter(mView.getContext());
        mScreenFilter = new ScreenFilter(mView.getContext());
    }

    /**
     * 畫布發生了改變
     *
     * @param gl
     * @param width
     * @param height
     */
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //開啟預覽
        mCameraHelper.startPreview(mSurfaceTexture);
        mCameraFilter.onReady(width,height);
        mScreenFilter.onReady(width,height);
    }

    /**
     * 開始畫畫吧
     *
     * @param gl
     */
    @Override
    public void onDrawFrame(GL10 gl) {
        // 配置螢幕
        //清理螢幕 :告訴opengl 需要把螢幕清理成什麼顏色
        GLES20.glClearColor(0, 0, 0, 0);
        //執行上一個:glClearColor配置的螢幕顏色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        // 把攝像頭的資料先輸出來
        // 更新紋理,然後我們才能夠使用opengl從SurfaceTexure當中獲得資料 進行渲染
        mSurfaceTexture.updateTexImage();
        //surfaceTexture 比較特殊,在opengl當中 使用的是特殊的取樣器 samplerExternalOES (不是sampler2D)
        //獲得變換矩陣
        mSurfaceTexture.getTransformMatrix(mtx);
        //
        mCameraFilter.setMatrix(mtx);
        //責任鏈
        int id = mCameraFilter.onDrawFrame(mTextures[0]);
        //加效果濾鏡
        // id  = 效果1.onDrawFrame(id);
        // id = 效果2.onDrawFrame(id);
        //....
        //加完之後再顯示到螢幕中去
        mScreenFilter.onDrawFrame(id);
    }

    /**
     * surfaceTexture 有一個有效的新資料的時候回撥
     *
     * @param surfaceTexture
     */
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        mView.requestRender();
    }

    public void onSurfaceDestroyed() {
        mCameraHelper.stopPreview();
    }
}

2 配置著色器

基本繪製

base_vertex.vert

//SurfaceTexture比較特殊
//float資料是什麼精度的
precision mediump float;

//取樣點的座標
varying vec2 aCoord;

//取樣器 不是從android的surfaceTexure中的紋理 採資料了,所以不再需要android的擴充套件紋理取樣器了
//使用正常的 sampler2D
uniform sampler2D vTexture;

void main(){
    //變數 接收畫素值
    // texture2D:取樣器 採集 aCoord的畫素
    //賦值給 gl_FragColor 就可以了
    gl_FragColor = texture2D(vTexture,aCoord);
}

base_frag.frag

//SurfaceTexture比較特殊
//float資料是什麼精度的
precision mediump float;

//取樣點的座標
varying vec2 aCoord;

//取樣器 不是從android的surfaceTexure中的紋理 採資料了,所以不再需要android的擴充套件紋理取樣器了
//使用正常的 sampler2D
uniform sampler2D vTexture;

void main(){
    //變數 接收畫素值
    // texture2D:取樣器 採集 aCoord的畫素
    //賦值給 gl_FragColor 就可以了
    gl_FragColor = texture2D(vTexture,aCoord);
}

處理

camera_vertex2.vert

// 把頂點座標給這個變數, 確定要畫畫的形狀
attribute vec4 vPosition;
//接收紋理座標,接收取樣器取樣圖片的座標
attribute vec4 vCoord;
//變換矩陣, 需要將原本的vCoord(01,11,00,10) 與矩陣相乘 才能夠得到 surfacetexure(特殊)的正確的取樣座標
uniform mat4 vMatrix;
//傳給片元著色器 畫素點
varying vec2 aCoord;
void main(){
    //內建變數 gl_Position ,我們把頂點資料賦值給這個變數 opengl就知道它要畫什麼形狀了
    gl_Position = vPosition;
    // 進過測試 和裝置有關(有些裝置直接就採集不到影像,有些呢則會映象)
    aCoord = (vMatrix * vCoord).xy;
    //aCoord =  vec2((vCoord*vMatrix).x,(vCoord*vMatrix).y);
}

camera_frag2.frag

// 把頂點座標給這個變數, 確定要畫畫的形狀
attribute vec4 vPosition;
//接收紋理座標,接收取樣器取樣圖片的座標
attribute vec4 vCoord;
//變換矩陣, 需要將原本的vCoord(01,11,00,10) 與矩陣相乘 才能夠得到 surfacetexure(特殊)的正確的取樣座標
uniform mat4 vMatrix;
//傳給片元著色器 畫素點
varying vec2 aCoord;
void main(){
    //內建變數 gl_Position ,我們把頂點資料賦值給這個變數 opengl就知道它要畫什麼形狀了
    gl_Position = vPosition;
    // 進過測試 和裝置有關(有些裝置直接就採集不到影像,有些呢則會映象)
    aCoord = (vMatrix * vCoord).xy;
    //aCoord =  vec2((vCoord*vMatrix).x,(vCoord*vMatrix).y);
}

3 建立Filter

AbstractFilter

public abstract class AbstractFilter {

    protected FloatBuffer mGLVertexBuffer;
    protected FloatBuffer mGLTextureBuffer;

    //頂點著色
    protected int mVertexShaderId;
    //片段著色
    protected int mFragmentShaderId;


    protected int mGLProgramId;
    /**
     * 頂點著色器
     * attribute vec4 position;
     * 賦值給gl_Position(頂點)
     */
    protected int vPosition;
    /**
     * varying vec2 textureCoordinate;
     */
    protected int vCoord;


    /**
     * uniform mat4 vMatrix;
     */
    protected int vMatrix;

    /**
     * 片元著色器
     * Samlpe2D 擴充套件 samplerExternalOES
     */
    protected int vTexture;


    protected int mOutputWidth;
    protected int mOutputHeight;

    public AbstractFilter(Context context, int vertexShaderId, int fragmentShaderId) {
        this.mVertexShaderId = vertexShaderId;
        this.mFragmentShaderId = fragmentShaderId;
        // 4個點 x,y = 4*2 float 4位元組 所以 4*2*4
        mGLVertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mGLVertexBuffer.clear();
        float[] VERTEX = {
                -1.0f, -1.0f,
                1.0f, -1.0f,
                -1.0f, 1.0f,
                1.0f, 1.0f
        };
        mGLVertexBuffer.put(VERTEX);


        mGLTextureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mGLTextureBuffer.clear();
        float[] TEXTURE = {
                0.0f, 1.0f,
                1.0f, 1.0f,
                0.0f, 0.0f,
                1.0f, 0.0f
        };
        mGLTextureBuffer.put(TEXTURE);


        initilize(context);
        initCoordinate();
    }


    protected void initilize(Context context) {
        String vertexSharder = OpenGLUtils.readRawTextFile(context, mVertexShaderId);
        String framentShader = OpenGLUtils.readRawTextFile(context, mFragmentShaderId);
        mGLProgramId = OpenGLUtils.loadProgram(vertexSharder, framentShader);
        // 獲得著色器中的 attribute 變數 position 的索引值
        vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition");
        vCoord = GLES20.glGetAttribLocation(mGLProgramId,
                "vCoord");
        vMatrix = GLES20.glGetUniformLocation(mGLProgramId,
                "vMatrix");
        // 獲得Uniform變數的索引值
        vTexture = GLES20.glGetUniformLocation(mGLProgramId,
                "vTexture");
    }


    public void onReady(int width, int height) {
        mOutputWidth = width;
        mOutputHeight = height;
    }


    public void release() {
        GLES20.glDeleteProgram(mGLProgramId);
    }


    public int onDrawFrame(int textureId) {
        //設定顯示視窗
        GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight);

        //使用著色器
        GLES20.glUseProgram(mGLProgramId);

        //傳遞座標
        mGLVertexBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGLVertexBuffer);
        GLES20.glEnableVertexAttribArray(vPosition);

        mGLTextureBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);


        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
        GLES20.glUniform1i(vTexture, 0);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        return textureId;
    }

    //修改座標
    protected void initCoordinate() {

    }

}

CameraFilter

/**
 * 不需要顯示到螢幕上
 * 寫入fbo (幀快取)
 */
public class CameraFilter  extends AbstractFilter{

    private int[] mFrameBuffers;
    private int[] mFrameBufferTextures;
    private float[] matrix;

    public CameraFilter(Context context) {
        super(context, R.raw.camera_vertex2, R.raw.camera_frag2);
    }

    @Override
    protected void initCoordinate() {
        mGLTextureBuffer.clear();
        //攝像頭是顛倒的
//        float[] TEXTURE = {
//                0.0f, 0.0f,
                1.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 1.0f
//        };
        //調整好了映象
//        float[] TEXTURE = {
//                1.0f, 0.0f,
//                0.0f, 0.0f,
//                1.0f, 1.0f,
//                0.0f, 1.0f,
//        };
        //修復旋轉 逆時針旋轉90度
        float[] TEXTURE = {
                0.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 0.0f,
                1.0f, 1.0f,
        };
        mGLTextureBuffer.put(TEXTURE);
    }

    @Override
    public void release() {
        super.release();
        destroyFrameBuffers();
    }

    public void destroyFrameBuffers() {
        //刪除fbo的紋理
        if (mFrameBufferTextures != null) {
            GLES20.glDeleteTextures(1, mFrameBufferTextures, 0);
            mFrameBufferTextures = null;
        }
        //刪除fbo
        if (mFrameBuffers != null) {
            GLES20.glDeleteFramebuffers(1, mFrameBuffers, 0);
            mFrameBuffers = null;
        }
    }

    @Override
    public void onReady(int width, int height) {
        super.onReady(width, height);
        if (mFrameBuffers != null) {
            destroyFrameBuffers();
        }

        //fbo的建立 (快取)
        //1、建立fbo (離屏螢幕)
        mFrameBuffers = new int[1];
        // 1、建立幾個fbo 2、儲存fbo id的資料 3、從這個陣列的第幾個開始儲存
        GLES20.glGenFramebuffers(mFrameBuffers.length,mFrameBuffers,0);

        //2、建立屬於fbo的紋理
        mFrameBufferTextures = new int[1]; //用來記錄紋理id
        //建立紋理
        OpenGLUtils.glGenTextures(mFrameBufferTextures);

        //讓fbo與 紋理髮生關係
        //建立一個 2d的影像
        // 目標 2d紋理+等級 + 格式 +寬、高+ 格式 + 資料型別(byte) + 畫素資料
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,mFrameBufferTextures[0]);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_RGBA,mOutputWidth,mOutputHeight,
                0,GLES20.GL_RGBA,GLES20.GL_UNSIGNED_BYTE, null);
        // 讓fbo與紋理繫結起來 , 後續的操作就是在操作fbo與這個紋理上了
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,mFrameBuffers[0]);
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,GLES20.GL_COLOR_ATTACHMENT0,
                GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
        //解綁
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);

    }

    @Override
    public int onDrawFrame(int textureId) {
        //設定顯示視窗
        GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight);

        //不呼叫的話就是預設的操作glsurfaceview中的紋理了。顯示到螢幕上了
        //這裡我們還只是把它畫到fbo中(快取)
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,mFrameBuffers[0]);

        //使用著色器
        GLES20.glUseProgram(mGLProgramId);

        //傳遞座標
        mGLVertexBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGLVertexBuffer);
        GLES20.glEnableVertexAttribArray(vPosition);

        mGLTextureBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);

        //變換矩陣
        GLES20.glUniformMatrix4fv(vMatrix,1,false,matrix,0);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        //因為這一層是攝像頭後的第一層,所以需要使用擴充套件的  GL_TEXTURE_EXTERNAL_OES
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
        GLES20.glUniform1i(vTexture, 0);

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

        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
        //返回fbo的紋理id
        return mFrameBufferTextures[0];
    }

    public void setMatrix(float[] matrix) {
        this.matrix = matrix;
    }
}

ScreenFilter

/**
 * 負責往螢幕上渲染
 */
public class ScreenFilter extends AbstractFilter{

    public ScreenFilter(Context context) {
        super(context,R.raw.base_vertex, R.raw.base_frag);
    }

}

4 工具類

OpenGLUtils

public class OpenGLUtils {

    public static String readRawTextFile(Context context, int rawId) {
        InputStream is = context.getResources().openRawResource(rawId);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line;
        StringBuilder sb = new StringBuilder();
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }

    public static int loadProgram(String vSource,String fSource){
        /**
         * 頂點著色器
         */
        int vShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        //載入著色器程式碼
        GLES20.glShaderSource(vShader,vSource);
        //編譯(配置)
        GLES20.glCompileShader(vShader);

        //檢視配置 是否成功
        int[] status = new int[1];
        GLES20.glGetShaderiv(vShader,GLES20.GL_COMPILE_STATUS,status,0);
        if(status[0] != GLES20.GL_TRUE){
            //失敗
            throw new IllegalStateException("load vertex shader:"+GLES20.glGetShaderInfoLog(vShader));
        }


        /**
         *  片元著色器
         *  流程和上面一樣
         */

        int fShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        //載入著色器程式碼
        GLES20.glShaderSource(fShader,fSource);
        //編譯(配置)
        GLES20.glCompileShader(fShader);
        //檢視配置 是否成功
        GLES20.glGetShaderiv(fShader,GLES20.GL_COMPILE_STATUS,status,0);
        if(status[0] != GLES20.GL_TRUE){
            //失敗
            throw new IllegalStateException("load fragment shader:"+GLES20.glGetShaderInfoLog(vShader));
        }


        /**
         * 建立著色器程式
         */
        int program = GLES20.glCreateProgram();
        //繫結頂點和片元
        GLES20.glAttachShader(program,vShader);
        GLES20.glAttachShader(program,fShader);
        //連結著色器程式
        GLES20.glLinkProgram(program);
        //獲得狀態
        GLES20.glGetProgramiv(program,GLES20.GL_LINK_STATUS,status,0);
        if(status[0] != GLES20.GL_TRUE){
            throw new IllegalStateException("link program:"+GLES20.glGetProgramInfoLog(program));
        }

        GLES20.glDeleteShader(vShader);
        GLES20.glDeleteShader(fShader);

        return program;
    }
    /**
     * 建立紋理並配置
     */
    public static void glGenTextures(int[] textures) {
        //建立
        GLES20.glGenTextures(textures.length, textures, 0);
        //配置
        for (int i = 0; i < textures.length; i++) {
            // opengl的操作 程式導向的操作
            //bind 就是繫結 ,表示後續的操作就是在這一個 紋理上進行
            // 後面的程式碼配置紋理,就是配置bind的這個紋理
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textures[i]);
            /**
             * 過濾引數
             *  當紋理被使用到一個比他大 或者比他小的形狀上的時候 該如何處理
             */
            // 放大
            // GLES20.GL_LINEAR  : 使用紋理中座標附近的若干個顏色,通過平均演算法 進行放大
            // GLES20.GL_NEAREST : 使用紋理座標最接近的一個顏色作為放大的要繪製的顏色
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_NEAREST);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);

            /*設定紋理環繞方向*/
            //紋理座標 一般用st表示,其實就是x y
            //紋理座標的範圍是0-1。超出這一範圍的座標將被OpenGL根據GL_TEXTURE_WRAP引數的值進行處理
            //GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T 分別為x,y方向。
            //GL_REPEAT:平鋪
            //GL_MIRRORED_REPEAT: 紋理座標是奇數時使用映象平鋪
            //GL_CLAMP_TO_EDGE: 座標超出部分被擷取成0、1,邊緣拉伸

            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.glBindTexture(GLES20.GL_TEXTURE_2D,0);
        }

    }
}

三 Demo

OpenGL_Ray