CSDN视频教程
OpenGL教程
本篇文章主要是视频教程的记录笔记,一方面会记录实现的过程,另一方面也会查漏补缺,对于一些视频教程一笔带过但比较重要的地方分析总结。
一、OpenGL ES 示例及GLSurfaceView源码分析
自定义GLSurfaceView及源码分析
自定义GLSurfaceView只需要三步:
①继承GLSurfaceView
②实现GLSurfaceView.Renderer接口
③编写glsl脚本(shader)
GLSurfaceView源码分析:
GLSurfaceView源码分析
分析GLSurfaceView源码通常从setRenderer入手:

主要就做了两件事:
①检查环境和变量同步配置
这里的
checkRenderThreadState就是检查mRenderer是否存在,如果存在,则直接抛出异常,意思就是我们的GLSurfaceView只能调用一次setRenderer,多次调用的话就会挂掉。②启动一个GL线程
GLThread就是我们平常说的GL线程,他就是用于跟OpenGL ES环境进行交互的线程。

在GLThread的#run方法中调用的#guardedRun方法就是GLThread的核心逻辑所在。

创建EglHelper,这个类是创建EGL环境的关键工具,我们后面需要仿照这个类写我们自己的EglHelper。

在其#start方法中获取到了EGL的实例,然后获得了默认的窗口Display

获取到配置后,创建EGL的上下文环境mEglContext。
接着回到#guardedRun方法中:

可以看到开启了一个死循环,因为绘制是一直进行的,这点不难理解。

往下走可以看到,当创建完Egl上下文环境后,会调用renderer中的onSurfaceCreated方法,告诉上层EGL上下文环境已经创建完毕

紧接着判断size是否改变,如果改变,调用renderer的onSizeChanged方法。
总结
1.GLThread:OpenGL ES的运行线程。包含创建EGL环境、调用GLRender的onSurfaceCreated、onSurfaceChanged和onDrawFrame方法及生命周期的管理。
2.EglHelper:负责创建EGL环境。
3.GLSurfaceView:负责Surface和状态改变。
整体流程图如下:

