OpenGL首先我们从字面意思来理解:Open Graphics Library,开放的图形库,图形库自然是处理图形的,所以简单来说OpenGL就是用来处理图形的一个三方库。
稍微技术流一点,作如下解释:是用于渲染2D,3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。
OpenGL在移动端的表现形式为OpenGLES,OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。
接下来我们从openGL在移动端的应用为入口,探一探它的奥秘。(以iOS平台为例)
一.用openGLES绘制图形的基本流程
1.UIView,要展示图形,还是需要基本的承载视图,UIView
2.layer,OpenGLES的描绘必须在CAEAGLLayer上才能显示出来,所以我们需要重写这个函数,修改view默认的layer返回类型,从CAEAGLLayer可以看出,它也属于Core Animation。
+(Class)layerClass{//默认是CALayer
//OpenGL内容只会在此类layer上描绘
return [CAEAGLLayer class];
}
3.context,EAGLContext对象是管理OpenGL ES渲染上下文,若想使用OpenGL ES 进行绘制工作,则必须有一个上下文对象.
-(void)setupLayerAndContext
{
_eaglLayer = (CAEAGLLayer *)self.layer;
_eaglLayer.opaque = YES;
_eaglLayer.drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking:@(NO),
kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8
};
_eaglLayer.contentsScale = screenScale;
// 指定 OpenGLES 渲染API的版本,在这里我们使用OpenGLES 3.0,由于3.0兼容2.0并且功能更强,为何不用更好的呢
//注:在iOS上,可以支持opengles3.0的最低环境是iphone5s ios7.0.
_context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
[EAGLContext setCurrentContext:_context];
}
4.buffer,renderBuffer和frameBuffer
renderBuffer:renderbuffer对象是应用程序分配的2D图像缓冲区。renderbuffer可以用来分配和存储颜色、深度或模板值,也可以用作framebuffer对象中的颜色、深度或模板附件。渲染缓冲区类似于屏幕外窗口系统提供的可绘制表面,例如pbuffer。但是,渲染缓冲区不能直接用作GL纹理。
frameBuffer:framebuffer对象(通常称为FBO)是颜色、深度和模板缓冲区连接点的集合;描述附加到FBO的颜色、深度和模板缓冲区的大小和格式等属性的状态;以及附加到FBO的纹理和renderbuffer对象的名称。可以将各种2D图像附加到framebuffer对象中的颜色附着点。这些包括存储颜色值的renderbuffer对象、二维纹理或cubemap面的mip级别,甚至三维纹理中的二维切片的mip级别。类似地,各种包含深度值的2D图像可以附加到FBO的深度附着点。这些可以包括一个renderbuffer,一个二维纹理的mip级,或者一个存储深度值的cubemap面。唯一可以附加到FBO模板附着点的2D图像是一个存储模板值的renderbuffer对象。
-(void)setupRenderBuffer{
glGenRenderbuffers(1, &_renderBuffer); //生成和绑定render buffer的API函数
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//为其分配空间
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
}
-(void)setupFrameBuffer{
glGenFramebuffers(1, &_frameBuffer); //生成和绑定frame buffer的API函数
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
//将renderbuffer跟framebuffer进行绑定
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
}
5.绘制渲染
-(void)render
{
//设置清屏颜色,默认是黑色,如果你的运行结果是黑色,问题就可能在这儿
glClearColor(0.3, 0.5, 0.8, 1.0);
/*
glClear指定清除的buffer
共可设置三个选项GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT
也可组合如:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
这里我们只用了color buffer,所以只需清除GL_COLOR_BUFFER_BIT
*/
glClear(GL_COLOR_BUFFER_BIT);
[_context presentRenderbuffer:_renderBuffer];
}
OK,到此我们就算是完成一个OpenGL ES的简单流程了。是不是特别简单呢?接下来我们讲讲坐标系统、着色器、渲染管线。
二.坐标系统
开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。这就是标准化设备坐标,只有在这个范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。
我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程,这个过程涉及以下五个重要的坐标系统:
局部空间(Local Space,或者称为物体空间(Object Space))
世界空间(World Space)
观察空间(View Space,或者称为视觉空间(Eye Space))
裁剪空间(Clip Space)
屏幕空间(Screen Space)
为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵,上图也有所表示,下面我们来讲解一下这三个矩阵。
投影矩阵
投影矩阵分为正交投影和透视投影,具体就不分析了,他们的区别就是:
正射投影矩阵直接将坐标映射到屏幕的二维平面内,从人的视觉效果出发,将会产生不真实的结果,而透视投影远处的顶点看起来比较小,符合人眼看物体近大远小的效果,所以我们一般使用可能会选透视投影
在代码里表示如下:
float aspect = self.frame.size.width/self.frame.size.height;
_projectionMatrix = GLKMatrix4MakePerspective(45.0*M_PI/180.0, aspect, 0.0001, 100);
glUniformMatrix4fv(_projectionSlot, 1, GL_FALSE, _projectionMatrix.m);
观察矩阵(摄像机矩阵)
如下图,我们可以直观理解为,摄像机即我们的眼睛,眼睛可能会动,则看到的物体也可能会变化。
在代码里表示如下:
eyeX = SXYZ * sinf(RZ) * cosf(RX);
eyeY = SXYZ * sinf(RX);
eyeZ = SXYZ * cosf(RZ) * cosf(RX);
eyeX += TX;
eyeY += TY;
eyeZ += TZ;
_camaraMatrix = GLKMatrix4MakeLookAt(eyeX, eyeY, eyeZ, TX, TY, TZ, 0, 1, 0);
glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, _camaraMatrix.m);
模型矩阵
模型矩阵即物体相对于自身变化,如图:
我们可以看到图中茶壶先旋转再平移与先平移再旋转最终的结果是不一样的,因为它都是基于物体本身,学过线性代数我们会知道矩阵乘法不满足交换律。模型矩阵在代码里表示如下:
_poiTitleModelViewMatrix = GLKMatrix4MakeTranslation(titlePoint.x, poiPlaneTop+0.005, titlePoint.y);
_poiTitleModelViewMatrix = GLKMatrix4RotateY(_poiTitleModelViewMatrix, RY);
_poiTitleModelViewMatrix = GLKMatrix4RotateX(_poiTitleModelViewMatrix, -RX);
camara_position_distance = sqrtf(pow(eyeX-titlePoint.x, 2)+pow(eyeY-(poiPlaneTop+0.005), 2)+pow(eyeZ-titlePoint.y, 2));
_poiTitleModelViewMatrix = GLKMatrix4Scale(_poiTitleModelViewMatrix, camara_position_distance, camara_position_distance, camara_position_distance);
三.着色器
顶点着色器(Vertex Shader)
在 openGL 编程中顶点着色器是必须的,顶点着色器的功能如下:
1.使用模型视图矩阵和投影矩阵进行顶点位置变换
2.法线变换,法线工规范化
3.纹理坐标生成和变换
4.计算每个顶点的光照
5.颜色计算
总的来说就是处理顶点和颜色数据。
如下是一个自定义的Vertex.glsl:
attribute vec4 Position;
attribute vec2 TexCoordIn;
varying vec2 TexCoordOut;
uniform bool isLocate;
uniform mat4 locateModel;
uniform mat4 Projection;
uniform mat4 View;
uniform mat4 Model;
void main(){
if (isLocate) {
gl_Position = Projection * View * locateModel * Position;
}else {
gl_Position = Projection * View * Model * Position;
}
// gl_Position = Position;
TexCoordOut = vec2(TexCoordIn.x, 1.0-TexCoordIn.y);
}
片元着色器(Fragment Shader)
片元着色器就是把顶点着色器的数据处理成实际屏幕坐标上的像素颜色
片元着色器的功能如下:
1.计算颜色
2.获取纹理值
3.往像素点中填充颜色值(纹理值/颜色值)
如下是一个自定义的Fragment.glsl:
precision mediump float;
varying mediump vec4 OutColor;
uniform bool is_side;
uniform float sideColor;
uniform bool is_sprite;
void main()
{
if (is_sprite) {
if (length(gl_PointCoord-vec2(0.5)) > 0.45) //0.5会冒出来
discard;
}else
gl_FragColor = OutColor;
gl_FragColor = vec4(gl_FragColor.r*OutColor.r,gl_FragColor.g*OutColor.g,gl_FragColor.b*OutColor.b,gl_FragColor.a*OutColor.a);
if (is_side) {
// gl_FragColor = gl_FragColor * vec4(sideColor,sideColor,sideColor,1.0);
gl_FragColor = gl_FragColor;
}
}
四.渲染管线
下图中展示整个OpenGL ES 2.0可编程渲染管线
图中Vertex Shader和Fragment Shader 是可编程管线;
1).Vertex Array/Buffer objects
顶点数据来源,这是渲染管线的顶点输入,VAO VBO是顶点存储的不同样式,他们在绘制时的方法也不一样。
2).Vertex Shader
顶点着色器通过矩阵变换位置、计算照明公式来生成逐顶点颜色已经生成或变换纹理坐标等基于顶点的操作。
3).Primitive Assembly
图元装配经过着色器处理之后的顶点在图片装配阶段被装配为基本图元。OpenGL ES 支持三种基本图元:点,线和三角形,它们是可被 OpenGL ES 渲染的。接着对装配好的图元进行裁剪(clip):保留完全在视锥体中的图元,丢弃完全不在视锥体中的图元,对一半在一半不在的图元进行裁剪;接着再对在视锥体中的图元进行剔除处理(cull):这个过程可编码来决定是剔除正面,背面还是全部剔除。
4).Rasterization
光栅化。在光栅化阶段,基本图元被转换为二维的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。
5).Fragment Shader
片元着色器通过可编程的方式实现对每个片元的操作。在这一阶段它接受光栅化处理之后的fragment,color,深度值,模版值作为输入,片元着色器可以抛弃片元,也可以生成一个或多个颜色值作为输出。
6).Per-Fragment Operations (逐片段操作)
它包含像素归属测试(Pixel Ownership Test)、裁剪测试(Scissor Test)、模板和深度测试(Stencil And Depth Test)、混合(Blending)、抖动(Dithering)这些对片段的处理。我们渲染3d图形常会用到这些。
7).Framebuffer:这是流水线的最后一个阶段,Framebuffer 中存储这可以用于渲染到屏幕或纹理中的像素值。
五.绘制
OpenGL ES可绘制的基本图元是点、线和三角形,如下我们分析一段绘制的代码(代码已经过处理):
-(void)render
{
[EAGLContext setCurrentContext:_context];
glClearColor([backColorArr[0] floatValue], [backColorArr[1] floatValue], [backColorArr[2] floatValue], 1);
// glEnable(GL_POLYGON_OFFSET_FILL); //解决z_fighting问题
// glPolygonOffset(1.0f, 1.0f);
glEnable(GL_BLEND); //允许混合
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
//MSAA处理
glBindFramebuffer(GL_FRAMEBUFFER, mMSAAFramebuffer);
glBindRenderbuffer(GL_RENDERBUFFER, mMSAARenderbuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, self.frame.size.width*2, self.frame.size.height*2);
[self updateCubeCenterToScreenPoint]; //确定边界
glUseProgram(_programHandle); //使用某个这色器程序
[self setupCubeProjectionAndCamara];
[self drawOutLine]; //画轮廓
[self drawTopPoi]; //画顶层poi 里面有多次深度测试的开启和关闭
glDisable(GL_DEPTH_TEST); //关闭深度测试
if (naviPathArr.count) {
for (int i=0; i<naviPathArr.count; i++) {
NSArray *onePath = naviPathArr[i];
[self drawNaviRoute:onePath]; //画路径和箭头
[self drawNaviArrow:onePath];
}
}
[self drawNormalOverlays]; //绘制覆盖物
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); //避免文字穿透
glUseProgram(_poiTitleProgramHandle);
[self setupTitleProjectionAndCamara];
if (isFirstDrawTile) {//如果第一次筛选,不是第一次的筛选都在手势里处理了
[self setupTitlesAndFacilyties]; //筛选文字和图例
isFirstDrawTile = NO;
}
[self drawTopPoiTexture]; //画顶面贴图
[self drawFacilities]; //画图例
[self drawPoiTitles]; //画文字
[self drawTextureOverlays]; //画纹理型覆盖物
if (isLocate) {//若需要画定位点
[self drawLocatePoint:locatePoint];
}
glUseProgram(_heatMapProgramHandle); //绘制热力图使用热力图的这色器程序
[self setupHeatMapProjectionAndCamara];
[self drawHeatMap]; //画热力图
//MSAA处理
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBuffer);
glBindFramebuffer(GL_READ_FRAMEBUFFER, mMSAAFramebuffer);
glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, (GLenum[]){GL_DEPTH_ATTACHMENT});
glBlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[_context presentRenderbuffer:GL_RENDERBUFFER];//调用这句话,图形才能渲染到屏幕上
}
六:后续
我们学习OpenGL可以懂得很多图形学上的知识,也能扩宽我们的眼界,这门技术可能跟我们工作的专业技术有较大区别,但可以给我们不一样的思想。如我是做iOS开发的,以前接触的图形上的东西就是view、layer这种,学了openGL后,会明白layer原来也是OpenGL ES的基本图元——两个三角形绘制而成。
在iOS12之后,OpenGL ES的api被废弃了,苹果还是主推他们自己研发的metal,对于OpenGL ES和metal,事实上很多api都非常相似,再学习成本不会很大。
如下两图是苹果渲染绘制框架的变化(OpenGL ES -> Metal)
七:最后我们从代码视角来看一下openGL ES绘制的整个流程
[self setupLayer];
[self setupContext];
[self setupDepthBuffer];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self setupProgram]; //配置program
[self setupProjectionMatrix];
[self setupModelViewMatrix];
[self setupRenderData];
[self render];
总结:事实上大家都在复杂化OpenGL的学习,而实际上,学习OpenGL复杂的只是需要我们多了解、先了解一些图形学知识,大量去学习OpenGL的一些理论,然后回头边学边做,后面学习实际上也差不多。而这些理论知识的学习,稍微有点基础,理解能力强点,也花不了几天时间。
参考资料:
1.苹果官网OpenGL ES Programming Guide:https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/Introduction/Introduction.html
2.LearnOpenGL-CN:https://learnopengl-cn.readthedocs.io/zh/latest/
3.渲染管线:https://www.cnblogs.com/edisongz/p/6918428.html