一文详解 OpenGL ES 3.x 渲染管线
OpenGL ES 构建的三维空间,其中的三维
实体由许多的三角形拼接构成
。如下图左侧所示的三维实体圆锥,其由许多三角形按照一定规律拼接构成。而组成圆锥的每一个三角形,其任意一个顶点由三维空间中 x、y、z 三个坐标分量来定义。
对于我们日常使用的移动手持设备,手机屏幕窗口由不连续的有限的二维像素小格子
构成的,每一个像素格子有x、y两个分量来定义。
因此在OpenGL ES绘制流程中,其主要工作是将三维空间中的坐标点(x,y,z)构成的三维图形,转化为手机屏幕上的二维像素点
。
这个转化过程主要分为两个步骤:
- 一个是顶点的变换;
三维空间中的实体,经过各种变换(主要是通过矩阵相乘进行坐标系的变换),变为可投影在二维平面上的有限的像素点数据; - 二是渲染着色;
根据传入的顶点颜色数据或图片纹理数据,将相应的颜色对应到响应的像素点。
在OpenGL ES中,其大概的绘制流程如下图所示:
这里将三维空间中构成三维实体的坐标信息,转换为手机屏幕上有限的二维像素的过程
,便是由OpenGL ES渲染管线(Graphics Pipeline)来实现。
- OpenGL ES Shading Language;
- OpenGL ES 3.x 渲染管线;
一、着色语言版本
正文开始之前,需要先了解一下 OpenGL ES Shading Language 着色语言。
在《一文了解 OpenGL ES》中,我们了解到 OpenGL ES 3.x采用的是可编程渲染管线
。
OpenGL ES 3.x将顶点着色器
、片元着色器
的编码权限开放给开发者。开发者需要使用OpenGL ES Shading Language(着色语言)
编码实现顶点着色器
、片元着色器
处理逻辑,从而渲染出自己想的展示效果。
对于OpenGL ES Shading Language,我们有必要先了解一下OpenGL ES 与 OpenGL ES Shading Language 的对应关系(了解对应的API版本,才能编写对应版本要求的Shading Language脚本)。
OpenGL ES Version | GLSL Version |
---|---|
1.0 | -- |
1.1 | -- |
2.0 | 100 |
3.0 | 300 |
3.1 | 310 |
3.2 | 320 |
- OpenGL ES 1.x为固定渲染管线,无Shading Language对应版本;
- OpenGL ES 2.x开始才有可编程渲染管线,
OpenGL ES 2.0
对应的OpenGL ES Shading Language
版本为1.00
- OpenGL ES 3.x使用可编程渲染管线,
OpenGL ES 3.0
对应的OpenGL ES Shading Language
版本为3.00
Shading Language 3.00 文档:
https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf
各OpenGL ES版本API文档:
https://www.khronos.org/registry/OpenGL/specs/es/
二、渲染管线
OpenGL ES中的渲染管线(OpenGL Rendering Pipeline)
指的是一系列的变换与渲染过程
,输入
是需要渲染的3D物体的相关描述信息数据
(例:顶点坐标、顶点颜色、顶点纹理等),经过渲染管线,输出一帧想要的图像
。
渲染管线
也称之为渲染流水线
,由显示芯片(GPU)内部处理图形信号的并行处理单元
组成,这些并行处理单元两两之间相互独立,根据显示芯片性能的不同处理单元的数量也存在很大的差异。将渲染工作通过显示芯片中多个相互独立的单元进行并行处理后,渲染的效率可以大大提高。
OpenGL ES 3.x详细的渲染管线如下图所示:
2.1 基本处理
该阶段设定三维空间中物体的顶点坐标
、顶点对应的颜色
、顶点的纹理坐标
等数据,并且指定三维物体的绘制方式
,如:点绘制、线段绘制或者三角形绘制等。
2.2 顶点缓冲区
顶点缓冲区在应用程序中是可选的,对于某些在整个场景中顶点数据基本不变的情况,可以在初始化阶段将顶点数据经基本处理后送入顶点缓冲区,在绘制每一帧想要的图像时就省去了顶点数据IO的步骤,直接从顶点缓冲区中获取顶点数据即可。相比于每次绘制时单独将顶点数据送入GPU的方式,可以在一定程度上节省GPU的 IO 带宽,提高渲染效率。
2.3 顶点着色器
顶点着色器是一个可编程的处理单元
,功能为执行顶点的变换
、光照
、材质的应用与计算
等顶点的相关操作
,每个顶点执行一次
。
其工作过程为将原始的顶点几何信息(顶点坐标、颜色、纹理)
及其他属性传送到顶点着色器中,经过自定义的顶点着色程序处理产生变化后的顶点位置信息,将变化后的顶点位置信息传递给后续图元装配阶段,对应的顶点纹理、颜色等信息则经光栅化后传递到片元着色器。
顶点着色器
替代了原有固定管线
(OpenGL 1.x采用固定渲染管线)的顶点变换、光照计算
,采用 着色语言进行开发(OpenGL ES Shading Language) ,开发人员可以根据自己的需求采用着色语言自行开发顶点变换、光照等功能,大大增加了程序的灵活性。
不同于OpenGL ES 2.0中,顶点着色器输入为attribute(属性变量)
,输出为varying(易变变量)
。
在OpenGL ES3.0中,顶点着色器的输入主要为待处理顶点相应的in(输入属性)
、uniform(统一变量)
、采样器以及临时变量
,输出主要为经过顶点着色器后生成的out(输出属性)
及一些内建输出变量
。
uniform(统一变量)
:
指的是对于同一组顶点组成的三维物体其所有顶点属性都相同的属性量
,一般为场景中当前的光源位置
、当前的摄像机位置
、投影系列矩阵
等,,可以使用 uniform (统一变量) 传入顶点着色器。in(输入属性)
指的是三维空间中物体每个顶点各自不同的信息所属的变量,一般为顶点的位置
、颜色
、法向量
等,每个顶点各自不同的信息都是以attribute变量的方式传入顶点着色器的。out(输出属性)
对于从顶点着色器传递到片元着色器的量
,如 用于传递到片元着色器中的顶点颜色,可以使用out (输出属性)。- gl_Position (内建变量):
内建变量为输出到OpenGL ES渲染管线的数据变量
。
顶点着色器从应用程序中获得原始的顶点位置数据,这些原始的顶点数据在顶点着色器中经过平移、旋转、缩放等数学变换后,生成新的顶点位置。新的顶点位置通过在顶点着色器中写入gl_Position传递到渲染管线的后继阶段继续处理。 - gl_PointSize (内建变量):
内建变量为输出到OpenGL ES渲染管线的数据变量
。
gl_PointSize的值一般只有在采用了点绘制方式之后才有意义。顶点着色器中可以计算一个点的大小(单位为像素),并将其赋值给gl_PointSize(float类型)以传递给渲染管线,如果没有明确赋值的话,默认值为1。
顶点着色器代码举例
着色器采用OpenGL ES Shading Language编写,为一种类C的编程语言,顶点着色器代码实现举例如下:
详细了解着色语言语法,可查看khronos
官方:
OpenGL ES Shading Language 3.00:
https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf
// 顶点着色器程序举例如下:
#version 300 es // 为着色语言版本v3.00,必须出现在第一行
// 总变换矩阵
uniform mat4 uMVPMatrix;
// 顶点位置
in vec3 aPosition;
// 顶点颜色
in vec4 aColor;
// 用于传递给片元着色器的变量
out vec4 vColor;
void main()
{
// 根据总变换矩阵计算此次绘制此顶点位置
gl_Position = uMVPMatrix * vec4(aPosition,1);
// 将接收的颜色传递给片元着色器
vColor = aColor;
}
uniform(统一变量)
如:顶点着色器举例程序中的uniform mat4 uMVPMatrix; 总变换矩阵
。in(输入属性)
如:顶点着色器举例程序中的顶点位置aPosition、顶点颜色aColor。out(输出属性)
如:顶点着色器举例程序中的out vec4 vColor; 顶点颜色
。内建输出变量
如:如输出到渲染管线的 gl_Position(顶点的最终位置)。
注:
输出属性(顶点颜色、纹理)在顶点着色器赋值后并不直接赋值到片元着色器中,而是经由光栅化阶段处理。将三维世界由顶点数据构成的物体离散为二维世界一个个像素点,片元着色器处理的数据是已经离散到二维显示平面上的一个个离散的像素点(每个像素点为一个片元)
。而这个离散过程中位于两个顶点之间的像素顶数据(颜色值、纹理数据),大多在光栅化阶段差值计算产生
。
上图中,展示的是一个自上而下
从黑到白的一个渐变三角形
。
在OpenGL ES的输入中,仅仅输入了三个顶点颜色值,顶点1 (0,0,0) 黑色、,顶点2 (1,1,1) 白色、顶点3 (1,1,1) 白色。而在三角形在光栅化阶段,投影到屏幕上之前,其他部分的颜色是由这三个输入顶点颜色值差值计算产生的
,例如:三角形高二分之一处的点颜色差值计算的结果为 (0.5,0.5,0.5) 灰色。
2.4 变换反馈
变化反馈能够将 “顶点着色器” 的计算结果输出,也就是可以将大量的向量、矩阵等运算交给GPU来运行,在一定程度上节省CPU的运算量。变化反馈的计算结果,可以输出到一个缓冲区中,该缓冲区中的内容还可作为另一个 “顶点着色器” 程序的输入数据。
2.5 图元装配
在这个阶段,主要有两个任务,一个是图元组装
,另一个是图元处理
。
图元组装
是顶点数据被结合成完整的图元。
例如:一个单独的点就是一个图元,它只需要一个点;一条直线也是一个图元,为两个点构成的图元;三角形则需要三个顶点构成一个图元。
图元组装时会有效的收集足够的顶点以组成一个单独的图元,方便进行后续处理。图元处理
最重要的是剪裁
,其任务是消除位于显示区域之外的部分
几何图元。
图元处理阶段:
如果图元完全位于视椎体
内,则传递图元进行后面的处理;
如果有一部分位于视椎体
内,则剪裁
该图元(剪裁图元可能会额外增加顶点
数据)。
如果其完全位于视椎体
外,则丢弃该图元,以节省GPU性能。
2.6 光栅化
光栅化就是将三维空间中连续的数学图形
,栅格化为二维显示平面上一个个像素点
的过程。
将由三维
顶点信息组成的几何图元
,栅格化
为帧缓冲区中由无数像素点
构成的二维图像
。这里,最终生成的帧缓冲区中每一个像素点
都被看做一个片元
。
例如,一条直线可能在屏幕上包含了5个像素,而光栅化就是将这条直线转化为5个片元,每个片元由顶点坐标、顶点颜色、顶点纹理坐标以及顶点的深度等信息组成。
光栅化实际是由以下两个过程组成:
- 将三维空间中的几何图元,投影到二维平面上:
- 将投影到二维平面上的几何图元,栅格化为帧缓冲区中一个个像素:
2.7 片元着色器
片元着色器
是用于处理片元相关数据的可编程单元
,可以执行纹理的采样
、颜色的汇总
、计算雾颜色
等操作,每片元执行一次(每个像素执行一次)
。
片元着色器
可编程单元替换了OpenGL ES 1.x固定渲染管线阶段中纹理颜色求和
、雾以及Alpha测试
等阶段,被其替代的功能将需要由开发人员用着色器语言编码完成。
顶点着色器每顶点执行一次
,片元着色器每片元[像素]执行一次
。
一般情况下片元的数量远远大于构成三维物体顶点的数量(例如:一个三角形由三个顶点数据构成,光栅化到屏幕上时,却有成百上千个像素点构成),因此片元着色器的执行次数明显大于顶点着色器的执行次数,开发中为提高运行效率,应尽量减小片元着色器的运算量
,将一些复杂运算放在顶点着色器中执行
。
out(输出属性)
从顶点着色器传递到片元着色器的数据变量
,如顶点着色器所介绍,由系统在顶点着色器后的光栅化阶段自动插值产生。out属性变量个数随需求而定,并没有硬性的变量数量上限限制。out vec4 fragColor
片元着色器用 fragColor 向OpenGL ES渲染管线写入计算完成的片元颜色值,此颜色值将进入渲染管线的后继阶段继续处理。
不同于OpenGL ES2.0的最终片元颜色输出到内建变量gl_FragColor
,OpenGL ES 3.0最终片元的颜色输出到输出属性fragColor
。
片元着色器代码举例
着色器采用OpenGL ES Shading Language编写,为一种类C的编程语言,片元着色器代码实现举例如下:
详细了解着色语言语法,可查看khronos
官方:
OpenGL ES Shading Language 3.00:
https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf
// 片元着色器举例(每个片元[像素]执行一次)
#version 300 es // 为着色语言版本v3.00,必须出现在第一行
precision mediump float;
// 接收从顶点着色器过来的参数(具体数值由光栅化阶段差值产生)
out vec4 vColor;
// 输出到的片元颜色
out vec4 fragColor
void main()
{
//片元的最终赋值
fragColor = vColor;
}
varying(易变变量)
例,接收从顶点着色器过来的颜色参数varying vec4 vColor
out vec4 fragColor
不同于OpenGL ES2.0的最终片元颜色输出到内建变量gl_FragColor
,OpenGL ES 3.0最终片元的颜色输出到输出属性fragColor
。
例,片元着色器具体程序中的fragColor = vColor
。
2.8 剪裁测试
如果程序中启用了剪裁测试,OpenGL ES 会检查每个片元在帧缓冲中对应的位置,若对应位置在剪裁窗口中则将此片元送入下一阶段,否则丢弃此片元。。
2.9 深度测试和模板测试
- 深度测试
- 模板测试
a、深度测试
深度测试是在片元处理过程中通过测试一个片元在视椎体
的深度坐标,来判断它是否被更小深度坐标的片断遮挡而决定是否真去绘制它(开启深度测试有助于节省GPU性能);没有深度测试,物体展现与否就会取决于绘制顺序而不是摆放的坐标。
// 在Android中打开深度测试
GLES30.glEnable(GLES20.GL_DEPTH_TEST);
b、模板测试
模板测试的主要功能为将绘制区域限定在一定的范围内,一般用在湖面倒影、镜像等场合。
模板测试涉及到一个词模板缓冲区
(Stencil Buffer)。在模板缓冲中,通常每个模板值(Stencil Value)是8位的,所以每个片元一共能有256种不同的模板值。
使用时,我们可以先将模板缓冲区刷成个某个固定的值,然后进行图像绘制,完成绘制后模板缓冲区中的值可能会被污染[污染],此时开发者就可以选择丢弃或是保留这些被污染的片段,从而构造湖面倒影或者镜像。
2.10 颜色缓冲混合
经过前面的几个阶段,已经产生了候选片元,而候选片元最终要进入帧缓冲成为像素。
在候选片元进入帧缓冲区成为像素的过程中:
- 若未开启了Alpha混合,则当前片元成为屏幕的像素,覆盖掉屏幕上的原有像素;
- 若开启了混合,则根据Alpha值将当前像素与候选片元进行颜色混合得出最终的颜色。
2.11 抖动
抖动是一种简单的操作,是一种在色彩空间较小的设备上展示较大色彩空间的图像的一种方法。
例如:在一个RGB_565
的设备上展示RGB_888
的图像,展示时如果简单进行数据截断位,会造成色彩的失真和生硬。抖动使用一个矩阵,来调整一个像素周围的像素的值,来使人眼产生错觉,而模拟
出原来的色彩。
这种技巧,对于只支持8位或者16位颜色信息的显示系统中非常有用。
// 开启抖动
GLES30.glEnable(GLES20.GL_DITHER);
2.12 帧缓冲
OpenGL ES的物体绘制并不是直接回执在屏幕上的,而是预先在帧缓冲区中进行绘制,每一绘制完成一帧再将绘制的结果交换到屏幕上。因此,每次绘制新的一帧数据时都需要清理缓冲区中的相关数据,否则可能产生错误的显示效果。
三、参考
OpenGL ES Shading Language 3.00:
https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf
OpenGL ES 3.0 API:
https://www.khronos.org/registry/OpenGL-Refpages/es3.0/
gl_pipeline:
http://www.songho.ca/opengl/gl_pipeline.html
learnopengl:
https://learnopengl-cn.github.io/01%20Getting%20started/01%20OpenGL/
GLSL详解:
https://zhuanlan.zhihu.com/p/349296191
OpenGL ES 变换反馈:
https://www.shangmayuan.com/a/4a48e882e765411b8597e12c.html