二、EGL环境创建
EGL:是OpenGL ES和本地窗口系统的接口,不同平台上EGL配置是不一样的,而OpenGL的调用方式是一致的,就是说:OpenGL跨平台就是依赖于EGL接口。(不同的平台分别配置不同的环境,跨平台能力通过统一api实现)
为什么要自己创建EGL环境?
当我们需要把同一个场景渲染到不同的Surface上时,此时系统GLSurfaceView就不能满足需求了,所以我们需要自己创建EGL环境来实现渲染操作。
注意:OpenGL整体是一个状态机,通过改变状态就能改变后续的渲染方式,而EGLContext就保存了所有状态,因此可以通过共享EGLContext来实现同一场景渲染到不同的Surface上。(直播视频贴纸需要)
创建自己的EglHelper
主要根据GLSurfaceView源码来创建。
①得到Egl实例
②得到默认的显示设备(窗口 EglDisplay)
③初始化默认显示设备
④设置显示设备的属性
⑤从系统中获取对应属性的配置
⑥创建EglContext
⑦创建渲染的Surface(EGLSurface)
⑧绑定EglContext和Surface到显示设备中
⑨刷新数据,显示渲染场景
实例:
public class EglHelper {
private EGL10 mEgl;
private EGLDisplay mEglDisplay;
private EGLContext mEglContext;
private EGLSurface mEglSurface;
public void initEgl(Surface surface, EGLContext eglContext) {
//1.
mEgl = (EGL10) EGLContext.getEGL();
//2.
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed");
}
//3.
int[] version = new int[2];
if (!mEgl.eglInitialize(mEglDisplay, version)) {
throw new RuntimeException("eglInitialize failed");
}
//4.
int[] attributes = new int[]{
EGL10.EGL_RED_SIZE, 8,//深度8位
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_ALPHA_SIZE, 8,
EGL10.EGL_DEPTH_SIZE, 8,
EGL10.EGL_STENCIL_SIZE, 8,
EGL10.EGL_RENDERABLE_TYPE, 4,//固定深度4位,必传参数!
EGL10.EGL_NONE//标记属性获取完毕
};
//下面都是从GLSurfaceView源码中获取到的
int[] num_config = new int[1];
if (!mEgl.eglChooseConfig(mEglDisplay, attributes, null, 1,
num_config)) {//拿到系统默认配置
throw new IllegalArgumentException("eglChooseConfig failed");
}
int numConfigs = num_config[0];
if (numConfigs <= 0) {
throw new IllegalArgumentException(
"No configs match configSpec");
}
//5.
EGLConfig[] configs = new EGLConfig[numConfigs];
if (!mEgl.eglChooseConfig(mEglDisplay, attributes, configs, numConfigs,
num_config)) {
throw new IllegalArgumentException("eglChooseConfig#2 failed");
}
//6.
if (eglContext != null) {//不为空则共享
mEglContext = mEgl.eglCreateContext(mEglDisplay, configs[0], eglContext/*共享context*/, null);
} else {
mEglContext = mEgl.eglCreateContext(mEglDisplay, configs[0], EGL10.EGL_NO_CONTEXT, null);
}
//7.
mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, configs[0], surface, null);
//8.
if (!mEgl.eglMakeCurrent(mEglDisplay
, mEglSurface, mEglSurface, mEglContext)) {
throw new RuntimeException("eglMakeCurrent failed");
}
}
//9.
public boolean swapBuffers() {
if (mEgl != null) {
return mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
}
return false;
}
public EGLContext getEglContext() {
return mEglContext;
}
public void destroyEgl() {//仿照源码
if (mEgl == null) return;
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_CONTEXT);
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEglSurface = null;
mEgl.eglDestroyContext(mEglDisplay, mEglContext);
mEglContext = null;
mEgl.eglTerminate(mEglDisplay);
mEglDisplay = null;
mEgl = null;
}
}
需要注意的是,此类的实现思路基本都是参考GLSurfaceView中EglHelper的实现,所以方法调用基本都能找到原型。
这里提一些点:
①swapBuffers:GLSurfaceView用的是双缓冲技术,swapBuffers其实就是交换缓冲区啦,所有的绘制都是绘制到一个后台的缓冲区里面的,如果不交换缓冲区,就看不到绘制的东西了
②glFlush:强制刷新,OPENGL是使用一条渲染管线线性处理命令的,一般情况下,我们提交给OPENGL的指令并不是马上送到驱动程序里执行的,而是放到一个缓冲区里面,等这个缓冲区满了再一次过发到驱动程序里执行;很多时候只有几条指令是填充不满那个缓冲区的,这就是说这些指令根本没有被发送到驱动里,所以我们要调用glFlush来强制把这些指令送到驱动里进行处理。
③共享纹理等是通过共享上下文即EGLContext来实现的
三、自定义GLSurfaceView
为什么要自定义GLSurfaceView?
①共享EGLContext,假如需要共享纹理,GLSurfaceView没有提供此方法
②通过EGLThread,可以构造出不同种类的GLSurfaceView,比如照相机界面,上面是主渲染,下面有一排滤镜的画面,这就需要共享纹理来实现,使用EGLThread就能构造出自己想要的效果
自定义GLSurfaceView步骤:
1.继承SurfaceView,并实现其Callback回调
2.自定义GLThread线程类,主要用于OpenGL的绘制操作
3.添加设置Surface和EglContext的方法
4.提供和系统GLSurfaceView相同的调用方法
自定义GLThread的作用:
这个类主要负责开启一个线程然后根据外部的生命周期调用EglHelper完成egl的环境搭建,并且和外部交互。
实现步骤
①继承SurfaceView,实现SurfaceHolder.Callback三个回调(surfaceCreated、sufaceChanged、surfaceDestroyed)


②自定义GLThread,初始化EglHelper,创建Egl环境;注意之前说过GLSurfaceView持有GLThread的实例,GLThread又持有GLSurfaceView的引用;这样GLSurfaceView可以用GLThread来执行渲染操作,GLThread也可以将回调传回给GLSurfaceView。

