初识OpenGL

130 阅读14分钟

前面介绍了音视频的编解码,我们知道视频解码得到YUV数据,然后把数据写入到Surface,最终渲染到SurfaceView,展示出画面,如果我们在视频渲染的基础上,添加一些特效,如滤镜、分屏、加水印,贴纸等,我们怎么做呢?

答案是通过OpenGL来完成。首先我们来了解一下OpenGL是什么,有哪些功能?

OpenGL是什么,有哪些功能?

OpenGL(Open Graphics Library)是一种跨平台的图形编程接口,用于渲染2D和3D图形。它提供了一系列的函数和命令,允许开发者利用图形硬件加速来创建高性能的图形应用程序。

OpenGL 的主要作用包括:

  1. 图形渲染:OpenGL 提供了一套强大的图形渲染功能,可以绘制各种几何图形、曲线、文本等,并对其进行变换、着色、纹理映射等操作。开发者可以利用这些功能来创建各种视觉效果,并实现逼真的图形渲染。

  2. 3D图形:OpenGL 是一种非常流行的 3D 图形编程接口,它支持三维模型的建模、变换、渲染等操作。通过 OpenGL,开发者可以创建逼真的三维场景,实现立体感和真实感效果。

  3. 跨平台支持:OpenGL 是一个跨平台的图形编程接口,可以在不同的操作系统上运行,如 Windows、MacOS、Linux等。这使得开发者可以使用相同的代码编写图形应用程序,并在不同的平台上进行部署和运行。

  4. 硬件加速:OpenGL 可以利用计算机的图形硬件加速来执行图形渲染任务,这样可以获得更高的渲染性能。通过与图形硬件的紧密集成,OpenGL 可以利用并行计算和专门的图形处理单元(GPU)来加速图形计算和渲染操作。

  5. 游戏开发:由于其强大的图形渲染功能和跨平台支持,OpenGL 在游戏开发领域得到广泛应用。许多游戏引擎和图形库都基于 OpenGL 构建,开发者可以利用 OpenGL 创建各种类型的游戏,包括2D游戏和3D游戏。

接下来看看OpenGL 是如何实现图形渲染的,弄清楚渲染的流程后,才可以更好的使用OpenGL来绘制各种图像。

OpenGl的图形渲染管线

OpenGL实现图形渲染是通过OpenGl的图形渲染管线来完成的。

OpenGl的图形渲染管线:指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。

图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。

image.png

  1. 顶点着色器 ,它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。

  2. 图元装配将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状。

  3. 几何着色器图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。

  4. 光栅化阶段,几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

  5. 片段着色器:片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

  6. 测试与混合:在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

通过上面几个步骤,我们了解到了OpenGL渲染的流程,都是通过一步一步去处理的,而这些处理是在OpenGL程序完成了,而OpenGL程序又交给GPU进行完成,GPU中有很多ALU计算单元,这些OpenGL程序就是在这些ALU单元里面处理的,从而解放了CPU。

Android 如何渲染图形了

看看在Android 中是如何渲染图像的,在哪个地方调用到了OpenGL来完成。我们先看看Android 的渲染架构图:

image.png

解释一下:

  • image stream produceers: 渲染数据的生产者,如Appdraw方法会把绘制指令通过canvas传递给framework层的RenderThread线程。
  • native FrameworkRenderThread线程通过surface.dequeue得到缓冲区graphic bufer,然后在上面通过OpenGL来完成真正的渲染命令。在把缓冲区交还给BufferQueue队列中。
  • image stream consumerssurfaceFlinger从队列中获取数据,同时和HAL完成layer的合成工作,最终交给HAL展示。
  • HAL: 硬件抽象层。把图形数据展示到设备屏幕

可以看到这是一个生产消费者模式:

image.png

  • 图像生产者: 也就是我们的APP,再深入点就是canvas->surface
  • 图像消费者:SurfaceFlinger
  • 图像缓冲区:BufferQueue,一般是3缓冲区

在Android 中有软件绘制,有硬件绘制:

image.png 可以看到软件绘制使用的是skia,而硬件绘制是通过调用OpenGL命令在GPU执行最终渲染到屏幕上。

到这里我们就可以知道,在图形绘制中,如果使用了硬件绘制,就会调用到OpenGL在GPU来完成绘制。

我们还知道所谓的硬件加速就是使用硬件绘制,把绘制工作交给GPU,而不是由CPU来绘制,减少了CPU的工作量。

