OpenGL与OpenGL在移动端的应用

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


image.png

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)

image.png

为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵,上图也有所表示,下面我们来讲解一下这三个矩阵。

投影矩阵

投影矩阵分为正交投影和透视投影,具体就不分析了,他们的区别就是:
正射投影矩阵直接将坐标映射到屏幕的二维平面内,从人的视觉效果出发,将会产生不真实的结果,而透视投影远处的顶点看起来比较小,符合人眼看物体近大远小的效果,所以我们一般使用可能会选透视投影

image.png

image.png

在代码里表示如下:

    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);
观察矩阵(摄像机矩阵)

如下图,我们可以直观理解为,摄像机即我们的眼睛,眼睛可能会动,则看到的物体也可能会变化。


image.png

在代码里表示如下:

    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);
模型矩阵

模型矩阵即物体相对于自身变化,如图:

image.png

我们可以看到图中茶壶先旋转再平移与先平移再旋转最终的结果是不一样的,因为它都是基于物体本身,学过线性代数我们会知道矩阵乘法不满足交换律。模型矩阵在代码里表示如下:

    _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可编程渲染管线


image.png

图中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都非常相似,再学习成本不会很大。


截屏2019-11-07下午8.32.41.png

如下两图是苹果渲染绘制框架的变化(OpenGL ES -> Metal)


image.png
image.png
七:最后我们从代码视角来看一下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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349