③在GLThread中开启死循环,根据设置的渲染模式(手动刷新,自动刷新),开启绘制操作,并将其回调传递给业务层;

特别需要注意的是#onDrawFrame回调,第一次需要手动调用;同时需要交换缓冲区(swapBuffers)!否则也会绘制失败!

④在surfaceCreated、surfaceChanged和surfaceDestroyed中设置EglThread相关操作。
主要是初始化EglThread并启动,设置EglThread的相关属性,销毁EglThread;
⑤封装api,将GLThread的方法暴露给自定义的GLSurfaceView,因为外界不跟EglThread打交道。支持外部设置Surface、EglContext、RenderMode、GLRender等;
⑥使用
新建子类继承抽象CustomEglSurfaceView类,在其构造方法中传入GLRenderer,用来监听每一帧回调;

然后在onSurfaceChanged的时候设置画布大小;在onDrawFrame中绘制每一帧;
四、渲染图片纹理(一)
渲染图片纹理步骤:
①编写着色器(顶点着色器和片元着色器)
②设置顶点、纹理坐标
③加载着色器
④创建纹理
⑤渲染图片
OpenGL ES坐标系:

顶点坐标系和纹理坐标系的范围还是有区别的。
【Tips!!】
OpenGL绘制的基本元素:点、直线、三角形。
所以四边形只能由两个三角形拼接而成。
四个点组合的可能性很多,必须遵循一定规则!

①要组成四方形
②方向要一致
来看几个错误案例:

所以绘制的图形,上面三角的地方肯定是绘制不出来的。

可以绘制出来,但是本来期望是竖屏的,会变成横屏的;(通过纹理可以看出来)
(如果是用GL_TRIANGLES绘制,必须得指定六个点,四个点的不行,组不出来:
v1,v2,v3和v3,v2,v4这种组合。)
OPENGL_三角形带GL_TRIANGLE_STRIP详解
【GL_TRIANGLE_STRIP】的绘制原理:
偶数:n-1,n-2,n
奇数:n-2,n-1,n
//TODO!!!!!!!,难道只有这一种组合方式??

顶点内存数组:

Shader编写:
顶点着色器

片元着色器

