OpenGL ES
OpenGL ES是一套图形硬件的软件接口,它直接跟GPU交互。
它是OpenGL图形库的一个子集,通常应用与嵌入式和手持设备,比如手机,平板电脑等。
渲染管线的功能简要的说主要有两个部分:
- 将3D坐标转换为2D坐标(每个坐标值可以理解为一个像素点)
- 把2D坐标转换为实际有颜色的像素
版本主要有两种:
- 1.x的版本主要是
固定渲染管线
, 它是不支持人们对渲染流程进行干预的。 - 2.0以上的版本是
可编程渲染管线
, 通过修改顶点着色器和片段着色器制作更加炫酷的效果。
可编程渲染管线主要通过GLSL的类C语言编写着色器来实现,着色器有两种:
- 顶点着色器,主要用于坐标转换等
- 片段着色器,主要用于对片段(像素点)着色
两种着色器的创建方式有两种:
- 在代码中创建字符串,这种加载速度快,但不利于修改查看
- 以文件的形式存在,这种使用需要程序解析文件数据,加载速度慢,但利于修改和查看
本篇文章主要讲述下:
- 渲染的大概流程
- GLSL语法
- 示例
学习更多的OpenGL,可参考: Learn OpenGL
渲染的大致流程:
OpenGL的渲染流程主要是:
如图所示:
几何着色器主要针对于四边形等多边形的处理,但在OpenGL ES中是不支持这个的。
如果需要绘制四边形,OpenGL ES的处理手段是绘制两个三角形组合为四边形。
简要说明下如上所示:
- 顶点数据 主要包括位置坐标,颜色,纹理等信息,这个是渲染的基础数据
- 顶点着色器 主要用于坐标转换和传递varying变量给片段着色器
- 图源装配 根据类型和顶点数据将其组合为点,线,三角形等,并执行裁切
- 光栅化 将图源转换为一系列的片段(像素点), 并进行采样计算插值
- 片段着色器 将光栅化计算的数值对片段进行着色
- 测试混合 会对片段执行透明度,深度,裁切,抖动等测试
最后会将片段放置在前后帧缓冲中,用于屏幕的绘制刷新。
渲染的最终目的其实就是将3D物体通过计算转换为2D屏幕显示, 在渲染的过程中,这些阶段的运行都通过上一个阶段的数据支持而运行的。
下面简要说下一些执行过程的细节。
顶点着色器
顶点着色器主要通过模型视图矩阵对坐标进行转换为裁切坐标系。
最终坐标数据被保存在GLSL的内置变量gl_Position
中。
另外在GLSL中有个varying
的可变变量,它通过光栅化的计算,会被传递给片段着色器。
所以,需要主要该变量在使用中一定要保证顶点和片段着色器变量类型的统一性,且它的数值不是恒定的。
图源装配
在《我所理解的cocos2d-x》中,它也被称为图元组装。简单的理解就是生成点,线,图形等。
常见的图元类型有:
- POINTS: 点
- LINES: 未连接的线段
- LINE_LOOP: 闭合且连接的选段
- LINE_STRIP: 未连接的线段
- TRIANGLE_STRIP: 链接的三角形
- TRIANGLE_FAN: 扇形三角形
- TRIANGES: 未连接的三角形
在图元生成后,OpenGL会通过裁切坐标系对图形进行检测。
- 图形在在裁剪坐标系以外的, 则丢弃,也就是不进行绘制
- 图形与裁剪坐标系相交的,会执行裁剪,生成新的图元
如果考虑下cocos的精灵显示,我们就能理解为什么放置在屏幕以外的节点有助于提升性能了。
光删化
它主要用于将上一个阶段图元生成后执行片段化操作, 每个片段可以理解为像素点。
每个片段都会计算出坐标位置,用于图像显示的映射。
计算的这个阶段被称为采样, 采样 主要有两种:
- 单一采样, 2D游戏常用,计算较为简单取中间数值即可
- 多重采样,3D游戏常用,基于附近多个位置进行计算,显示效果好,但性能耗费高
采样的数据是离散整数,如果图片存在斜边或者旋转,就会容易出现锯齿。
在cocos2d-x 中,默认单一采样,多重采样默认为不开启。 开启的参数是:multiSampling
。
采样设置不允许在游戏启动以后进行改变,主要是因为采样数据的计算和存储相关是不同的。
另外,在2D游戏中也不要考虑采用多重采样来解决锯齿问题,这里给出一个简单的处理方案:
图片适当制作大一点,然后进行缩小到适合位置,他的本质就是纹理过滤。
片段着色器
它主要将光栅化生成的数值,并获取纹理,对片段进行着色。数据被放到OpenGL ES内置变量gl_FragColor
中。
这里是渲染各种高级特效,比如光,阴影等效果的地方。
测试与混合
会经过多个阶段的测试操作,这些操作会丢失片段或者修改片段的颜色值。主要有:
- 裁剪测试:检测片段是否在设定的裁剪窗口内, 如果没有则丢失
- 透明度测试:检测片段的透明度是否符合条件
- 深度测试:检测片段的前后遮挡关系, 比如前后窗口遮挡, 如果被遮挡则不会显示
- 模版测试: 检测当前片段的模版值是否与模版缓冲区的模版值相对应
GLSL语法
使用语言是一种类C的程序语言。
渲染是一个很庞大的工作量,对性能极度敏感, 因此该语言相较于C语言有着诸多的限制:
- 不支持精度更高的
double
- 不支持引用,指针,比如
&
、*
、->
- 不支持字符串操作
- 不支持隐式转换
- 有限数组的使用
它是一种强类型语言, 因此变量和方法在使用之前必须定义它,且每个变量必须有特定的类型。
类型 | 描述 |
---|---|
void | 表示方法没有返回值 |
bool | 布尔类型,其值只能为true或false |
int | 有符号的整数, 支持八,十, 十六进制 |
float | 单精度浮点数 |
vec2~4 | 2~4的浮点型向量 |
ivec2~4 | 2~4的整型向量 |
bvec2~4 | 2~4的布尔型的向量 |
mat2~4 | 2x2, 3x3, 4x4的浮点型矩阵 |
sampler2D | 2D纹理句柄 |
samplerClub | 立方体纹理句柄 |
struct | 结构体 |
示例:
//------------------------- 标量 -------------------------
int c = 10;
float d = 1.0; // 注意float跟int, 浮点型一定要加上小数点
bool isExist = bool(d); // 使用()将float转换为bool类型
// ------------------------- 向量 -------------------------
// 主要用于颜色、法线、位置、纹理坐标等,类型可为float, int, bool等,支持2~4个数据。
// 初始化向量可以使用: ()
ivec3 a = vec3(1); // a = {1, 1, 1}
vec3 b = vec3(1.5, 2.4, 4.0); // b = {1.5, 2.4, 4.0}
vec3 c = vec3(b); // c = {1.5, 2.4, 4.0}
/*
支持使用下标访问向量中的成员
* x,y,z,w表示位置相关
* r,g,b,a表示颜色向量相关
* s,t,p,q表示纹理坐标相关
*/
vec3 d = b.xyz; // d = {1.5, 2.4, 4.0}
vec3 e = b.xyx; // e = {1.4, 2.4, 1.4}
vec4 f = vec4(c, 5.0); // 用vec3的变量和float变量初始化vec4
// 标量可以和向量交互使用
float f;
vec2 v,u;
u = v * f; // 等价于: u.x = v.x * f; u.y = v.y * f;
// ------------------------- 矩阵 -------------------------
// 成员只能为浮点型数据
mat3 data1 = mat3(
1.0, 0.0, 1.0,
2.0, 0.0, 2.0,
3.0, 0.0, 3.0,
);
// data1 = {1.0, 0.0, 1.0, 2.0, 0.0, 2.0,3.0, 0.0, 3.0,}
mat2 data2 = mat2(1.0); // data2 = {1.0, 1.0, 1.0, 1.0}
// 向量可以和矩阵交互使用
vec3 v, u;
mat3 m;
u = v * m;
// 等价于 u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
// u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
// u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
// ------------------------- 结构体 -------------------------
struct myst
{
float num;
vec3 color;
};
myst st;
myst st1 = myst(float(1.0), vec3(1.0, 1.0, 1.0));
// ------------------------- 数组 -------------------------
// 不能在声明时,直接初始化数组
int a[3];
a[0] = 1;
操作符:
操作符 | 描述 |
---|---|
() | 可用于初始化,显式转换 |
[] | 数组,向量,矩阵的下标 |
. | 结构体,向量成员的选择操作符 |
++ – | 自增,自减 |
+ - * / | 加,减,乘,除 |
! | 取反 |
> < >= <= == != | 大于,小于, 大于等于, 小于等于, 等于, 不等于 |
&& || | 逻辑与,逻辑或 |
?: | 条件判断 |
= += -= *= /= | 赋值 |
, | 逗号 |
限定符:
共有三种:in, out, inout, 分别表示输入, 输出, 输入输出。
在没有指定修饰符的情况下,默认为in
void func(float num) // 等价于 in float num
{
//
}
精度限定符:
符号 | 描述 |
---|---|
lowp | 低 |
mediump | 中 |
highp | 高 |
使用的时候, 要预先进行声明
#ifdef GL_ES
precision lowp float; // 表示使用低精度, 精度越高, 图像越清晰但是相应的计算量也越大
#endif
修饰符:
可放在变量类型的前面, 主要有如下几种:
修饰符 | 作用域 | 描述 |
---|---|---|
const | 局部 | 常量 |
attribute | 全局 | 应用程序发送给顶点着色器的逐顶点数据 |
uniform | 全局 | 应用程序发送给顶点和片段着色器的数据 |
varying | 全局 | 顶点着色器发送给片段着色器的变量, 数据会根据插值计算改变 |
- const: 仅能用于本地变量和函数参数中, 结构体成员和函数返回值不能使用。它必须在定义时被初始化, 使用const有助于外部程序为其赋值导致不必要的错误。
const vec3 data = vec3(1.0);
-
attribute: 仅能由应用程序传递给顶点着色器,且不能在其他任何类型的着色器程序中使用。
在顶点着色器中为只读状态, 不可修饰数组和结构体类型, 应用程序主要传递颜色, 位置, 法线等数据
attribute vec4 a_position;
attribute vec4 a_color;
-
uniform: 由应用程序在运行前传递数据, 一般为光照参数, 颜色值等,属于只读的全局变量。
它可以修饰任何基础数据类型
uniform vec4 lightPosition;
-
varying: 在顶点着色器中定义并赋值, 在片段着色器中可读取其值,但不能进行赋值。
它是可变的, 该变量会在每个顶点设置一个值, 然后在一个图元范围内被线性插值, 如果采用单一采样, 其值表示对应片段中心的值。
它的使用一定要确保顶点着色器和片段着色器必须使用相同的类型和名称,否则会导致链接错误。
// 顶点着色器
varying vec4 v_fragmentColor;
// 片段着色器
varying vec4 v_fragmentColor;
其他
- 支持关键字if-else, for, while, break, continue等, 另外片段着色器中支持
discard
来丢弃当前片段 - 顶点和片段的着色器都需要有一个main() 表示着色器程序的入口
- 函数允许重载,即函数名相同,但参数个数不同的函数
简单的示例:
/*
顶点着色器
1. 应用程序将坐标,颜色数据赋给vVertex, vColor
2. gl_Postion为顶点转换后获取的位置数据; 将vColor赋值给可变变量v_fragmentColor
*/
attribute vec4 vVertex;
attribute vec4 vColor;
varying vec4 v_fragmentColor;
void main() {
gl_Position = vVertex;
v_fragmentColor = vColor;
}
/*
片段着色器
1. 经过插值计算后的v_fragmentColor赋值给gl_FragColor
2. gl_FragColor为片段颜色
*/
varying vec4 v_fragmentColor;
void main() {
gl_FragColor = v_fragmentColor;
}
最后祝大家学习生活愉快!