音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总
我们在渲染过程中会出现一些问题,OpenGL提供了一些渲染技巧来解决这些问题,包括正背面剔除、深度测试、颜色混合,本文就针对这些问题以及解决方案进行分析学习
主要内容:
1、隐藏面消除(正背面剔除)
2、深度测试
3、颜色混合
4、通过甜甜圈案例体验这些问题以及渲染技巧的解决过程
1、隐藏面消除(重点)
1.1 问题
该案例绘制一个甜甜圈,并通过增加光照产生正反面。但是在进行画面旋转的时候,会发现出现显示甜甜圈反面的现象。这是不合理的,因为既然是反面我们就应该无法看。
这就是显示隐藏面的问题。
如图:
1.2 问题解决
针对显示隐藏面的问题,就要进行隐藏面消除。
隐藏面消除:
我们虽然绘制的是3D图像,但是显示在屏幕上的其实只是2D平面,一次只能看见正面,无法看到背面,所以当前画面不可见部分是无需绘制的。
因此在3D图像绘制时需要判断哪些画面对观察者是可见的,对有可见部分进行绘制,对不可见部分进行消除,这就叫隐藏面消除。
有两种方式可以解决该问题,油画法、正背面剔除法,油画法有其缺陷,通常使用的都是正背面剔除法,因此油画法仅做了解,正背面剔除需要掌握
1.3 油画法(仅做了解)
原理: 先绘制场景中离观察者较远的图像,再绘制较近的图像,这样就可以解决隐藏面消除。
弊端:
1、当多个图元互相重叠时就无法先绘制哪个图元了
2、而且不可见的画面也进行绘制,做了无效操作,浪费性能
1.4 正背面剔除法
1.4.1 原理:
原理很简单,在正面观察时只绘制正面画面,在背面观察时只绘制背面画面。
因为我们在二维画面中观察三维物体,一次观察并不能看到所有的画面,只能看到正面,所以我们每次渲染就只渲染正面,背面不做渲染,这样也可以提高渲染效率,可以提高50%的渲染性能。
1.4.2 判断过程
任何平面都有两个面,正面和背面,正面和背面的区分是通过图形绘制的顶点顺序和观察者的方位判断的。
正面是逆时针顺序绘制,背面是顺时针顺序绘制。
通过下面图示进行判断
1、观察者在观察一个正方体,这里以1、2、3顶点组成的三角形作为被观察者
2、按照图示方位左侧三角形时顺时针,作为背面,右侧三角形时是逆时针,作为正面
3、当观察者转到右边,那么左侧三角形时逆时针,成为正面,右侧三角形是顺时针,成为背面
1.4.3 正背面剔除的API
正背面剔除的开启和关闭:
glEnable(GL_CULL_FACE);//开启正背面剔除
glDisable(GL_CULL_FACE);//关闭正背面剔除
熟记,glEnable和glDisable是所有状态的开启和关闭,只要记住正背面的状态改变使用的是GL_CULL_FACE即可。
设置正背面剔除的正背面选择
/*
mode表示用户选择剔除的那个面,包括GL_FRONT,GL_BACK,GL_FRONT_AND_BACK,默认是GL_FRONT
*/
void glCullFace(GLenum mode);
了解即可,一般不会改动,默认的是背面,我们一般用默认的就可以,没必要改动
设置绕序方式:
glFrontFace(GL_CCW);
了解即可,用户指定绕序那个为正面,默认是GL_CCW,即逆时针为正面,一般都不要更改
1.4.4 代码实现
//根据设置iCull标记来判断是否开启背面剔除
if(iCull)
{
glEnable(GL_CULL_FACE);//开启正背面剔除
glFrontFace(GL_CCW);//用户指定绕序那个为正面,默认是GL_CCW,即逆时针为正面,一般都不要更改
glCullFace(GL_BACK);//剔除背面
}else{
glDisable(GL_CULL_FACE);//关闭正背面剔除
}
- 通过glEnable()开启正背面剔除,glEnable()方法是所有的渲染技巧使用的方法,重要的是后面的GL_CULL_FACE表明这是开启的正背面剔除
- 并且有开启必须有关闭,其他的渲染技巧也是这样的,因为OpenGL是状态机,改变的是整个状态
2、深度测试
2.1 问题
绘制画面时,距离较近的画面先绘制,距离较远的画面后绘制,当两个画面重叠时,这个重叠部分较近的画面会使用较远画面的深度值,这样就会导致出现坍塌效果。
效果图:
2.2 深度测试
2.2.1 概念认识
深度:
深度就是像素点的Z值,像素点在3D画面到摄像机的距离(也就是距离观察者)
深度缓冲区:
在显存中有一块区域,存储着画面中每个像素点的深度值,值越大,距离摄像机越远,这个区域就是深度缓存区。
深度值的范围为(0,1)之间. 值越⼩表示越靠近观察者,值越⼤表示越远离观察者。默认值为1,也就是我们在清空缓存区的时候,会设置为默认值。
深度缓冲区,⼀般由窗⼝管理系统,GLFW创建.深度值⼀般由16位,24位,32位值表示. 通常是24位.位数越⾼,深度精确度更好.
深度测试:
深度缓冲区(DepthBuffer)和颜⾊缓存区(ColorBuffer)是对应的.颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息. 每个像素点的都有自己的深度值和颜色值,并且需要通过深度值来判断颜色值的获取。
在决定是否绘制⼀个物体表⾯时, ⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较. 如果⼤于深度缓冲区中的值,则丢弃这部分.否则利⽤这个像素对应的深度值和颜⾊值.分别更新深度缓冲区和颜⾊缓存区. 这个过程称为”深度测试”。
2.2.2 深度缓存区方法(Depth-Buffer),也叫Z-Buffer方法
原理:
开启深度测试,通过深度缓冲区存储的深度与当前像素的深度进行判断,较小者存入。
常见API:
//开启深度测试
glEnable(GL_DEPTH_TEST);
//关闭深度测试
glDisable(GL_DEPTH_TEST);
//指定深度测试判断式
glDepthFunc(GLEnum mode);
//打开/阻断深度缓存区写入
//value:GL_TURE表示开启深度缓存区写入;GL_FALSE表示关闭深度缓存区写入
void glDepthMask(GLBool value);
仅记忆开启和关闭深度测试需要传入GL_DEPTH_TEST即可,对于其他的API知道有这个玩意儿就够了。
注:深度测试判断式
默认是GL_LESS,一般不用设置。
2.2.3代码实现:
//根据设置iDepth标记来判断是否开启深度测试
if(iDepth){
glEnable(GL_DEPTH_TEST);//开启深度测试
} else {
glDisable(GL_DEPTH_TEST);//关闭深度测试
}
2.3 多边形偏移(基本上很少遇到,仅了解)
2.3.1 问题
针对深度测试,会出现闪烁问题(Z-Fighting(Z冲突,闪烁)问题)
开启深度测试后,OPenGL会去比较深度缓冲区的深度值,但是如果深度缓冲精度的限制对于深度相差非常小的情况下,OpenGL无法准确判断两者的深度值,就会导致深度测试的结果不可预测,显示出来的现象交错闪烁。
2.3.2 原理
简单来说就是在深度测试前将立方体的深度值做一些细微的调整,让深度相差非常小的两个图层的间隔更大,这样深度缓冲区就可以准确判断。
2.3.3 常用API
/*
参数类型有:
点填充的偏移:GL_POLYGON_OFFSET_POINT
线填充的偏移:GL_POLYGON_OFFSET_LINE
全填充的偏移:GL_POLYGON_OFFSET_FILL
*/
//开启多边形偏移
glEnable(GL_POLYGON_OFFSET_FILL);
//关闭多边形偏移
glDisable(GL_POLYGON_OFFSET_FILL);
/*
指定偏移量
传入两个参数factor和units,一般分别设置为-1和1
*/
void glPolygonOffset(Glfloat factor,Glfloat units);
详细说明:
1、这两个参数应用到片段上总偏移的计算方程式为Depth Offset = (DZ * factor) + (r * units);
2、DZ:深度值(Z值)
3、r:使得深度缓冲区产⽣变化的最⼩值,具体是由OpenGL 平台指定的
⼀个常量.能产⽣于窗⼝坐标系的深度值中可分辨的差异最⼩值.r 。
4、得到的结果负值,将使得z值距离我们更近,⽽正值,将使得z值距离我们更远,
指定偏移量基本用不到,了解即可
2.3.4 具体实现
glEnable(GL_POLYGON_OFFSET_LINE);
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
glPolygonOffset(-1.0f,-1.0f);
2.4 ZFighting闪烁问题预防(仅了解)
1、不要将两个物体靠的太近
避免渲染时三⻆形叠在⼀起。这种⽅式要求对场景中物体插⼊⼀个少量的偏移,那么就可能避免ZFighting现象。例如上⾯的⽴⽅体和平⾯问题中,将平⾯下移0.001f就可以解决这个问题。当然⼿动去插⼊这个⼩的偏移是要付出代价的。
2、尽可能将近裁剪⾯设置得离观察者远⼀些
上⾯我们看到,在近裁剪平⾯附近,深度的精确度是很⾼的,因此尽可能让近裁剪⾯远⼀些的话,会使整个裁剪范围内的精确度变⾼⼀些。但是这种⽅式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯参数。
3、使⽤更⾼位数的深度缓冲区
通常使⽤的深度缓冲区是24位的,现在有⼀些硬件使⽤使⽤32位的缓冲区,使精确度得到提⾼。
3、混合(重点)
3.1 问题
当两个图层重叠时,如果上层是不透明的,可以用深度测试解决,如果上层是半透明的,就无法简单的使用深度测试来解决,而是需要通过颜色混合来设置颜色。
3.2 图层混合
3.2.1 介绍
OpenGL渲染时,会把颜色值放到颜色缓冲区,每个像素的深度值放在深度缓冲区。当深度测试被关闭,新的颜色值会简单地覆盖颜色缓冲区中已经存在的其他值。当深度测试被打开,则会保留深度值Z更小的。但如果打开了 混合功能,那么下层的颜色值就不会被清除了。
只有最上面的图层颜色设置透明度之后混合才有效果。
如果没有开启混合,虽然上层图层有透明度,也会不发生混合。
3.2.1 常用API
//打开混合
glEnable(GL_BlEND);
//关闭混合
glDisable(GL_BIEND);
3.2.2 混合方程式(重点)
在固定着色器中只要打开开关就可以打开混合状态,但是在可编程着色器中是通过混合方程式来实现的,所以需要着重看这个
混合方程式:
/*
混合方程式:
Cf :最终计算参数的颜⾊
Cs : 源颜⾊,作为当前渲染命令结果进⼊颜⾊缓存区的颜⾊值
Cd :⽬标颜⾊,已经存储在颜⾊缓存区的颜⾊值。
S:源混合因⼦
D:⽬标混合因⼦
*/
Cf = (Cs * S) + (Cd * D)
OpenGL也提供了很多混合方程式供我们选择,一般用第一种就可以
混合因子
源颜色和目标颜色都是业务需要,那么混合因子的设置是如何选择的呢?
OpenGL给了很多混合因子供我们选择
1、表中R、G、B、A 分别代表 红、绿、蓝、alpha。
2、表中下标S、D,分别代表源、⽬标。
3、表中C 代表常量颜⾊(默认⿊⾊)。
3.2.3 案例
问题描述:
如果颜⾊缓存区已经有⼀种颜⾊红⾊(1.0f,0.0f,0.0f,1.0f),作为⽬标颜⾊Cd,如果在这上⾯⽤⼀种alpha为0.6的蓝⾊(0.0f,0.0f,1.0f,0.6f)。
当混合功能被启动时,元颜色和目标颜色的组合方式是混合方程式控制的。
混合方程式选择:
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
计算
S = 源alpha值 = 0.6f
D = 1 - 源alpha值= 1-0.6f = 0.4f
⽅程式Cf = (Cs * S) + (Cd * D) 等价于 = (Blue * 0.6f) + (Red * 0.4f)
3.3 代码实现
//1.开启混合
glEnable(GL_BLEND);
//2.开启组合函数 计算混合颜色因子
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
//3.使用着色器管理器
//*使用 单位着色器
//参数1:简单的使用默认笛卡尔坐标系(-1,1),所有片段都应用一种颜色。GLT_SHADER_IDENTITY
//参数2:着色器颜色
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
//4.容器类开始绘制
squareBatch.Draw();
//5.关闭混合功能
glDisable(GL_BLEND);
4、代码案例
4.1 正背面剔除、深度测试案例
具体代码请下载查看甜甜圈案例,分析注释较为清晰,这里仅分析关键代码
4.1.1 甜甜圈实现
甜甜圈不需要我们手动绘制,只需要调用系统提供的甜甜圈即可
//创建一个甜甜圈
//void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
//参数1:GLTriangleBatch 容器帮助类
//参数2:外边缘半径
//参数3:内边缘半径
//参数4、5:主半径和从半径的细分单元数量
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
为了演示功能,代码中使用到了矩阵堆栈的计算、通过键位进行视角控制、右键菜单栏选项的实现,这些不需要考虑。
4.1.2 正背面剔除实现
重点在于开启关闭正背面剔除
//根据设置iCull标记来判断是否开启背面剔除
if(iCull)
{
glEnable(GL_CULL_FACE);//开启正背面剔除
glFrontFace(GL_CCW);//用户指定绕序那个为正面,默认是GL_CCW,即逆时针为正面,一般都不要更改
glCullFace(GL_BACK);//剔除背面
}else{
glDisable(GL_CULL_FACE);//关闭正背面剔除
}
现象:
4.1.3 深度测试
深度测试的开启关闭
//根据设置iDepth标记来判断是否开启深度测试
if(iDepth){
glEnable(GL_DEPTH_TEST);//开启深度测试
} else {
glDisable(GL_DEPTH_TEST);//关闭深度测试
}
现象:
4.2 颜色混合案例
具体代码请下载查看颜色混合案例,分析注释较为清晰,这里仅分析关键代码
演示内容为:四个固定正方形+1个可移动正方形,可移动正方形移动到固定正方形上就可以出现颜色混合
具体实现:
1、正方形的实现就是简单的图元绘制,可参考最早的绘制三角形案例来实现。
2、可移动正方形的绘制时就需要使用到颜色混合
注:正方形的可移动代码仅辅助案例实现,无需关注
添加数据
//绘制1个移动矩形
squareBatch.Begin(GL_TRIANGLE_FAN, 4);
squareBatch.CopyVertexData3f(vVerts);
squareBatch.End();
//绘制4个固定矩形
GLfloat vBlock[] = { 0.25f, 0.25f, 0.0f,
0.75f, 0.25f, 0.0f,
0.75f, 0.75f, 0.0f,
0.25f, 0.75f, 0.0f};
greenBatch.Begin(GL_TRIANGLE_FAN, 4);
greenBatch.CopyVertexData3f(vBlock);
greenBatch.End();
GLfloat vBlock2[] = { -0.75f, 0.25f, 0.0f,
-0.25f, 0.25f, 0.0f,
-0.25f, 0.75f, 0.0f,
-0.75f, 0.75f, 0.0f};
redBatch.Begin(GL_TRIANGLE_FAN, 4);
redBatch.CopyVertexData3f(vBlock2);
redBatch.End();
GLfloat vBlock3[] = { -0.75f, -0.75f, 0.0f,
-0.25f, -0.75f, 0.0f,
-0.25f, -0.25f, 0.0f,
-0.75f, -0.25f, 0.0f};
blueBatch.Begin(GL_TRIANGLE_FAN, 4);
blueBatch.CopyVertexData3f(vBlock3);
blueBatch.End();
GLfloat vBlock4[] = { 0.25f, -0.75f, 0.0f,
0.75f, -0.75f, 0.0f,
0.75f, -0.25f, 0.0f,
0.25f, -0.25f, 0.0f};
blackBatch.Begin(GL_TRIANGLE_FAN, 4);
blackBatch.CopyVertexData3f(vBlock4);
blackBatch.End();
绘制
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 0.5f };
GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };
GLfloat vBlue[] = { 0.0f, 0.0f, 1.0f, 1.0f };
GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };
//召唤场景的时候,将4个固定矩形绘制好
//使用 单位着色器
//参数1:简单的使用默认笛卡尔坐标系(-1,1),所有片段都应用一种颜色。GLT_SHADER_IDENTITY
//参数2:着色器颜色
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vGreen);
greenBatch.Draw();
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
redBatch.Draw();
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vBlue);
blueBatch.Draw();
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vBlack);
blackBatch.Draw();
颜色混合
//组合核心代码
//1.开启混合
glEnable(GL_BLEND);
//2.开启组合函数 计算混合颜色因子
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
//3.使用着色器管理器
//*使用 单位着色器
//参数1:简单的使用默认笛卡尔坐标系(-1,1),所有片段都应用一种颜色。GLT_SHADER_IDENTITY
//参数2:着色器颜色
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
//4.容器类开始绘制
squareBatch.Draw();
//5.关闭混合功能
glDisable(GL_BLEND);