OpenGL ES加载Shader
1.创建Shader(着色器:顶点或片元)
int shader = GLES20.glCreateShader(shaderType);
2.加载Shader源码并编译shader
GLES20.glShaderSource(shader,source);
GLES20.glCompileShader(shader);
3.检查是否编译成功
GLES20.glGetShaderiv(shader,GLES20.GLES20.GL_COMPILE_STATUS,compiled,0);
4.创建一个渲染程序:
int program = GLES20.glCreateProgram();
5.将着色器程序添加到渲染程序中:
GLES20.glAttachShader(program,vertexShader);
6.链接源程序:
GLES20.glLinkProgram(program);
这些步骤都很通用;
7.检查链接程序是否成功
GLES20.glGetProgramiv(program,GLES20.GL_LINK_STATUS,linkStatus,0);
8.获取着色器中属性
int vPosition = GLES20.glGetAttribLocation(program,"v_Position");
9.使用源程序
GLES20.glUseProgram(program);
10.使顶点属性数数组有效
GLES20.glEnableVertexAttribArray(vPosition);
11.为顶点属性赋值
GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,8,vertexBuffer);
12.绘制图形
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
五、纹理渲染(二)
真正开始渲染纹理。
OpenGL ES绘制纹理过程
1.加载shader和生成program过程不变
2.创建和绑定纹理:
GLES20.glGenTextures(1,textureId,0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);
3.设置环绕和过滤方式
环绕(图片超过纹理坐标范围或小于纹理坐标范围)下面的S代表x轴,T代表Y轴;
GLES20.glTextParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_LINEAR);
GLES20.glTextParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_LINEAR);
过滤(纹理像素映射到坐标点)
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);
4.设置图片(bitmap)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,bitmap,0);
5.绑定顶点坐标和纹理坐标
6.绘制图形
一、首先封装几个操作:
①加载着色器
public static String readTextFileFromResource(Context context, int resourceId){
StringBuilder body = new StringBuilder();
try {
InputStream inputStream = context.getResources().openRawResource(resourceId);
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String nextLine;
while( (nextLine = bufferedReader.readLine()) != null ){
body.append(nextLine);
body.append('\n');
}
} catch (Exception e) {
e.printStackTrace();
}
return body.toString();
}
②创建、加载、编译着色器
GLES20.glCreateShader(type)
GLES20.glShaderSource(shaderObjectId, shaderCode);
GLES20.glCompileShader(shaderObjectId);
GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
③链接着色器到OpenGL上
GLES20.glCreateProgram();
GLES20.glLinkProgram(programObjectId);
GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);
创建顶点着色器、片元着色器
顶点着色器:
attribute vec4 v_Position;//顶点坐标
attribute vec2 f_Position;//纹理坐标
varying vec2 ft_Position; //纹理坐标传递给片元着色器
void main() {
ft_Position = f_Position;
gl_Position = v_Position;
}
片元着色器:
precision mediump float;
varying vec2 ft_Position;
uniform sampler2D sTexture; //sampler2D 是GLES 内置的取样器
void main() {
gl_FragColor = texture2D(sTexture, ft_Position); //texture2D是内置函数,用于2D纹理取样
}
自定义GLSurfaceView、Render
1.实现类
public class GLTextureView extends CustomEglSurfaceView {
public GLTextureView(Context context) {
this(context,null);
}
public GLTextureView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public GLTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setGLRender(new TextureRender(context));
}
}
public class TextureRender implements CustomEglSurfaceView.CustomGLRender {
private Context mContext;//用来加载着色器
public TextureRender(Context context){
this.mContext = context;
}
@Override
public void onSurfaceCreated() {
}
@Override
public void onSurfaceChanged(int width, int height) {
}
@Override
public void onDrawFrame() {
}
}
主要操作都在我们自定义的Render中。
2.确定顶点坐标的范围、为顶点坐标分配本地内存地址

//顶点着色器中 绘制的坐标范围
private float[] vertexData = {
-1f, -1f,
1f, -1f,
-1f, 1f, //前三个就是一个三角形
1f, 1f //最后差一个点;
};
//为坐标分配本地内存地址(OpenGL没有虚拟机,直接绘制在本地内存中)
private FloatBuffer vertexBuffer;
public TextureRender(Context context) {
this.mContext = context;
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length*4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.put(0);
}
3.确定纹理坐标范围,为纹理坐标分配本地内存地址

//顶点坐标范围
private float[] vertexData = {
-1f, -1f,
1f, -1f,
-1f, 1f, //前三个就是一个三角形
1f, 1f //最后差一个点;
};
//纹理坐标范围
private float[] fragmentData = { //点要和顶点坐标对应上!【顶点坐标范围为-1~1,纹理坐标范围为0~1】
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
};
//为坐标分配本地内存地址(OpenGL没有虚拟机,直接绘制在本地内存中)
private FloatBuffer vertexBuffer;
private FloatBuffer fragmentBuffer;
public TextureRender(Context context) {
this.mContext = context;
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.put(0);
fragmentBuffer = ByteBuffer.allocateDirect(fragmentData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(fragmentData);
fragmentBuffer.put(0);
}
可以看到顶点坐标和纹理坐标处理如出一辙;
4.顶点着色器属性操作

【5.绘制纹理】【重点】

在onSurfaceCreated中开始生成纹理、绑定并激活纹理、加载纹理图片:
①生成纹理
int[] textureIds = new int[1];
GLES20.glGenTextures(1,textureIds,0);
textureId = textureIds[0];
同时将纹理id保存下来,以后再操作纹理,都是通过这个id来操作的。方便我们在每一帧绘制的时候绑定此纹理。
②绑定并激活纹理
//绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glUniform1i(sampler2D,0);
③设置纹理环绕方式
即纹理小于画布或超出画布范围时纹理的显示样式
//设置纹理环绕方式
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);
④加载纹理图片
(纹理和图片如何绑定的????)
private void loadBitmap() {
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.mipmap.my2);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,bitmap,0);
bitmap.recycle();
bitmap = null;
}
⑤在每一帧绘制的时候绑定和解绑纹理

