Android OpenGL ES 2.0 入门第二课

本文目录

一. 前言
二. OpenGL ES中的形状
三. 渲染管线的流程
四. 读取顶点数据 Vertex Data
五. 顶点着色器 Vertex Shader
五. 组装图元 Shape Assembly
五. 光栅化 Rasterization
六. 片段着色器 Fragment Shader
七. 测试与混合 Tests and Blending
八. 帧缓存 / 渲染缓存

前言

在上一课的学习里面,我们简单的认识了OpenGL ES2.0程序的基本写法,必须要有GLSurfaceView渲染表面和GLSurfaceView.Renderer渲染器才可以实现。同时我们也清楚知道OpenGLES绘画的内容主要在Rederer里面去编写,那么我们这次就学习实现二维的形状——三角形。在学习之前也要学习一些知识点:管线渲染的流程、着色器编程GLSL,它们的作用非常大,所以一定要好好学。

  • Android OpenGL ES 2.0 初次体验

OpenGL ES中的形状

在OpenGL ES中只有直线三角形,点和直线可以用于某些效果,但是,只有三角形才能用来构建复杂的对象和纹理的场景。具体使用是将点放到一个组里构建出三角形,再告诉OpenGL ES 如何连接这些点。如果想要构建出更复杂的图形,例如拱形,圆球等等,那么我们就需要足够的点拟合这样的曲线。

三角形绘制球体

渲染管线的流程

在 OpenGL ES 的 3D 空间中,屏幕和窗口却是 2D 像素数组,这就导致 OpenGL ES 的大部分工作都是关于把 3D 坐标转变为适应屏幕的 2D 像素。3D 坐标转为 2D 坐标的处理过程是由 OpenGL ES 的图形渲染管线(Graphics Pipeline,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:

  • 第一部分是把 3D 坐标转换成 2D 坐标
  • 第二部分是将 2D 坐标转换成有颜色的像素

2D 坐标和像素的区别:2D 坐标精确表示一个点在 2D 空间中的位置,而 2D 像素是这个点的近似值,2D 像素受到屏幕/窗口分辨率的限制。

OpenGL ES 采用C/S编程模型,客户端运行在 CPU 上,服务端运行在 GPU 上,调用 OpenGL ES 函数的时,由客户端发送至服务器端,并被服务端转换成底层图形硬件支持的绘制命令。

渲染管线的流程

读取顶点数据 Vertex Data

为了渲染一个三角形,我们以数组的形式传递3个 3D 坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);

顶点数据
//设置三角形颜色和透明度(r,g,b,a),绿色不透明
float color[] = {0.0f, 1.0f, 0.5f, 1.0f};
//设置三角形顶点数组,默认按逆时针方向绘制
static float triangleCoords[] = {
        0.0f, 0.5f, 0.0f, // 顶点
        -0.5f, -0.5f, 0.0f, // 左下角
        0.5f, -0.5f, 0.0f  // 右下角
};

顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个 3D 坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示,它可以包含任何我们想用的数据,但是简单起见,我们假定每个顶点只由一个 3D 位置和一些颜色值组成。

public static FloatBuffer floatBuffer;
public static FloatBuffer fBuffer(float[] a) {
    //先初始化buffer,数组的长度*4,因为一个float占4个字节
    ByteBuffer mbb = ByteBuffer.allocateDirect(a.length * 4);
    //数组排列用nativeOrder,设置字节顺序为本地操作系统顺序
    mbb.order(ByteOrder.nativeOrder());
    //转换为浮点型缓冲
    floatBuffer = mbb.asFloatBuffer();
    //在缓冲区内写入数据
    floatBuffer.put(a);
    //设置缓冲区起始位置
    floatBuffer.position(0);
    return floatBuffer;
}

以上就是读取顶点数据的代码,在 allocateDirect 方法分配了内存并指定了大小之后,下一步就是 order 告诉 ByteBuffer 按照本地字节序组织它的内容。本地字节序是指,当一个值占用多个字节时,比如 32 位整型数,字节按照从最重要位到最不重要位或者相反顺序排列。接下来 asFloatBuffer 方法可以得到一个反映底层字节的 FloatBuffer 类实例,避免直接操作单独的字节,而是使用浮点数。最后,通过 put 方法就可以把数据从 Java 层内存复制到缓冲区,当进程结束时,这块内存就会被释放掉。


顶点着色器Vertex Shader

着色器(Shader):是在GPU上运行的小程序,此程序使用OpenGL ES SL语言来编写,它是一个描述顶点或像素特性的简单程序。

在渲染管线中传输的每个顶点坐标位置,OpenGL ES都会调用顶点着色器对每个顶点执行一次运算,它还可以使用顶点数据来计算该顶点的坐标、颜色、光照和纹理坐标等。

顶点着色器

在顶点着色器中最主要的任务是执行顶点坐标变换,我们设定的图元坐标是一种本地坐标,但GL是无法识别出的,所以可以在顶点着色器中对本地坐标执行模型视图变换,将本地坐标转化为裁剪坐标系的坐标值。

顶点着色器的另一个功能是向后面的片段着色器提供一组易变变量(varying)。易变变量会在图元装配阶段(简单说,图元装配之后,所有 3D 的图元将被转化为屏幕上 2D 的图元。)之后被执行插值计算,如果是单重采样,其插值点为片段的中心,如果多重采样,其插值点可能为多个采样片段中的任意一个位置。易变变量可以用来保存插值计算片段的颜色,纹理坐标等信息

