今天我们讲一下OpenGL与OpenGL在移动端的应用 OpenGL,Open Graphics Library,开放式图形库,就是一个库,与我们平时使用的三方库差不多。 OpenGL在移动端的表现形式为OpenGLES(OpenGL for Embedded Systems),是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。
好,进入正题,以下讲解以iOS为主,有别的端的同学可能友好性不够。
第一,我们来讲下写一个openGLES代码的基本流程。
image.png
image.png
在iOS里,渲染最直接的表现形式是UIView,像layer,CGContext等也得基于它,openGLES同样,大家不用把它想复杂,跟咱们正常的代码习惯差不多。
所以开始,我们需要新建一个继承于UIView的类,接下来是重写这个子类View的 (Class)layerClass{}
类方法,这个方法默认返回的是[CALayer Class]
,我们使用openGLES,这里必须得返回[CAEAGLLayer class]
,这也没什么好说的,规定,我们继续完善这个CAEAGLLayer,看以下的layer属性设置。看一下就行吧,它设置的属性是不维持渲染内容和颜色格式为RGBA8,从CAEAGLLayer可以看出,CA嘛,它也属于Core Animation。
_eaglLayer = (CAEAGLLayer *)self.layer;
_eaglLayer.opaque = YES;
_eaglLayer.drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking:@(NO),
kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8
};
_eaglLayer.contentsScale = screenScale;
做完了这一步,我们需要配置一下openGLES的渲染上下文EAGLContext
,
_context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
[EAGLContext setCurrentContext:_context];
api我们可选ES1、ES2、ES3,大家可以根据自己需求选择,虽然ES1可能大家不会用到吧,EAGLContext这个就是openGLES的渲染上下文。
接下来是OpenGL很重要的一部分buffer,renderBuffer和frameBuffer,实际上我们还可能用到别的buffer,如depthBuffer等,它们也都属于renderBuffer。
renderBuffer:renderbuffer对象是应用程序分配的2D图像缓冲区。renderbuffer可以用来分配和存储颜色、深度或模板值,渲染缓冲区类似于屏幕外窗口系统提供的可绘制表面。简单来说就是渲染图形的基本样子。
frameBuffer:framebuffer对象(通常称为FBO)是颜色、深度和模板缓冲区连接点的集合;描述附加到FBO的颜色、深度和模板缓冲区的大小和格式等属性的状态;以及附加到FBO的纹理和renderbuffer对象的名称。
简单来理解frameBuffer像是一个管理者,管理着所有支撑渲染的RenderBuffers和Textures(纹理),FBO有很多Attachment Point,我们使用Attachment让真正起作用的、具有实际内存空间占用的Renderbuffer和Texutures依附在FBO上。如下,我们给frameBuffer依附的是renderBuffer,纹理texture暂且放一放,有机会再说。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer);
做完上面这些,就是最后一步了,绘制我们想要绘制的图形。
我们设置清屏颜色,这个跟UIView的backgroudColor差不多,可以理解为openGL的背景色,清屏颜色默认是黑色,它的代码表现为
glClearColor(0.3, 0.5, 0.8, 1.0);
RGBA型的。
然后是glClear(GL_COLOR_BUFFER_BIT);
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
最后是[_context presentRenderbuffer:_renderBuffer];
,把renderBuffer渲染到上下文中,将最终渲染结果提交。做了这一步,我们的OpenGLES绘制就会有结果,虽然只是一个清屏颜色即背景色。
到此,openGLES绘制的基本流程就完事了,我们回头来看一下,实际上流程就几步:
View—>Layer—>Context—>Buffer—>Render
而且这几步也非常简单,所以大家想要做一个openGLES的应用不要想得太复杂,流程还是很简单的。
想要做一些稍复杂功能,咱们也就是往这个流程里再添加代码而已。这里提一下,可能涉及到的如:着色器程序配置、深度缓存、颜色混合、纹理处理、矩阵变换处理等,需要什么往里加就是啦,不用担心。
好,基本流程讲完了,接下来我们说一些基础概念吧。 来,我们讲一下openGL的坐标系统。 在绘制图形之前,我们必须先给OpenGL输入一些顶点数据。(VA0,VBO),OpenGL是一个3D图形库,所以我们在OpenGL中,指定的所有坐标都是3D坐标(x、y、z坐标)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(即x、y和z)上都为-1.0到1.0的范围内时才处理它。这就是标准化设备坐标,只有在这个范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。所以这当中肯定涉及坐标的变换。
我们要画的物体通常自己有一个坐标的范围,如一个建筑,它本身的坐标可能是实际的长宽高,我们拿到它的长宽高,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素展示出来。
将原始坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程,这个过程涉及以下五个重要的坐标系统:
1.局部空间(Local Space,或者称为物体空间(Object Space)),即物体本身的坐标(如建筑的长宽高)
2.世界空间(World Space),(如建筑在某个区域的x,y,z,如在一个广场它的坐标和在一个市它的坐标,这是不一样的)
3.观察空间(View Space,或者称为视觉空间(Eye Space)) (我们眼睛看起来这个建筑的xyz坐标,大家可以想象一下)
4.裁剪空间(Clip Space) (这是什么意思呢,由于视觉效果,有可能物体有些不在视觉范围内,导致只能看到部分物体,如我们在一栋楼下,看不全整栋楼)
5.屏幕空间(Screen Space) (屏幕空间即正在展示给我们的看的,在设备上就是设备屏幕)。
这个过程涉及几个矩阵,模型矩阵(Model),观察矩阵(View),投影矩阵(Projection),即MVP矩阵。
最后的裁剪空间,视口(ViewPort),所以我们在写代码的时候多半会有这么一句代码:glViewport(0, 0, self.frame.size.width,self.frame.size.height);
用来确定可视空间。
接下来我们简单说说MVP这三个矩阵:
投影矩阵 投影矩阵分为正交投影和透视投影,具体就不分析了,他们的区别就是: 正交投影矩阵直接将坐标映射到屏幕的二维平面内,从人的视觉效果出发,将会产生不真实的结果,而透视投影远处的顶点看起来比较小,符合人眼看物体近大远小的效果,所以我们一般使用可能会选透视投影。我们来看一个直观的效果。
image.png
,如图,投影矩阵确实更符合人的视觉效果,再看现实中一个很好的例子,火车轨道,两轨之间距离看起来就是近大远小,实际上是一样的:
image.png
投影矩阵在代码中表现如下:
代码语言:javascript复制 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);
GLKMatrix4MakePerspective(float fovyRadians, float aspect, float nearZ, float farZ)
fovyRadians是视角,它接受一个弧度值, aspect视图宽高比,nearZ近视点,farZ远视点。
glUniformMatrix4fv
是把矩阵信息传递到我们的着色器程序里去,是咱们代码和着色器代码的沟通桥梁,类似的还有挺多,如把顶点信息、颜色信息等传入着色器程序,这里就小提一下吧。
观察矩阵(摄像机矩阵) 如下图,我们可以直观理解为,摄像机即我们的眼睛,眼睛会动,看到的物体也会变化,大家应该都能理解吧,就是你从不同角度方位看物体,物体给你的展现是不一样的。
image.png
观察矩阵在代码中的表现如下:
代码语言:javascript复制 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);
代码语言:javascript复制GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,float centerX, float centerY, float centerZ, float upX, float upY, float upZ)
此方法表示eye在(eyeX,eyeY,eyeZ)的坐标,望向(centerX,centerY,centerZ),同时eye坐标系的y轴正方向为(upX,upY,upZ),像我这段代码里,眼睛坐标xyz望向物体的中心坐标xyz,eye坐标系的y轴正方向(0,1,0)。
模型矩阵 模型矩阵即物体相对于自身变化,如图:
image.png
我们可以看到图中茶壶先旋转再平移与先平移再旋转最终的结果是不一样的,因为它都是基于物体本身,学过线性代数我们会知道矩阵乘法不满足交换律。模型矩阵在代码里表示如下:
代码语言:javascript复制 _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);
这好像就没什么好说的了,基于自身的平移旋转缩放,我们普通的view有这样做过,这里就不多做解释了。 好了,关于矩阵的介绍我们差不多就说到这里,实际大家根据自己的需求用不同矩阵。
接下来,我们说一下着色器这个概念。我们说一说顶点着色器和片元着色器。 在 openGL 编程中顶点着色器是必须的,我们开始没用是因为我们还没绘制图形呢,顶点着色器的功能有: 1.使用矩阵进行顶点位置变换 2.法线变换,法线工规范化 3.纹理坐标生成和变换 4.计算每个顶点的光照 5.颜色计算 实际上就是对顶点数据做一些处理。
代码语言:javascript复制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);
}
我们看这段代码,这段代码里有输入的顶点数据Position,输出的顶点数据gl_Position,gl_Position是经过一些变换的,如这段代码中根据外部变量isLocate做的不同变化,处理成屏幕上的坐标。我们也可以看到,它的变换经历了透视投影矩阵、观察矩阵和模型矩阵。 这段代码里也有纹理坐标的输入和输出,它不像顶点坐标是xyzw四维向量,它只有xy两个坐标,输入是TexCoordIn,输出是TexCoordOut,1.0-TexCoordIn.y是因为纹理坐标的y坐标与我们设备屏幕的y坐标相反,它的y由上到下为负,是笛卡尔坐标系,我们屏幕由上到下为正。
再来看看片元着色器: 片元着色器就是把顶点着色器的数据处理成实际屏幕坐标上的像素颜色 片元着色器的功能如下: 1.计算颜色 2.获取纹理值 3.往像素点中填充颜色值(纹理值/颜色值) 此图是一个自定义的Fragment.glsl:
代码语言:javascript复制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;
}
}
片元着色器里得指定数据的精度,所以有precision mediump float;
这一行指定数据类型为float,中等精度,当然还有低和高精度,不同的精度消耗的性能不一样。
这个片元着色器是输出颜色的,vec4 OutColor
,输出的是rgba。我们可以在这里对颜色做一些处理修改等操作。
接下来,我们得说一下在openGL里非常重要的可编程渲染管线这个概念,看这个图:
1).Vertex Array/Buffer objects 顶点数据来源,这是渲染管线的顶点输入,VAO VBO是顶点存储的不同样式,他们在绘制时的方法也不一样。
2).Vertex Shader 顶点着色器通过矩阵变换位置、计算照明公式来生成逐顶点颜色已经生成或变换纹理坐标等基于顶点的操作。
3).Primitive Assembly 图元装配经过着色器处理之后的顶点在图片装配阶段被装配为基本图元。OpenGL ES 支持三种基本图元:点,线和三角形,它们是可被 OpenGL ES 渲染的。
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 中存储这可以用于渲染到屏幕或纹理中的像素值。
总结一下: 1.我们的顶点数据经过顶点着色器处理,变换成我们绘制想要的顶点数据; 2.再用图元装配,这些顶点该用点线还是三角形装配; 3.接下来就是光栅化,把图形变成我们可以在屏幕上展示的像素,它包含坐标颜色等; 4.再经过片元着色器,对这些顶点、颜色等做我们想要的效果; 5.接着通过Per-Fragment Operations (逐片段操作),是否要对绘制的图形做深度、裁剪或是混合等; 6.处理完最后提交得到我们最终要渲染的像素,通过提交就能得到我们想要绘制的图形。
在最后,我们来看一点代码,流程一般也就是这样的
代码语言:javascript复制[self setupLayer];
[self setupContext];
[self setupDepthBuffer];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self setupProgram]; //配置program
[self setupMatrix];
[self setupRenderData];
[self render];
看一点点render里的处理吧:
代码语言:javascript复制-(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];//调用这句话,图形才能渲染到屏幕上
}