【OpenGL纹理详解】
OpenGL API
OpenGL纹理
先看一下纹理相关Api

这里着重提两点:
①GLES20.glActiveTexture:
一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
纹理单元GL_TEXTURE0默认总是被激活,所以我们在前面的例子里当我们使用glBindTexture的时候,无需激活任何纹理单元。
OpenGL-VBO(顶点缓冲对象)
VBO(Vertex Buffer Object)
OpenGL-VBO
为什么要用VBO呢?
如果我们不使用VBO,每次在onDrawFrame采用GLES20.glDrawArrays绘制图形时都是从本地内存处获取顶点数据然后传输给OpenGL来绘制,这样就会频繁的操作CPU->GPU,增大了开销,降低了渲染效率。
使用VBO,就能把顶点数据缓存到GPU开辟的一段内存中,然后使用时不必再从本地获取,而是直接从显存中获取,这样就能提升绘制的效率。
创建VBO

1.创建VBO
GLES20.glGenBuffers(1,vbos,0);
2.绑定VBO
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,vbos[0]);
3.分配VBO需要的缓存大小
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,vertex.length4,null,GLES20.GL_STATIC_DRAW);
4.为VBO设置顶点数据的值
GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER,0,vertexData,length4,vertexBuffer);
5.解绑VBO
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,0);
使用VBO
1.绑定VBO
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,vbos[0]);
2.设置顶点数据
GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,8,0);
3.解绑VBO
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,0);



七、OpenGL-FBO(帧缓冲对象)
FBO: Frame Buffer Object
为什么要用FBO?
当我们需要对纹理进行多次渲染采样时(比如美颜、模糊等),而这些渲染采样是不需要展示给用户看的,这时我们就可以用一个单独的缓冲对象(离屏渲染)来存储我们这几次渲染采样的结果,等处理完后才显示到窗口上。
好处:
提高渲染效率,避免闪屏,可以很方便的实现纹理共享等。
渲染方式:
渲染到缓冲区: 深度测试和模板测试(主要是3D场景使用)
渲染到纹理:图像渲染
FBO工作流程

创建FBO
1.创建FBO
GLES20.glGenBuffers(1,fbos,0);
2.绑定FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,fbos[0]);
3.设置FBO分配内存大小
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_RGBA,720,1280,0,GLES20.GL_RGBA,GLES20.GL_UNSIGNED_BYTE,null);
4.把纹理绑定到FBO
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,GLES20.GL_COLOR_ATTACHHMENT0,GLES20.GL_TEXTURE_2D,textureid,0);
5.检查FBO是否绑定成功
GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)!=GLES20.GL_FRAMEBUFFER_COMPLETE
6.解绑FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
使用FBO
1.绑定FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,fbos[0]);
2.获取需要绘制的图片纹理,然后绘制渲染
3.解绑FBO
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
4.再把绑定到FBO的纹理绘制渲染出来
Tips:

顶点坐标系、纹理坐标系和FBO坐标系不同,如果将图片按照原有的方式绘制到FBO当中,会发现图片按照x轴翻转了。
所以需要替换掉原有的坐标系,即fragmentData:

