大橙子网站建设,新征程启航
为企业提供网站建设、域名注册、服务器等服务
作为一个一个iOS底层开发小白,一直以来对于底层原理,都是一知半解的状态,希望从此时记录自己学习底层知识的过程,也希望对其他开发中可以有一个好的帮助,因为自己看其他人的博客时候,大多数作者都是基于自己认知的前提下,记录自己的知识,在我看来对于入门的人来说,非常晦涩难懂,所以希望自己的文章,可以更加帮助其他人循序渐进的了解更多关于 iOS 的知识。文章也会尽量一步步的探索更多业务开发之外的东西,对于任何方面的技能,都可以友善的帮助后来的开发者。文章中有任何错误,不恰当的地方,欢迎随时指正,多多交流才可以共同进步。想要学习 OpenGL 首先需要了解 OpenGL/OpenGL ES/Metal 三者之间的联系:
珲春网站制作公司哪家好,找创新互联!从网页设计、网站建设、微信开发、APP开发、成都响应式网站建设公司等网站项目制作,到程序开发,运营维护。创新互联从2013年创立到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联。
通俗来讲就是, Apple 作为大厂,肯定要发展自己的底层渲染技术,所以推出了 Metal ,在推出 Metal 之前,苹果的底层渲染也是基于 OpenGL/OpenGL ES 的,在 iOS 12.0以后,苹果摒弃了 OpenGL 的相关 API ,使用 Metal 作为自己的渲染技术,但是 OpenGL 相关的 API 依然可以使用,因为在 Metal 之前,苹果提供了非常丰富的关于使用 OpenGL 相关 API ,类似于苹果推出了 swift ,但是 OC 依然是自己主流语言一样。了解了相关的背景,我们接下来要知道图形 API 究竟是用来解决什么样的问题存在的。
OpenGL/OpenGL ES/Metal 在任何项目中解决问题的本质就是利用 GPU 芯片来高效的渲染图形图像。图形 API 是 iOS 开发者唯一接近 GPU 的方式。想要了解 OpenGL 就要先学习关于 OpenGL 的专业名词,了解了这些,才可以对以后的学习,有更加深刻的认识。
状态机在 OpenGL 可以这么理解, OpenGL 可以记录自己的状态(当前所使用的颜色、是否开启了混合功能等),可以接输入(当调用 OpenGL 函数的时候,实际上可以看成 OpenGL 在接受我们的输入),如我们调用 glColor3f ,则 OpenGL 接收到这个输入后会修改自己的“当前颜色”这个状态, OpenGL 可以进入停止状态,不再接收输入。在程序退出前, OpenGL 总会先停止工作;
这里有一个 iOS 中很经常听到的概念, 离屏渲染 ,很多人知道离屏渲染会对 APP 的性能造成较大的开销,但是却不知道原理是什么,相信大家了解了上面关于 OpenGL 关于交换缓冲区的概念后,有了一个更清晰的认识,即: Off-Screen Rendering 是需要开辟新的缓冲区的,不停地切换上下文的环境则是对性能的很大的消耗,所以在 iOS 开发中,我们应当尽量的避免离屏渲染。
在学习 OpenGL 的过程中,直接非常直观的掌握并理解这些概念并不是一件容易的事,但是至少需要在入门阶段,大致的了解这些概念的意思,然后通过后续的学习,慢慢的巩固前面学到的知识,温故而知新,一步步的打开关于 iOS 底层渲染知识的大门,学习的越来越深入,慢慢的回过头看以前的知识点的时候,就会豁然开朗了。
希望能一步步的记录自己学习的过程,慢慢进步,慢慢成长,文章中有任何错误的地方,欢迎指正。希望能多多交流,共同进步。
一、搭建开发环境
1. 打开Xcode, 新建一个工程
选择:IOS - Application - Single View Application模板
输入工程名称和基本信息,勾选“UseStoryboards”,然后创建
2. 添加必要的框架
在“Build Phases”中,添加三个框架
3. 修改viewController。h
添加 “#import”,并将它修改为继承"GLKViewController"
4. 修改“view”的类
双击“MainStoryboard.storyboard”展开,选择"view"
然后,在“Identity Inspector"中,将它的类改为”GLKView“
好了,OpenGL的环境基本上搭建出来了。
二、增加自己代码
基本上,所有的代码都是加到ViewController.m文件中
1、添加全局属性声明
@interface ViewController ()@property(strong,nonatomic)EAGLContext*
context;@property(strong,nonatomic)GLKBaseEffect* effect;@end@implementation
ViewController@synthesize context, effect;
2、 添加一组顶点数据
这是一个正方形顶点数组。实际上它是二个三角形接合而成的
GLfloat squareVertexData[48] ={ 0.5f, 0.5f, -0.9f, 0.0f, 0.0f, 1.0f, 1.0f,
1.0f, -0.5f, 0.5f, -0.9f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f, -0.5f, -0.9f,
0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f, -0.5f, -0.9f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.9f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, -0.5f, -0.5f, -0.9f, 0.0f,
0.0f, 1.0f, 1.0f, 1.0f,};
每行顶点数据的排列含义是:
顶点X、顶点Y,顶点Z、法线X、法线Y、法线Z、纹理S、纹理T。
在后面解析此数组时,将参考此规则。
顶点位置用于确定在什么地方显示,法线用于光照模型计算,纹理则用在贴图中。
一般约定为“顶点以逆时针次序出现在屏幕上的面”为“正面”。
世界坐标是OpenGL中用来描述场景的坐标,Z+轴垂直屏幕向外,X+从左到右,Y+轴从下到上,是右手笛卡尔坐标系统。我们用这个坐标系来描述物体及光源的位置。
三、初始化OpenGL环境
1、 基本的初始化代码
在ViewController.m中有个函数(void)viewDidLoad,它是程序运行时,初始化回调函数。在viewDidLoad函数内补充我们自己的初始化代码。
// 使用“ES2”创建一个“EAGLEContext”实例 self.context = [[[EAGLContext
alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2]autorelease]; //
将“view”的context设置为这个“EAGLContext”实例的引用。并且设置颜色格式和深度格式。 GLKView* view =
(GLKView*)self.view; view.context = self.context; view.drawableColorFormat =
GLKViewDrawableColorFormatRGBA8888; view.drawableDepthFormat =
GLKViewDrawableDepthFormat24; //
将此“EAGLContext”实例设置为OpenGL的“当前激活”的“Context”。这样,以后所有“GL”的指令均作用在这个“Context”上。随后,发送第一个“GL”指令:激活“深度检测”。
[EAGLContext setCurrentContext:context]; glEnable(GL_DEPTH_TEST); //
创建一个GLK内置的“着色效果”,并给它提供一个光源,光的颜色为绿色。 self.effect = [[[GLKBaseEffect
alloc]init]autorelease]; self.effect.light0.enabled = GL_TRUE;
self.effect.light0.diffuseColor = GLKVector4Make(0.0f, 1.0f, 0.0f, 1.0f);
2、 运行。现在应该是粉红色屏幕了(目前场景仍是空的),说明初始化过程没问题
四、 将项点数据写入能用的顶点属性存储区
1、 写入过程
首先将数据保存进GUP的一个缓冲区中,然后再按一定规则,将数据取出,复制到各个通用顶点属性中。
注:如果顶点数据只有一种类型(如单纯的位置坐标),换言之,在读数据时,不需要确定第一个数据的内存位置(总是从0开始),则不必事先保存进缓冲区。
2、 顶点数组保存进缓冲区
//
声明一个缓冲区的标识(GLuint类型)让OpenGL自动分配一个缓冲区并且返回这个标识的值.绑定这个缓冲区到当前“Context”.最后,将我们前面预先定义的顶点数据“squareVertexData”复制进这个缓冲区中。
// 注:参数“GL_STATIC_DRAW”,它表示此缓冲区内容只能被修改一次,但可以无限次读取。 GLuint buffer;
glGenBuffers(1, buffer); glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(squareVertexData), squareVertexData,
GL_STATIC_DRAW);
3、将缓冲区的数据复制进能用顶点属性中
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 4*8,
(char*)NULL + 0);
首先,激活顶点属性(默认它的关闭的)。“GLKVertexAttribPosition”是顶点属性集中“位置Position”属性的索引。
顶点属性集中包含五种属性:位置、法线、颜色、纹理0,纹理1。
它们的索引值是0到4。
激活后,接下来使用“glVertexAttribPointer”方法填充数据。
无论是OpenGL 还是 OpenGL ES 或者 Metal ,本质上还是利用GPU来进行高效的渲染图形图像。
换句话说图形API,是我们iOS开发者唯一接近GPU的方式。
首先,“状态机”这个东西是什么,学过编译原理的同学,一定听过或了解这三个字。 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。
如果说下图“自动门”是个状态机,他 记忆 了open和close状态,知道自己处在不同状态时,下一步要干什么,是“开门”还是“关门”。 当在close状态时,你 输入 一个开门的信号,他就会切换成open状态
状态机的特点:
回到 OpenGL 状态机:
简单来说就是实现图形的底层渲染
A. 比如在游戏开发中,对于游戏场景/游戏人物的渲染
B. 比如在音视频开发中,对于视频解码后的数据渲染
C. 比如在地图引擎,对于地图上的数据渲染
D. 比如在动画中,实现动画的绘制
E. 比如在视频处理中,对于视频加上滤镜效果
OpenGL/OpenGL ES/Metal 在任何项目中解决问题的本质就是利用 GPU 芯片来高效渲染图形图像。
图形 API 是 ios 开发者唯一接近 GPU 的方式。
OpenGL 阶段:
OpenGL ES 阶段:
Metal 阶段:
固定管线/存储着⾊器
顶点数据是由CPU/GPU来处理?
顶点缓存区:区域(不在内存!-显卡显存中。)
片元着色器
像素着色器
片元函数
GPUImage
[-1,1]标准化设备坐标系(NDC)
物体/世界/照相机空间-右手系
规范化设备坐标:左手系。
x,y,z = 0,1,2
注意OpenGL中坐标系 OpenGL中的物体,世界,照相机坐标系都属于右手坐标系,而规范化设备坐标系(NDC)属于左手坐标系。笼统的说OpenGL使用右手坐标系是不合适的
OpenGL希望每次顶点着色后,我们的可见顶点都为标准化设备坐标系 (Normalized Device Coordinate, NDC)。也就是说每个顶点的x,y,z都应该在-1到1之间,超出这个范围的顶点将是不可见的。
通常情况下我们会自己设定一个坐标系范围,之后再在顶点着色器中将这些坐标系变换为标准化设备坐标,然后这些标准化设备坐标传入光栅器(Rasterizer),将他们变换为屏幕上的二维坐标和像素。
将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分布进行的,也是类似于流水线那样。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系系统(Coordinate System)。将物体的坐标变到几个过渡坐标系(Intermediate Coordinate System)的优点在于 在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就变得明显。对我们来说比较重要的总共有5个不同的坐标系统。
这是一个顶点在最终被转化为片段之前需要经历的所有不同的状态。为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是 模型(Model)、观察(View)、投影(Projection)三个矩阵。
物体顶点的起始坐标在局部空间(Local Space),这里称为局部坐标(Local Coordinate),他在之后在变成世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后转为屏幕坐标(Screen Coordinate)
的形式结束。
物体坐标系: 每个物体都有他独立的坐标系,当物理移动或者改变方向时。该物体相关联的坐标系将随之移动或改变方向。
物体坐标系是以物体本身而言,比如,我先向你发指令,”向前走一步“,是向你的物体坐标系发指令。我并不知道你会往哪个绝对的方向移动。比如说,当你开车的时候,有人会说向左转,有人会说向东。但是,向左转是物体坐标系的概念,而向东则是世界坐标系概念。
在某种情况下,我们可以理解物体坐标系为模型坐标系。因为模型顶点的坐标都是在模型坐标系中描述的。
照相机坐标系: 照相机坐标系是和观察者密切相关的坐标系。照相机坐标系和屏幕坐标系相似,差别在于照相机坐标系处于3D空间中,而屏幕坐标系在2D平面里。
为什么要引入惯性坐标系? 因为物体坐标系转换到惯性坐标系只需要旋转,从惯性坐标系转换到世界坐标系只需要平移。
OpenGL最终的渲染设备是2D的,我们需要将3D表示的场景转换为最终的2D形式,前面使用模型变换和视觉变换将物体坐标转到照相机坐标系后,需要进行投影变换,将坐标从照相机坐标系转换为裁剪坐标系,经过透视除法后,变换到规范化设备坐标系(NDC),最后进行视口变换后,3D坐标才变换到屏幕上的2D坐标,这个过程入下图:
在上面的图中, 注意,OpenGL只定义了裁剪坐标系、规范化设备坐标系、屏幕坐标系,而局部坐标系、世界坐标系、照相机坐标系都是为了方便用户设计而自定义的坐标系,他们的关系如下图:
OpenGL 然后对裁剪坐标执行透视除法从而将他们变换到标准化设备坐标。 OpenGL 会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标关联一个屏幕上的点。这个过程称为视口变换
局部坐标系(模型坐标系)是为了方便构造模型而设立的坐标系,建立模型时我们无需关心最终对象显示在屏幕那个位置。
模型变换的主要目的是通过变换使得用顶点属性定义或者3d建模软件构造的模型,能够按照需要,通过缩小、平移等操作放置到场景中合适的位置, 通过模型变换后,物体放置在一个全局的世界坐标系中,世界坐标系是所有物体交互的一个公共坐标系
视变换是为了方便观察场景中物体而建立的坐标系,在这个坐标系中相机是个假设的概念,是为了便于计算而引入的。相机坐标系中的坐标,就是从相机的角度来解释世界坐标系中的位置
OpenGL中相机始终位于原点,指向 -Z轴,而以相反的方式来调整场景中物体,从而达到相同的观察效果。例如要观察-Z轴方向的一个立方体的右侧面,可以有两种方式:
GLShaderManager的初始化
GLShaderManager shaderManager;
shaderManager.InitializeStockShaders();
既然有了 Metal 我们是否还有学习 OpenGL ES 的必要呢.我个人认为暂时还是有必要的.OpenGL /OpenGL ES/ Metal 在任何项目中解决问题的本质就是利用GPU芯片来⾼效渲染图形图像.所以它们底层的原理相近,首先了解OpenGL之后再去了解Metal会更加容易, 其二OpenGL是跨平台的框架.保不齐以后会在其他的地方用到,所谓技多不压身.毕竟不可能保证一直都做苹果开发吧.
理论枯燥且乏味, 但是我们联想一下flutter开发中用到的context, iOS开发中CoreGraphics里也有用到context. 是不是此时心中就有了一定的答案.我个人理解其设计模式大同小异. 顾名思义我们能够通过context拿到很多必要的状态和数据.
比如需要显示一个正方形, 则需要两个正等边三角形图元来完成
为了读取效率起见, 提前分配一块显存, 将顶点数组存放在显存中. 这部分显存就叫做 顶点缓冲区
OpenGL渲染的过程中会经历很多节点. 这些节点串起来就是管线.
常见的着⾊器主要有: 顶点着⾊器(VertexShader) , ⽚段着⾊器(FragmentShader) , 几何着⾊(GeometryShader) , 曲⾯细分着⾊器(TessellationShader)
OpenGL ES 中只⽀持 顶点着⾊器 和 片段着⾊器 .
光栅化就是把顶点数据转换为片元的过程。片元中的每一个元素对应于帧缓冲区中的一个像素。
如果把渲染比作是画画, 那么顶点着色器操作相当与确定画的内容的框架. 而之后往框架里填充内容的过程就是光栅化.
填充好内容之后就是片元着色器操作像素点填充颜色等操作
这里附上一张流程图:
纹理可以理解为图⽚。 在渲染图形时需要在顶点围成的区域中填充图⽚,使得场景更加逼真。⽽这⾥使⽤的图⽚,就是常说的纹理。只是在OpenGL,我们更加习惯叫纹理,⽽不是图⽚。