文章目录
一 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);
}
}
}