demo:
创建FBO:
package com.rye.opengl.course_y.other.render;
import android.content.Context;
import android.opengl.GLES20;
import com.rye.opengl.R;
import com.rye.opengl.hockey.ShaderHelper;
import com.rye.opengl.hockey.TextureResourceReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
/**
* Create by [Rye]
* <p>
* at 2022/6/3 7:44 下午
*/
public class FBORender {
private Context mContext;//用来加载着色器
private int program;
//顶点着色器属性
private int vPosition; //顶点坐标
private int fPosition; //纹理坐标
private int sampler2D;
//VBO缓存
private int vboId;
//顶点坐标范围
private float[] vertexData = {
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
//纹理坐标范围
private float[] fragmentData = { //点要和顶点坐标对应上!【顶点坐标范围为-1~1,纹理坐标范围为0~1】
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
};
//为坐标分配本地内存地址(OpenGL没有虚拟机,直接绘制在本地内存中)
private FloatBuffer vertexBuffer;
//为纹理坐标分配本地内存地址
private FloatBuffer fragmentBuffer;
public FBORender(Context context) {
this.mContext = context;
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
fragmentBuffer = ByteBuffer.allocateDirect(fragmentData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(fragmentData);
fragmentBuffer.position(0);
}
public void onCreate() {
String vertexSource = TextureResourceReader.readTextFileFromResource(mContext, R.raw.course_demo1_vertex_shader);
String fragmentSource = TextureResourceReader.readTextFileFromResource(mContext, R.raw.course_demo1_fragment_shader);
program = ShaderHelper.buildProgram(vertexSource, fragmentSource);//创建program并绑定着色器
//获取顶点着色器属性
vPosition = GLES20.glGetAttribLocation(program, "v_Position");
fPosition = GLES20.glGetAttribLocation(program, "f_Position");
//纹理
sampler2D = GLES20.glGetUniformLocation(program, "sTexture");
//----------VBO
int[] vbos = new int[1];
GLES20.glGenBuffers(1, vbos, 0);
vboId = vbos[0];
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId);
//将顶点着色器和片元着色器大小统一放到此VBO中,开启GPU内存(显存)
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexData.length * 4 + fragmentData.length * 4, null,
GLES20.GL_STATIC_DRAW);
GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0, vertexData.length * 4, vertexBuffer);
GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, vertexData.length * 4, fragmentData.length * 4, fragmentBuffer);
GLES20.glBindTexture(GLES20.GL_ARRAY_BUFFER, 0);
}
public void onChange(int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
public void onDraw(int textureId) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
GLES20.glUseProgram(program);
//绑定纹理,不绑定也绘制不出来纹理呐
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);//替换为source texture
//绑定VBO
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId);
//使顶点生效
GLES20.glEnableVertexAttribArray(vPosition);
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8/*一个位置占4位,说明用两个数据代表一个点*/,
0);//去掉vertexBuffer,buffer从CPU中获取,效率低下,应该使用VBO,减少CPU到GPU的开销;
GLES20.glEnableVertexAttribArray(fPosition);
GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8/*一个位置占4位,说明用两个数据代表一个点*/,
vertexData.length * 4);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);//这里可以改成3个试试,会有奇特现象;
//纹理解绑,去掉也可以渲染出来
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
//单个纹理可以不解绑,多个纹理必须解绑,否则会渲染之前纹理的图片
GLES20.glBindTexture(GLES20.GL_ARRAY_BUFFER, 0);
}
}
然后就是在原有的渲染器中使用FBO:
①跟VBO创建方式类似,在onSurfaceCreated中创建fboId:

②设置FBO缓存大小,将纹理绑定到FBO

③在onSurfaceCreated、onSurfaceChanged、onDrawFrame中调用FBORender中我们自定义的生命周期



④在onDrawFrame中绑定FBO/解绑FBO

⑤正确执行完上面内容,图片可以绘制出来,但图片是上下颠倒的,原因如下:
【纹理坐标系和FBO坐标系不同】

按照之前绘制纹理的方法,就会导致图片倒过来,解决的方法,就是按照图示,将原先纹理坐标的位置替换为FBO的位置。


目前两个Render都在同一个EGL环境里,在同一个EGL线程里;后面我们需要把EglContext拿出来实现共享。
八、正交投影
现存的问题:

解决方法就是利用正交投影归一化坐标。

