一、 OpenGL简介
- OpenGL(Open Graphics Library)是一个跨编程语言、跨平台的编程图形程序接口,它将计算机的资源抽象称为一个个OpenGL的对象,对这些资源的操作抽象为一个个的OpenGL指令
- OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计,去除了许多不必要和性能较低的API接口。
-
DirectX:是由很多API组成的,DirectX并不是一个单纯的图形API. 最重要的是DirectX是属于Windows上一个多媒体处理API.并不支持Windows以外的平台,所以不是跨平台框架. 按照性质分类,可以分为四大部分,显示部分、声音部分、输入部分和网络部分.
- 显示部分担任图形处理的关键,分为DirectDraw(DDraw)和Direct3D(D3D),前者主要负责2D图像加速。后者则主要负责3D效果的显示,比如CS中的场景和人物、FIFA中的人物等等,都是使用了DirectX的Direct3D。
- 声音部分中最主要的API是DirectSound,除了播放声音和处理混音之外,还加强了3d音效,并提供了录音功能
- Metal: Apple为游戏开发者推出了新的平台技术 Metal,该技术能够为 3D 图像提高 10 倍的渲染性能.Metal 是Apple为了解决3D渲染而推出的框架,在2014年以前苹果一直沿用OpenGL ES 来解决底层渲染.而后开始慢慢将自身的底层框架的依赖从OpenGL ES迁移到Metal.但其核心的处理思想还是源于OpenGL ES.对于适应于OpenGL ES的开发者而言并没有太大的改变.
引言:
从2014年到2018年.苹果才完成了系统内部从OpenGL ES过度到Metal.直到WWDC 2018,Apple 宣布 iOS 12 将弃用 OpenGL / CL.
二、OpenGL使用场景
- 在游戏开发中,对于游戏场景/游戏人物的渲染
- 在音视频开发中,对于视频解码后的数据渲染
- 在地图引擎,对于地图上的数据渲染
- 在动画中,实现动画的绘制
- 在视频处理中,对于视频加上滤镜效果
- 等等......
三、OpenGL专业名词解析
-
OpenGL 上下文(context)
在应用程序调用任何OpenGL的指令之前,需要安排首先创建一个OpenGL的上下文。这个上下文是一个非常庞大的状态机,保存了OpenGL中的各种状态,这也是OpenGL指令执行的基础。
OpenGL的函数不管在哪个语言中,都是类似C语言一样的面向过程的函数,本质上都是对OpenGL上下文这个庞大的状态机中的某个状态或者对象进行操作,当然你得首先把这个对象设置为当前对象。因此,通过对OpenGL指令的封装,是可以将OpenGL的相关调用封装成为一个面向对象的图形API的。
由于OpenGL上下文是一个巨大的状态机,切换上下文往往会产生较大的开销,但是不同的绘制模块,可能需要使用完全独立的状态管理。因此,可以在应用程序中分别创建多个不同的上下文,在不同线程中使用不同的上下文,上下文之间共享纹理、缓冲区等资源。这样的方案,会比反复切换上下文,或者大量修改渲染状态,更加合理高效的。
-
OpenGL 状态机
状态机是理论上的一种机器,我们可以把状态机描述成一个对象在其生命周期中所经历的各种状态,状态间的转变,发生转变的动因,条件及转变中所执行的活动。或者说是一种行为,说明对象在其生命周期中响应事件所经历的状态序列以及那些状态事件的响应。因此具有以下特点:
- 有记忆功能,能记住当前的状态。
- 可以接收输入,根据输入的内容和自己的原先状态,修改自己当前状态,并且可以有对应输出
- 当进入特殊状态(停机状态)的时候,将不再接收输入,停止工作。
在OpenGL中
- OpenGL可以记录自己的状态(如当前所使用的颜色,是否开启了混合功能等)
- OpenGL中可以接收输入(当调用OpenGL函数的时候,实际上可以看成OpenGL在接收我们的输入),我们调用glColor3f,则OpenGL接收到这个输入后会就修改自己的“当前状态”这个状态。
- OpenGL可以计入停止状态,不再接收输入。程序在退出前,OpenGL总会先停止工作的。
我们可以使用
glColor*函数
来选择一种颜色,以后绘制的所有物体颜色都是这种颜色,除非再次使用glColor*函数
重新设置。可以使用
glColor*函数
来选择一种颜色,以后绘制的所有物体都是这种颜色,除非再次使用glColor*函数
重新设定。可以使用
glTexCoord*函数
来设置一个纹理坐标,以后绘制的所有物体都是采用这种纹理坐标,除非再次使用glTexCoord*函数
重新设置。可以使用
glBlendFunc函数
来指定混合功能的源因子和目标因子,以后绘制的所有物体都是采用这个源因子和目标因子,除非再次使用glBlendFunc函数
重新指定。可以使用
glLight*函数
来指定光源的位置、颜色,以后绘制的所有物体都是采用这个光源的位置、颜色,除非再次使用glBlendFunc函数
重新指定。
OpenGL是一个状态机,它保持自身的状态,除非用户输入一条命令让它改变状态。
例如:
//获取是否深度测试/混合
glIsEnabled(GL_DEPTH_TEST);
glIsEnabled(GL_BLEND);
//开启深度测试/混合
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
//关闭深度测试/混合
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
-
渲染(Rendering)
将图形/图像数据转换成3D空间图像叫做渲染(Rendering)
-
顶点数组(VertexArray)和顶点缓冲区(VertexBuffer)
画图一般是先画好图像的骨架,然后在往骨架中填充颜色,对于OpenGL也是一样的。顶点数据就是要画的骨架,和现实中不同的是,OpenGL中的图像都是由图元组成。在OpenGLES中,有三种类型的图元:点、线、三角形。这些顶点数据一般存储在内存中。开发者可以选择函数指针,在调用绘制方法的时候,直接由内存传入顶点数据。这部分被称为顶点数组。一般性能高的做法是提前分配一块显存,将顶点数据预先传入到显存中去,这部分显存就称作顶点缓冲区。
顶点是指我们在绘制一个图形的时候,他在顶点位置的数据,而这个数据一般是存储在内存数组中或者将其缓存到GPU内存中。
-
管线
OpenGL中渲染图形一般会经历一个一个的节点,一般我们把这些节点称作为管线,可以想象成流水线,每个任我类似流水线一般执行,任务之间有先后顺序。管线是一个抽象的概念,之所以成为管线是因为显卡在处理数据的时候是按照一个固定的顺序来的,而且严格按照这个顺序执行。
-
固定管线
在早期OpenGL版本,它封装了很多着色器程序内置的一端包含光照、坐标变换、剪裁等等诸多功能的固定shader程序来完成,来帮助开发者来完成图形的渲染,而且开发者只需要传入相应的参数,就能快速的完成图形的渲染,类似于iOS开发中封装的API,我们只需要调用,就可以实现相应的功能,不需要关注底层的原理。
但是OpenGL使用场景非常丰富,固定管线或者存储着色器无法完成每一个业务,这时我们就需要开放成可编程的。
着色器程序(Shader)
就全面的将固定渲染管线架构面成了可编程的渲染管线。因此,OpenGL在实际调用绘制函数之前,我们需要制定一个shader编译成的着色器程序。常见的着色器有顶点着色器(VertexShader)
、片段着色器(fragmentShader)/片元着色器(fragmentShader)/像素着色器(PixelShader)
、几何着色器(GeimetryShader)
、曲面细分着色器(TessellationShader)
。直到OpenGL3.0,只支持顶点着色器和片元着色器。
OpenGL在处理shader是,和其他编程一样。通过编译、链接等步骤,生成了着色器程序(glProgram),着色器程序同时包含了顶点着色器和片元着色器的运算逻辑。在OpenGL进行绘制的时候,首先由顶点着色器对传入的顶点数据进行计算,在进行图元装配,将顶点转换为图元。然后进行光栅化处理,将图元这种矢量图形转换为栅格化数据。最后,将栅格化数据传入片元着色器中进行运算。片元着色器会对栅格化数据的每一个像素进行运算,并决定像素的颜色。
-
顶点着色器(VertexShader)
一般用于处理每个图形的顶点变换的(旋转/平移/投影等)
顶点着色器是OpenGL中可用于计算顶点属性的程序。顶点着色器是逐顶点运算的程序。也就是每个顶点都会执行一次顶点着色器,当然这是并行的,并且顶点着色器运算过程中无法访问其他顶点数据。
一般来说典型的粗腰计算的顶点属性质保包括顶点坐标变换、顶点光照运算等。顶点坐标由自身坐标系转换归一化坐标系的计算,就是在这里发生的。
-
片段着色器(fragmentShader)
一般用于处理图形中每个像素的颜色的计算和填充
片段着色器是OpenGL中用于计算片段(像素)颜色的程序。片段着色器是逐像素运算的程序,也就是说每个像素都会执行一次片段着色器,当然也是并行的。
-
GLSL(OpenGL Shading Language)
OpenGL着色语言(OpenGL shading Language)是用来在OpenGL中着色编程的语言,也是开发者写的自定义程序,他们实在图形的GPU上执行的。代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器)
-
光栅化(Rasterization)
是把顶点数据转换成片元的过程。具有将图形化为一个个栅格组成的图像的作用,特点是每个元素对应帧缓冲中的每一个像素。
光栅化就是把顶点数据转换成片元的过程。片元中的每一个元素对应缓冲区中的一个像素。
光栅化其实就是一种将几何图元变成二位图像的过程。该过程包含了两个部分。第一部分:决定窗口坐标中那些整型栅格区域被基本图元占用;第二部分:分配一个颜色值和深度值到各个区域。
把物体的数学描述以及与物体相关的颜色信息转换为屏幕上用于对应位置额的像素及用于填充像素的颜色,这个过程称作光栅化。
-
纹理
纹理我们可以理解为图片。在渲染图形中需要编码填充图片,为了使得场景更加逼真 而这里使用的图片就是常说的纹理,但是在OpenGL,我们更常叫纹理
-
混合(Blending)
在测试阶段之后,如果像素依然没有被剔除,那么像素的颜色将会和帧缓冲区中颜色附着上的颜色进行混合,混合的算法可以通过OpenGL的函数进行指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,一般可以通过像素着色器进行实现,当然性能会比原生的混合算法差一些。
-
变换矩阵
例如图形想发生平移,缩放,旋转变换.就需要使用变换矩阵.
-
投影矩阵(Projection)
用于将3D坐标转换为二维屏幕坐标,实际线条也将在二维坐标下进行绘制.
-
渲染上屏/交换缓冲区(SwapBuffer)
渲染缓冲区一般映射的是系统的资源比如窗口。如果将图像直接渲染到窗口对应的渲染缓冲区,则可以将图像显示到屏幕上。
但是,值得注意的是,如果每个窗口只有一个缓冲区,那么在绘制过程中屏幕进行了刷新,窗口可能显示出不完整的图像。
为了解决这个问题,常规的OpenGL程序至少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在一个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。
由于显示器的刷新一般是逐行进行的,因此为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进行交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步。
使用了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进行下一帧的渲染,使得帧率无法完全达到硬件允许的最高水平。为了解决这个问题,引入了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,而垂直同步发生时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利用硬件性能的目的。
OpenGL坐标系
2D笛卡尔坐标系
在二维绘图中,最为常用的坐标系统是笛卡尔坐标系统. 笛卡尔坐标由一个X和一个Y坐标构成.X表示水平方向位置,Y表示垂直方向的位置.
3D笛卡尔坐标系
将2D笛卡尔坐标系系统扩展到三维空间中.从水平和垂直方向,增加了深度分量.
增加的深度分量用Z来表示,Z轴同时垂直于X,Y轴.它代表的是一条从屏幕中心朝向读者的直线.
为了更好的观察,我们将Y轴向做旋转,把X轴向下和后渲染.否则Z轴将直面我们,我们无法具体观察到Z轴存在.
现在我们可以用3个坐标(X,Y,Z)来指定三维空间中的一个位置.
视口
窗口是以像素为单位度量. 在开始在窗口中绘制点,线,形状之前,必须告诉OpenGL 如何把指定坐标映射为屏幕坐标.
坐标系统必须从逻辑笛卡尔坐标映射到物理屏幕像素坐标. 这个映射是通过一种叫做视口(viewPort
)的设置来指定.
在我们代码中,我们会通过glViewPort
函数来实现视口的设计. 视口就是窗口内部用于绘制裁剪区域的客户区域.
投影:从3D到2D
不管我们觉得自己的眼睛看到的三维立体图像多么真实.屏幕上像素实际上只有二维的. 我们的手机屏幕本身就是二维的,我们是无法真实去呈现立体图形.
那么OpenGL 是如何将笛卡尔坐标系映射成可以在屏幕上显示的二维坐标的?
在这里需要用到投影.我们需要指定投影空间,指定在窗口显示的视景体(Viewing Volume).并指定如何对它进行变换.
举例子: 类似于立体图像站在镜子前.
投影分为2种.
- 第一种正投影(Orthographics Projection)或平行投影. 使用正投影时,需要指定一个正方形/长方形的视景体. 在视景体以外的任何物体都不会被绘制. 并且使用正投影所以实际大小相同的物体在屏幕上都具有相同的大小.不管它们是否存在远近问题. 正投影比较适合平面图形/2D图形渲染时使用.
- 第二种透视投影(Perspective Projection).它在3D开发中更为常见. 同样需要指定视景体的.而这个视景体并不是类似于正方体,看起来像平截体. 透视投影一般会使用于3D图像渲染.因为它会更加逼真.
左手坐标系和右手坐标系
注意
OpenGL
中坐标系OpenGL
中的物体、世界、照相机坐标系都属于右手坐标系,而规范化设备坐标系使用左手坐标系。笼统地说OpenGL
使用右手坐标系是不合适的。
坐标系
OpenGL 希望每次顶点着色后,我们的可见顶点都为标准化设备坐标(Normalized Device Coordinate,NDC)。也就是说每个顶点的z,y,z都应该在−1到1之间,超出这个范围的顶点将是不可见的。
通常情况下我们会自己设定一个坐标范围,之后再在顶点着色器中将这些坐标变换为表转化设备坐标。然后这些标化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标和像素。
将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
就是一个顶点在最终被转化为片段之前需要经历的所有不同状态.为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。物体顶点的起始坐标再局部空间(Local Space),这里称它为局部坐标(Local Coordinate),它在之后会变成世界坐标(world Coordinate),观测坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Corrdinate)的形式结束
在3D图形学中常用的坐标系:
- 世界坐标系
- 物体坐标系
- 摄像机坐标系
- 惯性坐标系
世界坐标系: 世界坐标系是系统的绝对坐标系,在没有建立用户坐标系之前画面上所有的点的坐标都可以在该坐标系的原点来确定各自的位置.世界坐标系始终是固定不变的
物体坐标系: 每个物体都有他们独立的坐标系.当物理移动或者改变方向时.该物体相关联的坐标系将随之移动或改变方向
物体坐标系是以物体本身而言,比如,我先向你发指令,“向前走一步”,是向您的物体坐标体系指令。我并不知道你会往哪个绝对的方向移动。比如说,当你开车时,有人会说向左转,有人说向东。但是,向左转是物体坐标系的概念,而向东则是世界坐标系中的。
在某种情况下,我们可以理解物体坐标系为模型坐标系。因为模型顶点的坐标都是在模型坐标系中描述的。
摄像机(照相机)坐标系:在坐标系的范畴里,摄像机坐标系和照相机坐标系都是一样的意义。照相机坐标系是和观察者密切相关的坐标系。照相机坐标系和屏幕坐标系相似,差别在于照相机坐标系处于3D空间中,而屏幕坐标系在2D平面里。
惯性坐标系: 指的是世界坐标系到物体坐标系的"半途". 惯性坐标系的原点和物体坐标原点重合,但惯性坐标系的轴平行于世界坐标系的轴.
为什么要引入惯性坐标系?因为物体坐标系转换到惯性坐标系只需要旋转,从惯性坐标系转换到世界坐标系只需要平移.
坐标变换的全局图
在上面的图中,注意,OpenGL只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系,它们的关系如下图所示
图中左边的过程包括模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成;
图中右边的两个步骤,包括透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。
将坐标系统组合在一起
OpenGL
然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL
会使用 glViewPort
内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点。这个过程称为视口变换