Android 中如何使用OpenGL呢

  1. 创建 GLSurfaceView 对象:GLSurfaceView 是一个专门用于显示 OpenGL 图形的视图,它继承自 SurfaceView,并实现了 GLSurfaceView.Renderer 接口用于处理渲染逻辑。创建 GLSurfaceView 对象时,需要设置 OpenGL ES 的版本号和 Renderer 对象。

  2. 实现 Renderer 接口:创建一个类来实现 GLSurfaceView.Renderer 接口,这个类负责处理 OpenGL 的渲染逻辑。接口中定义了三个方法:

    • onSurfaceCreated(GL10, EGLConfig):在 GLSurfaceView 创建时被调用,用于初始化 OpenGL 相关的设置,比如背景颜色、光照等。
    • onSurfaceChanged(GL10, int, int):在 GLSurfaceView 的尺寸发生变化时被调用,比如屏幕旋转或大小改变。在这个方法中,可以更新投影矩阵,并设置视口的参数。
    • onDrawFrame(GL10):在每一帧渲染时被调用,将在此方法中完成实际的绘制工作。
  3. 编写顶点着色器和片段着色器:顶点着色器和片段着色器是 OpenGL 渲染的核心部分。顶点着色器用于处理输入的顶点数据,并进行坐标变换等操作。片段着色器用于处理每个像素的颜色输出。顶点着色器和片段着色器都是使用 GLSL(OpenGL Shading Language)编写的。

  4. 创建着色器程序对象:在 Renderer 类中,创建 OpenGL 着色器程序对象并连接顶点着色器和片段着色器。通过调用 GLES20.glCreateProgram() 方法创建一个程序对象,并通过 GLES20.glAttachShader() 和 GLES20.glLinkProgram() 方法将顶点着色器和片段着色器链接到程序对象上。

  5. 准备顶点数据和颜色数据:在 Triangle 类中,定义了三角形的顶点坐标和颜色数组。通过 GLES20.glVertexAttribPointer() 方法将顶点坐标和颜色数据传递给 OpenGL。

  6. 渲染循环:在 GLSurfaceView 的 onDrawFrame() 方法中,执行实际的绘制操作。首先,清除绘图缓冲区内容,然后通过设置视图矩阵、投影矩阵和模型矩阵计算出 MVP 矩阵。最后,调用 Triangle 类的 draw() 方法进行绘制操作。

  7. 释放资源:在 GLSurfaceView 销毁时,需要释放相关资源,包括着色器程序和其他 OpenGL 对象。

使用OpenGL绘制一个三角形

首先创建自定义一个GLSurfaceView.Renderer

public class TriangleRender implements GLSurfaceView.Renderer {}

在里面定义顶点着色器程序

private final String vertexShaderCode =
        "attribute vec4 vPosition; " +
                "void main() {" +
                "gl_Position = vPosition;" +
                "}";

定义片段着色器程序

private final String fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}";

在 onSurfaceCreated方法中,加载顶点程序和片段程序

static float triangleCoords[] = {
        0f,  0.5f, 0.0f, // top
        -0.5f, -0.5f, 0.0f, // bottom left
        0.5f, -0.5f, 0.0f  // bottom right
};
//设置颜色,依次为红绿蓝和透明通道
float color[] = { 1.0f, 0f, 0f, 1.0f };

public int loadShader(int type, String shaderCode){
    //根据type创建顶点着色器或者片元着色器
    int shader = GLES20.glCreateShader(type);
    //将资源加入到着色器中,并编译
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);
    return shader;
}
/**
 * surface  创建完成,加载顶点程序和片段程序
 * @param gl
 * @param config
 */
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 在这里 加载顶点着色器和片段着色器程序,再把这两个程序放到 着色器程序中去
    //OpenGL 都是静态api
    //glClearColor(float red, float green, float blue, float alpha):设置清除颜色缓冲区时的背景颜色。
    // red、green、blue 和 alpha 分别代表红、绿、蓝和透明度通道的值,取值范围为 [0, 1]。
    GLES20.glClearColor(0.5f,0.5f,0.5f,1.0f);
    //申请底层空间;triangleCoords 坐标 ,float 4个字节
    ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
    //GPU 重新整理一下内存
    bb.order(ByteOrder.nativeOrder());
    //将坐标数据转换为FloatBuffer,用以传入OpenGL ES程序
    vertexBuffer = bb.asFloatBuffer();
    //放入坐标
    vertexBuffer.put(triangleCoords);
    //从第一位读取
    vertexBuffer.position(0);
    //加载程序
    int vertexShader =  loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);
    int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);
    //glCreateProgram():创建一个着色器程序对象,用于连接顶点着色器和片段着色器。
    mProgram = GLES20.glCreateProgram();//返回一个句柄
    //glAttachShader(int program, int shader):将顶点着色器或片段着色器附加到着色器程序对象上。program 参数是着色器程序对象的引用,shader 参数是着色器对象的引用。
    GLES20.glAttachShader(mProgram,vertexShader);
    //将片元着色器加入到程序中
    GLES20.glAttachShader(mProgram,fragmentShader);
    //glLinkProgram(int program):链接着色器程序,使其成为可执行的 OpenGL 图形渲染程序。program 参数是着色器程序对象的引用。
    GLES20.glLinkProgram(mProgram);
}

在 onDrawFrame 渲染回调方法中,给着色器程序赋值

/**
 * 渲染的回调
 * @param gl
 */