具体操作步骤:
1、顶点着色器中添加矩阵
attribute vec4 v_Position;
attribute vec2 f_Position;
attribute vec2 ft_Position;
uniform mat4 u_Matrix;
void main(){
ft_Position = f_Position;
gl_Position = v_Position*u_Matrix;
}
2.然后根据图形宽高和屏幕宽高计算长度
Matrix.orthoM(~)
3.使用正交投影
GLES20.glUniformMatrix4fv(uMatrix,false,matrix,0);
具体操作:
①拷贝一份顶点着色器,新增投影矩阵所需的属性;
attribute vec4 v_Position;//顶点坐标
attribute vec2 f_Position;//纹理坐标
varying vec2 ft_Position; //纹理坐标传递给片元着色器
uniform mat4 u_Matrix; //投影矩阵
void main() {
ft_Position = f_Position;
gl_Position = v_Position*u_Matrix;
}
②在onSurfaceChanged中设置纹理位置
@Override
public void onSurfaceChanged(int width, int height) {
GLES20.glViewport(0, 0, width, height);
fboRender.onChange(width, height);
//操作投影矩阵
if (width > height) {//横屏
//TODO 数值要根据图片尺寸来,不能写死,//左负右正
Matrix.orthoM(matrix, 0, -width / ((height / 1243f) * 700f), width / ((height / 1243f) * 700f), -1f, 1f, -1f, 1f); // height/1080 算出图片拉伸的比例,700是图片宽度
} else {
Matrix.orthoM(matrix, 0, -1, 1, -height / ((width / 700f) * 1243f), height / ((width / 700f) * 1243f), -1f, 1f); // height/1080 算出图片拉伸的比例,700是图片宽度
}
}
③在glUseProgram后使用正交投影

矩阵旋转
主要用于纹理角度旋转,可以操作x,y、z方向上的旋转。
比如之前FBO坐标系和纹理坐标系原点不同导致图片翻转,那么我们就可以用旋转方法纠正,api如下:
Matrix.rotateM(matrix,o,a,x,y,z);
a: 角度,范围在-360~+360之间
正数:逆时针旋转
负数:顺时针旋转
x,y,z: 分别表示相应坐标轴
调用api的地方和正交投影一样,都在onSurfaceChanged中调用:


多Surface渲染同一纹理
①首先利用离屏渲染把图像渲染到纹理texture中
②通过共享EGLContext和texture,实现纹理共享
③然后在新的Render里面把可以对texture进行新的滤镜操作
实际操作要注意的思路
①渲染到多Surface,实际上就是渲染到多个SurfaceView当中,所以我们需要新建多个SurfaceView
②这些SurfaceView又各自对应自己Render,这些Render就可以参考我们的主Render,当然也有所区别:
不用自己加载纹理,拿到纹理id即可;
也不需要使用FBO,因为我们在主Render中已经将纹理绘制到FBO当中了。
③这些SurfaceView是需要使用主SurfaceView的上下文环境的,即Context,通过统一的上下文环境共享纹理。
④纹理传递的时机就在于在主Render中创建好纹理后,通过回调返回textureId,然后就可以初始化多个SurfaceView。
⑤多个Surface的Render的生命周期不需要像FBO那样回调了,就是直接执行了。
单Surface渲染多纹理
原理:
主要利用OpenGL ES绘制多次,把不同的纹理绘制到纹理或窗口上。
实际操作:
只需要改变OpenGL ES从顶点数组 开始取点的位置就可以了。
1.vertexBuffer.position(index)
2.GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,8,index);
index:内存中起始位置(字节)
demo:
①新增顶点坐标:

②加载新的纹理图

③在onDrawFrame中绘制两次纹理,当然两个是不同的图


效果图:

跟我们设置的顶点坐标正好对应起来:

在单Surface上渲染多纹理后,还可以将混合后的纹理渲染到多Surface上,操作流程同上一张的多Surface渲染同一纹理。
OpenGL ES 预览摄像头画面
原理:
利用OpenGL生成纹理并绑定到SurfaceTexture,然后把camera的预览数据设置显示到SurfaceTexture中,这样就可以在OpenGL中拿到摄像头数据并显示了。
(SurfaceTexture构造函数中有个参数是textureId,这就是其与OpenGL的桥梁)
具体流程
1.着色器操作:

①可以看到首先是扩展头不同,要使用OES的扩展头
②扩展纹理由sampler2D换为samplerExternalOES
③绑定的纹理也是扩展的OES纹理
拿到camera的纹理id后,将其传入SurfaceTexture的构造函数中去即可。

