Android视频编码及推流教程

CSDN视频教程
OpenGL教程
本篇文章主要是视频教程的记录笔记,一方面会记录实现的过程,另一方面也会查漏补缺,对于一些视频教程一笔带过但比较重要的地方分析总结。

一、OpenGL ES 示例及GLSurfaceView源码分析

自定义GLSurfaceView及源码分析

自定义GLSurfaceView只需要三步:
①继承GLSurfaceView
②实现GLSurfaceView.Renderer接口
③编写glsl脚本(shader)

GLSurfaceView源码分析:
GLSurfaceView源码分析
分析GLSurfaceView源码通常从setRenderer入手:

image.png

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

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

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

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

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

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

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

image.png

紧接着判断size是否改变,如果改变,调用renderer的onSizeChanged方法。

总结

1.GLThread:OpenGL ES的运行线程。包含创建EGL环境、调用GLRender的onSurfaceCreated、onSurfaceChanged和onDrawFrame方法及生命周期的管理。
2.EglHelper:负责创建EGL环境。
3.GLSurfaceView:负责Surface和状态改变。
整体流程图如下:


image.png

二、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

Android自定义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)


image.png

image.png

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


image.png

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


image.png

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

④在surfaceCreated、surfaceChanged和surfaceDestroyed中设置EglThread相关操作。
主要是初始化EglThread并启动,设置EglThread的相关属性,销毁EglThread;
⑤封装api,将GLThread的方法暴露给自定义的GLSurfaceView,因为外界不跟EglThread打交道。支持外部设置Surface、EglContext、RenderMode、GLRender等;

⑥使用
新建子类继承抽象CustomEglSurfaceView类,在其构造方法中传入GLRenderer,用来监听每一帧回调;


image.png

然后在onSurfaceChanged的时候设置画布大小;在onDrawFrame中绘制每一帧;

四、渲染图片纹理(一)

渲染图片纹理步骤:
①编写着色器(顶点着色器和片元着色器)
②设置顶点、纹理坐标
③加载着色器
④创建纹理
⑤渲染图片

OpenGL ES坐标系:


image.png

顶点坐标系和纹理坐标系的范围还是有区别的。

【Tips!!】
OpenGL绘制的基本元素:点、直线、三角形。
所以四边形只能由两个三角形拼接而成。
四个点组合的可能性很多,必须遵循一定规则!

image.png

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

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

可以绘制出来,但是本来期望是竖屏的,会变成横屏的;(通过纹理可以看出来)
(如果是用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!!!!!!!,难道只有这一种组合方式??
image.png

顶点内存数组:


image.png

Shader编写:
顶点着色器


image.png

片元着色器


image.png
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.确定顶点坐标的范围、为顶点坐标分配本地内存地址


image.png
   //顶点着色器中 绘制的坐标范围
    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.确定纹理坐标范围,为纹理坐标分配本地内存地址


image.png
    //顶点坐标范围
    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.顶点着色器属性操作

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

在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;
    }

⑤在每一帧绘制的时候绑定和解绑纹理


image.png

【OpenGL纹理详解】

OpenGL API
OpenGL纹理
先看一下纹理相关Api

image.png

这里着重提两点:
①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

image.png

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,length
4,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);

image.png

image.png
image.png

七、OpenGL-FBO(帧缓冲对象)

FBO离屏渲染

FBO: Frame Buffer Object

为什么要用FBO?
当我们需要对纹理进行多次渲染采样时(比如美颜、模糊等),而这些渲染采样是不需要展示给用户看的,这时我们就可以用一个单独的缓冲对象(离屏渲染)来存储我们这几次渲染采样的结果,等处理完后才显示到窗口上。

好处:
提高渲染效率,避免闪屏,可以很方便的实现纹理共享等。

渲染方式:
渲染到缓冲区: 深度测试和模板测试(主要是3D场景使用)
渲染到纹理:图像渲染

FBO工作流程

image.png

创建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:


image.png

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


image.png

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:

image.png

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

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

image.png

image.png

④在onDrawFrame中绑定FBO/解绑FBO
image.png

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

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


image.png

image.png

目前两个Render都在同一个EGL环境里,在同一个EGL线程里;后面我们需要把EglContext拿出来实现共享。

八、正交投影

现存的问题:


image.png

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


image.png

OpenGL ES正交投影

具体操作步骤:
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后使用正交投影


image.png

矩阵旋转

主要用于纹理角度旋转,可以操作x,y、z方向上的旋转。
比如之前FBO坐标系和纹理坐标系原点不同导致图片翻转,那么我们就可以用旋转方法纠正,api如下:

Matrix.rotateM(matrix,o,a,x,y,z);

a: 角度,范围在-360~+360之间
正数:逆时针旋转
负数:顺时针旋转
x,y,z: 分别表示相应坐标轴

调用api的地方和正交投影一样,都在onSurfaceChanged中调用:


image.png
image.png

多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:
①新增顶点坐标:


image.png

②加载新的纹理图


image.png

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

image.png

效果图:


image.png

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


image.png

在单Surface上渲染多纹理后,还可以将混合后的纹理渲染到多Surface上,操作流程同上一张的多Surface渲染同一纹理。

OpenGL ES 预览摄像头画面

原理:
利用OpenGL生成纹理并绑定到SurfaceTexture,然后把camera的预览数据设置显示到SurfaceTexture中,这样就可以在OpenGL中拿到摄像头数据并显示了。
(SurfaceTexture构造函数中有个参数是textureId,这就是其与OpenGL的桥梁)

具体流程

1.着色器操作:


image.png

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


image.png

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

摄像头

矩阵调整摄像头画面方向

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

image.png

image.png

具体操作流程

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就可以拿到摄像头数据进行编码。
实现流程:


image.png

实现关键点:


image.png

第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分别渲染到三个纹理当中


image.png

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


image.png

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

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


image.png

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

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

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

②在render中初始化矩阵
image.png

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


image.png
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容