NDK51_OpenGL:FBO
文章目錄
一 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
來進行判斷,但這個判斷稍顯麻煩,所以這裡我選擇使用:
從攝像頭使用的紋理首先繪製到
CameraFilter
的FBO中,這樣無論是否開啟效果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);
}
}
}