@Override
public void onDrawFrame(GL10 gl) {
    //使用程序
    GLES20.glUseProgram(mProgram);

    //glGetAttribLocation(int program, String name):用于获取指定着色器程序对象中指定顶点属性的位置索引。
    // program:表示着色器程序对象的引用,name 是一个字符串,表示顶点属性的名称
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
    //glEnableVertexAttribArray(int location):启用顶点属性数组。location 参数是顶点属性数组的位置索引。
    GLES20.glEnableVertexAttribArray(mPositionHandle);
    //glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, Buffer pointer):
    // 指定顶点属性数组的位置、格式和数据。
    // index 是顶点属性数组的位置索引,
    // size 表示每个属性的分量数,
    // type 表示每个分量的数据类型,
    // normalized 表示是否对属性值进行归一化,
    // stride 表示相邻顶点之间的字节偏移量,
    // pointer 是存储顶点数据的缓冲区。
    GLES20.glVertexAttribPointer(mPositionHandle, 3,
            GLES20.GL_FLOAT, false,
            12, vertexBuffer);
    //glGetUniformLocation(int program, String name): 方法用于获取着色器程序中的 uniform 变量的位置。
    // program:表示着色器程序对象的引用,即调用 glCreateProgram 创建的对象。
    // name:表示要获取位置的 uniform 变量的名称,它是一个字符串。
    mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
    //glUniform4fv(int location, int count, float[] values, int offset):设置浮点型 uniform 变量的值。
    // location 参数是 uniform 变量的位置索引,count 是要设置的向量数量,values 是存储浮点数值的数组,offset 是数组中的偏移量。
    GLES20.glUniform4fv(mColorHandle, 1, color, 0);
    //glDrawArrays(int mode, int first, int count):根据顶点数据绘制图形。mode 参数表示渲染的图元类型,
    // 常见的有 GL_POINTS(绘制点)、GL_LINES(绘制线段)、GL_TRIANGLES(绘制三角形)等,
    // first 指定从顶点数组的哪一位置开始绘制,count 指定需要绘制的顶点数量。
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
    //禁用顶点属性数组,
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

给GLSurfaceView 设置版本号

glSurfaceView.setEGLContextClientVersion(2)

给GLSurfaceView 设置 Render对象

glSurfaceView.setRenderer(TriangleRender())

给GLSurfaceView 设置 渲染方式

/*渲染方式,RENDERMODE_WHEN_DIRTY表示被动渲染,只有在调用requestRender或者onResume等方法时才会进行渲染。
//RENDERMODE_CONTINUOUSLY表示持续渲染*/
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY

设置activity的回调

override fun onResume() {
    super.onResume()
    glSurfaceView.onResume()
}

override fun onPause() {
    super.onPause()
    glSurfaceView.onPause()
}

这样一个一个三角形就绘制出来了:

image.png

OpenGL GLSL 一些介绍

OpenGL GLSL(OpenGL Shading Language)是一种用于编写着色器程序的语言。它是一种高级语言,专门用于在图形硬件上执行着色器代码,实现对图形渲染的控制和定制。 和别的高级语言一样,里面也有一些类型和变量。

变量使用

void 表示空,用于无返回值的函数。比如顶点shader中main函数:
void main() {
    gl_Position = vPosition;
}
float、int、bool

分别代表浮点型,整型,布尔型。定义各类型变量如下:

float f = 1.0;
int i =0;
bool b = true;

vec2、vec3、vec4

分别包含2、3、4个float类型的向量。定义变量如下:

vec2 v2 = vec2(1.0, 0.0);
vec3 v3 = vec3(1.0, 0.0, 0.5);
vec4 v4 = vec4(1.0, 0.0, 0.5,0.5);

如果只给一个参数,则向量中其他值也会使用此值,比如给vec4一个1.0的值:

vec4 v5 = vec4(1.0);

等价于:

vec4 v5 = vec4(1.0,1.0,1.0,1.0);

注意提供给向量的参数只能是1个或者对应向量个数,比如vec4类型不能提供2个参数:

vec4 v6 = vec4(1.0,1.0);

上面给vec4提供2个参数的写法是错误的。 经常使用的内置变量gl_Position和gl_FragColor就是vec4类型,在片段shader中设置颜色:

gl_FragColor = vec4(1.0,0.0,0.0,1.0);

ivec2, ivec3, ivec4和vec2、vec3、vec4用法一样,区别就是ivec的参数是int类型。 bvec2, bvec3, bvec4和vec2、vec3、vec4用法一样,区别就是bvec的参数是bool类型。

顶点着色器
attribute vec4 vPosition;
void main() {
        gl_Position = vPosition;
};

片段着色器
precision mediump float; 
uniform vec4 vColor; 
void main() { 
     gl_FragColor = vColor; 
}  

内置变量

内置变量含义值数据类型
gl_PointSize点渲染模式,方形点区域渲染像素大小float
gl_Position顶点位置坐标vec4
gl_FragColor片元颜色值vec4
gl_FragCoord片元坐标,单位像素vec2
gl_PointCoord点渲染模式对应点像素坐标vec2

参考链接

关于 Android 渲染你应该了解的知识点

总结

  1. 介绍了什么是OpenGL
  2. OpenGL如何完成图形渲染
  3. OpenGL在Android 中如何使用
  4. 使用OpenGL绘制一个简单的三角形
  5. 介绍了OpenGL GLSL语言的一些变量说明
  6. 接下来会继续介绍OpenGL纹理的使用和滤镜的添加