接着通过回调,将SurfaceTexture传递给Camera,通过Camera来预览内容;

摄像头
矩阵调整摄像头画面方向
原理:获取activity的方向,然后利用矩阵旋转和翻转来纠正摄像头方向
核心api:Matrix.rotateM(matrix,o,a,x,y,z);


具体操作流程
1.顶点着色器中新增变换矩阵(uniform mat4 u_Matrix;)
2.设置activity的清单文件属性(configChanges),保证屏幕旋转时activity不会被重新创建
3.获取当前activity的旋转方向(getDefaultDisplay.getRotation),并通过Matrix.rotateM动态调整OpenGL画面的方向
4.在activity的onConfigureChanged中调用第3步的方法。
(4.同时调整当前预览的尺寸为屏幕的宽高,不再限制为固定值。)
视频编码录制
原理:
得到Mediacodec的输入Surface,然后OpenGL把摄像头数据渲染到这个Surface中,Mediacodec就可以拿到摄像头数据进行编码。
实现流程:

实现关键点:

第4步就是把h264视频和aac音频合成mp4视频流
添加文字水印
原理:
把文字变成图片,用多纹理渲染的方式来添加。其他图片水印直接用多纹理渲染即可。
绘制水印流程:
1.确认水印添加的位置
2.设置水印的高度
3.根据水印高度和水印图片长宽比计算水印的顶点坐标
Tips:需要绘制在FBORender中,因为其实普通纹理:sampler2D;
而CameraRender中是samplerExternalOES,绘制水印会无效;(原因TODO)
在FBO中添加的水印只是预览的,录制的Render中是没有的,所以录制的Render也需要添加一样的代码,可以抽离;
摄像头视频和MP3音频合成
原理:
编码视频的同时,再编码音频,最后合成一个完整的视频。
这里的音频可以来自麦克风也可以来自第三方库提供的PCM数据。
音频编码demo:
其原理也是将pcm数据放入ByteBuffer入队列编码。
计算播放时间
PCM播放时间 = PCM实际大小/PCM单位大小(字节)
如:采样率:44100 位数:16biy 声道数:2
有4096个采样,其播放时间为:
t =(409622)/(4410022) //0.0x秒
播放yuv数据
原理:和渲染一般纹理原理相同。
区别:Y、U、V纹理需要分别渲染。
Tips:
1.可使用VLC播放器播放YUV视频。(必须注意其中的宽高要换成yuv原始视频数据的宽高!)https://codeantenna.com/a/UhWcllVIWG
/Applications/VLC.app/Contents/MacOS/VLC --demux rawvideo --rawvid-fps 15 --rawvid-width 320 --rawvid-height 180 --rawvid-chroma I420 /Users/domain/Desktop/test_yuv420p_320x180.yuv
步骤:
1.创建Render及顶点、片段着色器
2.片段着色器中需要三个sampler2D,因为我们需要将y、u、v分别渲染到三个纹理当中

3.在onSurfaceCreated中新建对应的三个纹理并进行相关设置,如绑定方式等

4.初始化FBO相关设置
5.在onDrawFrame中通过active各自纹理渲染yuv三个通道数据(yuv数据由外部传入),最后别忘了清空yuv的buffer数据

6.将纹理绘制FBO中再渲染

7.新建自定义SurfaceView,设置render为上面自定义的Render。同时作为桥梁提供设置每一帧yuv数据的口子

8.通过io操作读入yuv数据,将其传入到render中持续渲染。(这里u、v的byte数组要除4是因为这里用的是422的采样率,I420,NV12,NV21都是442的采样率,w*h就是4倍了)

9.还有一点就是这里用了FBO,纹理坐标和FBO坐标系不同,所以这里要用矩阵翻转(否则你会看到视频沿着x轴翻转了180度)。
①在yuv的顶点着色器中添加matrix(FBO的顶点着色器中不要加!)

②在render中初始化矩阵

③然后再渲染纹理的时候使用矩阵翻转即可