想要定义一个着色器程序,还要通过一种特殊的语言去编写:OpenGL Shading Language,简称GLSL。GLSL语言类似于 C 语言或者 Java 语言,它的程序入口也是一个名为 main 的函数。关于 GLSL 的部分,完全可以单独写一篇博客了,暂时先不详细阐述。

"attribute vec4 vPosition;" +
        "void main() {" +
        "  gl_Position = vPosition;" +
        "}";

首先我们定义了 a_Position 变量,它是 vec4 类型的,而 attribute 只能存在于顶点着色器中,一般用于保存顶点数据,它可以在数据缓冲区中读取数据。然后,数据缓存区中的顶点坐标会赋值给 a_Position ,a_Position 会传递给最终坐标 gl_Position


组装图元Shape Assembly

在顶点着色器程序输出顶点坐标之后,各个顶点被按照绘制命令中的图元类型参数,以及顶点索引数组被组装成一个个图元。通过组装图元,所有 3D 的图元已经被转化为屏幕上 2D 的图元。

组装图元

光栅化 Rasterization

在光栅化阶段,基本图元被转换为供片段着色器使用的片段,片段是由很多小的像素组成,它们包含位置、颜色和纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。随后这些片段会送到片段着色器中处理,也就是这个阶段:从顶点数据到可渲染在显示设备上的像素的质变过程。

光栅化过程

另外,在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

直线片段光栅化过程

片元在成为像素之前,还会做多种测试(比如深度测试、透明度测试、模板测试等,这些测试目前接触到的一般在3D图像中更常使用,比如深度测试进行物体的遮挡效果的渲染,模板测试可以用于描边等,2D中应用较少)以决定其最终是否会被显示为像素。所以,严格来说,“片元”和“像素”并不是一一对应的。


片段着色器 Fragment Shader

片段着色器的主要作用是计算每一个片段最终的颜色值(或者丢弃该片段)。

片段着色器

在片段着色器之前的阶段,渲染管线都只是在和顶点、图元打交道。在 3D 图形程序开发中,贴图是最重要的部分,程序可以通过 GL 命令上传纹理数据至 GL 内存中,这些纹理可以被片段着色器使用。片段着色器可以根据顶点着色器输出的顶点纹理坐标对纹理进行采样,以计算该片段的颜色值。

另外,片段着色器也是执行光照等高级特效的地方,比如可以传给片段着色器一个光源位置和光源颜色,可以根据一定的公式计算出一个新的颜色值,这样就可以实现光照特效。

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

第一行的 mediump 指的就是片段着色器的精度了,有三种可选,这里用中等精度就行了。uniform 则表示该变量是不可变的了,也就是固定颜色了,目前显示固定颜色就好了。

gl_FragColor 变量就是 OpenGL 最终渲染出来的颜色的全局变量,而u_Color 就是我们定义的变量,通过在 Java 层绑定到 u_Color 变量并给它赋值,就会传递到 Native 层的 gl_FragColor 中。


测试与混合 Tests and Blending

像素所有权测试用来判断帧缓冲区中该位置的像素是否属于当前 OpenGL ES,例如在窗口系统中该位置可能会被其他应用程序窗口遮挡,此时该像素则不会被显示。

在片段测试之后,片段要么被丢弃,要么每个片段对应的颜色,深度,模板值会被写入帧缓冲区,最终呈现在设备屏幕上。帧缓冲区中的颜色值也可以被读回到客户端应用程序中,这样可以实现绘制到纹理的效果。

至此,OpenGL ES 渲染管道最终将每个像素点的颜色,深度,模板等数据输送到帧缓存中(Framebuffer)。


帧缓存 / 渲染缓存

总的来说,帧缓存是接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域。它存储着 OpenGL ES 绘制每个像素点最终的所有信息:颜色,深度和模板值。更通俗点,可以理解成存储屏幕上最终显示的一帧画面的区域。

渲染缓存则存储呈现在屏幕上的渲染图像,它也被称作颜色缓冲区,因为它本质上是存储要显示的颜色。多个纹理对象或多个渲染缓存对象,可通过连接点(attachment points)连接到帧缓存对象上。

可以同时存在很多帧缓存,并且可以通过 OpenGL ES 让 GPU 把渲染结果存储到任意数量的帧缓存中。但是,只有将内容绘制到视窗体提供的帧缓存中,才能将内容输出到显示设备。视图系统提供的帧缓存通常由两个缓存对象组成,一个前端缓存,一个后端缓存。

前帧缓存决定了屏幕上显示的像素颜色。程序的渲染结果通常保存在后帧缓存在内的其他帧缓存,当渲染后的后帧缓存包含一个完成的图像时,前后帧缓存会立即互换,前帧缓存变成新的后帧缓存,后帧缓存变成新的前帧缓存。


完整的渲染管线的过程

参考文献

  1. https://juejin.im/post/5af0ebfa518825671d207fa4#heading-4

  2. http://colin1994.github.io/2017/04/01/OpenGLES-Lesson01/#1-_%E9%A1%B6%E7%82%B9%E6%95%B0%E7%BB%84

  3. http://linbinghe.com/2018/b8f62c8f.html

  4. https://blog.csdn.net/srk19960903/article/details/74942401

你可能感兴趣的:(Android OpenGL ES 2.0 入门第二课)