图像以及动画

图像处理

CoreImage

简介

Core Image 是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。
一个 滤镜 是一个对象,有很多输入和输出,并执行一些变换。例如,模糊滤镜可能需要输入图像和一个模糊半径来产生适当的模糊后的输出图像。
一个 滤镜链 是一个链接在一起的滤镜网络,使得一个滤镜的输出可以是另一个滤镜的输入。以这种方式,可以实现精心制作的效果。
iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
虽然使用CoreImage框架能够满足大部分日常图片滤镜处理之类的需求,而且是系统内置框架,性能上肯定是经过一番考究的,但是其API使用起来比较麻烦,尤其是基于对摄像头数据流的实时滤镜(目前大部分直播APP要求这个功能),所以如果项目中有这方面的需求,这里推荐一个优秀的开源框架GPUImage。

应用场景

图像处理,比如滤镜,画笔,马赛克,增高,相机,AR 贴图

示例

Metal

简介

Metal 是一个和 OpenGL ES 类似的面向底层的图形编程接口,通过使用相关的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的时候发布.

应用场景

示例

CoreGraphics

简介

Core Graphics是一个基于C的绘图专用的API族,它经常被称为QuartZ或QuartZ 2D,是一个二维绘图引擎,同时支持iOS和Mac系统。它提供了低级别、轻量级、高保真度的2D渲染。该框架可以用于基于路径的绘图、变换、颜色管理、脱屏渲染,模板、渐变
QuartZ 2D在开发中比较常用的是截屏/裁剪/自定义UI控件,QuartZ 2D在iOS开发中的主要价值是自定义UI控件
QuartZ 2D能完成的工作

(1).绘制图形: 线条\三角形\矩形\圆\弧等
(2).绘制文字
(3).绘制\生成图片(图像)
(4).读取\生成PDF
(5).截图\裁剪图片
(6).自定义UI控件

应用场景

UIBezierPath

简介

UIBezierPath属于UIKit框架,是CoreGraphics对path的封装,使用UIBezierPath可以绘制直线、矩形、椭圆、不规则图形、多边形和贝塞尔曲线等,只要是能想到的线条都能画出。

应用场景

QuartzCore

简介

QuartzCore主要结构
  1. CoreAnimation
  2. CADisplayLink定时器
  3. CALayer 及其子类(参考上方链接)
  4. CAMediaTiming协议相关
  5. CATransaction事物相关
  6. CATransform3D

可以看出所有的类和协议都以CA开头,所以可以把此框架认为就是核心动画框架

应用场景

OpenGLES

简介

OpenGL可用于渲染2D和3D图像,是一个多用途的开源图形库。OpenGL设计用来将函数命令转换成图形命令,发送到GPU中。GPU正是被设计用来处理图形命令的,所以OpenGL的绘制非常高效。
OpenGLES是OpenGL的简化版本,抛弃了冗余的文件及命令,使之专用于嵌入式设备。OpenGLES使得移动APP能充分利用GPU的强大运算能力。iOS设备上的GPU能执行更精确的2D和3D绘制,以及更加复杂的针对每个像素的图形脚本(shader)计算。

基础概念

OpenGL 是什么 ?

OpenGL(Open Graphics Library)是 Khronos Group (一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准)开发维护的一个规范,它是硬件无关的。它主要为我们定义了用来操作图形和图片的一系列函数的 API,需要注意的是 OpenGL 本身并非 API

而 GPU 的硬件开发商则需要提供满足 OpenGL 规范的实现,这些实现通常被称为”驱动“,它们负责将 OpenGL 定义的 API 命令翻译为 GPU 指令。所以你可以用同样的 OpenGL 代码在不同的显卡上跑,因为它们实现了同一套规范,尽管内部实现可能存在差异。

OpenGL ES 和 OpenGL 有什么关系 ?

OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。该规范也是由 Khronos Group 开发维护。

OpenGL ES 是从 OpenGL 裁剪定制而来的,去除了 glBegin/glEnd,四边形(GL_QUADS)、多边形(GL_POLYGONS)等复杂图元等许多非绝对必要的特性,剩下最核心有用的部分。

可以理解成是一个在移动平台上能够支持 OpenGL 最基本功能的精简规范

[图片上传失败...(image-ccd977-1595851022454)]

OpenGL ES 横跨在两个处理器之间,协调两个内存区域之间的数据交换

为什么要使用 OpenGL ES ?

通常来说,计算机系统中 CPU、GPU 是协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。所以,尽可能让 CPU 和 GPU 各司其职发挥作用是提高渲染效率的关键

正如我们之前提到过,OpenGL 正是给我们提供了访问 GPU 的能力,不仅如此,它还引入了缓存(Buffer)这个概念,大大提高了处理效率。

[图片上传失败...(image-72907a-1595851022454)]

图中的剪头,代表着数据交换,也是主要的性能瓶颈。

从一个内存区域复制到另一个内存区域的速度是相对较慢的,并且在内存复制的过程中,CPU 和 GPU 都不能处理这区域内存,避免引起错误。此外,CPU / GPU 执行计算的速度是很快的,而内存的访问是相对较慢的,这也导致处理器的性能处于次优状态,这种状态叫做“数据饥饿”,简单来说就是空有一身本事却无用武之地

针对此,OpenGL 为了提升渲染的性能,为两个内存区域间的数据交换定义了缓存。缓存是指 GPU 能够控制和管理的连续 RAM。程序从 CPU 的内存复制数据到 OpenGL ES 的缓存。通过独占缓存,GPU 能够尽可能以有效的方式读写内存。 GPU 把它处理数据的能力异步地应用在缓存上,意味着 GPU 使用缓存中的数据工作的同时,运行在 CPU 中的程序可以继续执行。

另外,在 iOS 平台上,SpriteKitCore ImageCore Animation 也都是基于 OpenGL ES 实现的,所以在它们各自的领域,也都有不错的表现。

在图像处理方面,Core Image 提供了便捷的使用以及高效的性能,但是使用原生的 OpenGL ES 会更灵活,可定制性更高,同时支持跨平台。

学习 OpenGL ES 需要关注哪些内容

当然,如果你想全面系统的了解 OpenGL ES,那么每个接口,每种数据类型,OpenGL 工作原理,图形渲染管线每个阶段做了什么,如何编写着色器脚本等等都是需要了解的。这样的话,对着红蓝宝书学习是没有错的。

毋庸置疑,这样的学习必定是漫长枯燥的。

  • 你可能看了半天,学会渲染一个旋转的立方体,然后被一堆矩阵变换公式折腾的死去活来…
  • 又或者看了半天,了解了一大堆概念,混合,深度测试,模版测试,面剔除等等,但是却不知道什么时候该用…

无可厚非,OpenGL 需要学习的东西太多太多(至少我个人还只是学了点皮毛),但是它们也有轻重之分,也有更好的学习方式。

本系列要做的,就是先详述必备的概念,便于之后的学习。然后用最直接的方式,针对图像处理,逐步实现各种效果,来慢慢深入学习 OpenGL。毕竟真正做出了东西,才会有学习的动力。

OpenGL 渲染流程

一个用来渲染图像的OpenGL程序需要执行的主要操作如下所示:

  • 从OpenGL的几何图元中设置数据 ,用于构建图形。
  • 使用不同的着色器(shader)对输入的图元数据执行计算操作,判断它们的位置、颜色,以及其他渲染属性。
  • 将输入图原点额数学描述转换为与屏幕位置相对应的像素片元(fragment)。这一步也被称为光栅化(rasterization)。
  • 最后,针对光栅化过程产生的每个片元,执行片元着色器(fragment shader),从而决定这个片元的最终颜色和位置。
  • 如果有必要,还需要对每个片元执行一些额外的操作,例如判断片元对应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合。
状态机

​ OpenGL 是一个状态机,它维持自己的状态,并根据用户调用的函数来改变自己的状态。根据状态的不同,调用同样的函数也可能产生不同的效果。

在 OpenGL 的世界里,大多数元素都可以用状态来描述,比如:

  • 颜色、纹理坐标、光源的各种参数…
  • 是否启用了光照、是否启用了纹理、是否启用了混合、是否启用了深度测试…

OpenGL 会保持状态,除非我们调用 OpenGL 函数来改变它。

  • 比如你用 glEnablexxx 开启了一个状态,在以后的渲染中将一直保留并应用这个状态,除非你调用 glDisablexxx 及同类函数来改变该状态或程序退出。

  • 又或者当前颜色是一个状态变量,可以把当前颜色设置为白色、红色或其他任何颜色,在此之后绘制的所有物体都将使用这种颜色,直到把当前颜色设置为其他颜色。

    理解了状态机这个概念,我们再来看 OpenGL ES 提供的 API,就会非常明了,因为OpenGL 当中很多 API,其实仅仅是向 OpenGL 这个状态机传数据或者读数据。

上下文

上面提到的各种状态值,将保存在对应的上下文(Context)中。

OpenGL ES 上下文(EAGLContext) : 管理所有 iOS 要绘制的 OpenGL ES 信息。

类似在 Core Graphics 中做任何事情都需要一个 Core Graphics 上下文

通过放置这些状态到上下文中,上下文可以跟踪用于渲染的帧缓存、用于几何数据、颜色等的缓存。还会决定是否使用如纹理、灯光等功能以及会为渲染定义当前的坐标系统等。并且在多任务的情况下,就能很容易的共享硬件设备,而互不影响各自的状态

因此渲染的时候,要指定对应的当前上下文

渲染管线

在 OpenGL 中,任何事物都在 3D 空间中,而屏幕和窗口却是 2D 像素数组,这导致 OpenGL 的大部分工作都是关于把 3D 坐标转变为适应你屏幕的 2D 像素。3D 坐标转为 2D 坐标的处理过程是由 OpenGL 的图形渲染管线(Graphics Pipeline,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D 坐标转换为 2D 坐标,第二部分是把 2D 坐标转变为实际的有颜色的像素。

2D 坐标和像素也是不同的,2D 坐标精确表示一个点在 2D 空间中的位置,而 2D 像素是这个点的近似值,2D 像素受到你的屏幕/窗口分辨率的限制。

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。它的工作过程和车间流水线一致,各个模块各司其职但是又相互依赖

下图就是渲染管线:

[图片上传失败...(image-dd4ab8-1595851022454)]

PS:OpenGL ES 采用服务器/客户端编程模型,客户端运行在 CPU 上,服务端运行在 GPU 上,调用 OpenGL ES 函数的时,由客户端发送至服务器端,并被服务端转换成底层图形硬件支持的绘制命令。

[图片上传失败...(image-c90108-1595851022454)]

左边的客户端程序通过调用 OpenGL ES 接口,将顶点,着色器程序,纹理,以及其他一些 GL 状态参数传入右边的 GL 服务端, 然后在客户端调用绘制命令的时候, GL 便会将输入的图元,逐一执行渲染管线的每个阶段,然后将每个像素的颜色值写入到帧缓存中, 最后视窗系统就可以将帧缓存中的颜色值显示在屏幕上。 此外,应用程序也可以从帧缓存中读取数据到客户端。

在整个管线中,顶点着色器和片段着色器是可编程的部分,应用程序可以通过提供着色器程序在 GPU 中被作用于渲染管线,可编程就是说这个操作可以动态编程实现而不必固定写死在代码中。可动态编程实现这一功能一般都是脚本提供的,在 OpenGL ES 中也一样,编写这样脚本的能力是由 OpenGL 着色语言(OpenGL Shading Language, GLSL)提供的。

那可编程管线有什么好处呢?方便我们动态修改渲染过程,而无需重写编译代码。当然也和很多脚本语言一样,调试起来不太方便。其他阶段则只能使用一些固定的 GL 命令来影响该阶段的执行。

下面以绘制一个三角形为例,针对渲染管线的各个阶段,详细分析。

1. 顶点数组

为了渲染一个三角形,我们以数组的形式传递3个 3D 坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个 3D 坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们假定每个顶点只由一个 3D 位置和一些颜色值组成。

至此,你可能会疑惑,

  • 我们仅仅是传递了三个点,但是 OpenGL ES 是怎么知道它们用来组成三角形呢?
  • 加入我要绘制一个 3D 模型,那么要怎么传入顶点数据?

为了让 OpenGL 知道我们的坐标和颜色值构成的到底是什么,OpenGL 需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给 OpenGL 。OpenGL 支持三种基本图元:点,线和三角形。

当然,OpenGL ES 并不提供对 3D 模型的定义,在传入 OpenGL ES 之前应用程序应该首先将 3D 模型转换为一组图元的集合。每个模型是独立绘制的,修改其中一个模型的一些设置并不会影响其他模型。

[图片上传失败...(image-ccfcab-1595851022454)]

​ 每个图元由一个或者多个顶点组成,每个顶点定义一个点,一条边的一端或者三角形的一个角。每个顶点关联一些数据,这些数据包括顶点坐标,颜色,法向量以及纹理坐标等。所有这些顶点相关的信息就构成顶点数据,这些数据首先被上传到 GL 服务端,然后就可以进行绘制。

PS:OpenGL 中的命令总是按照它被接收到的顺序执行,这意味着一组图元必须被全部绘制完毕才会开始绘制下一组图元。同时也意味着程序对帧缓冲的像素读取的结果一定是该命令之前所有 OpenGL 命令执行的结果。

2. 顶点着色器

顶点着色器对每个顶点执行一次运算,它可以使用顶点数据来计算该顶点的坐标,颜色,光照,纹理坐标等,在渲染管线中每个顶点都是独立地被执行。

在顶点着色器中最重要的任务是执行顶点坐标变换,应用程序中设置的图元顶点坐标通常是针对本地坐标系的。本地坐标系简化了程序中的坐标计算,但是 GL 并不识别本地坐标系,所以在顶点着色器中要对本地坐标执行模型视图变换,将本地坐标转化为裁剪坐标系的坐标值。

顶点着色器的另一个功能是向后面的片段着色器提供一组易变变量(varying)。易变变量会在图元装配阶段之后被执行插值计算,如果是单重采样,其插值点为片段的中心,如果多重采样,其插值点可能为多个采样片段中的任意一个位置。易变变量可以用来保存插值计算片段的颜色,纹理坐标等信息。

顶点着色器实现了顶点操作的通用可编程方法。
image

顶点着色器的输入包括:

  • 着色器程序 一一描述顶点上执行操作的顶点着色器程序源代码或者可执行文件。
  • 顶点着色器输入(或者属性) 一一用顶点数组提供的每个顶点的数据。
  • 统一变量(uniform) 一一顶点(或者片段)着色器使用的不变数据。
  • 采样器 一一代表顶点着色器使用纹理的特殊统一变量类型。

顶点着色器的输出在OpenGL ES 2.0中称为可变(varying)变量,但在OpenGL ES 3.0中改名为顶点着色器输出变量。

顶点着色器可以用于通过矩阵变换位置、计算照明公式来生成逐顶点颜色以及生成或者变换纹理坐标等基于顶点的传统操作。

顶点着色器取得一个位置及相关的颜色数据作为输入属性,用一个 4x4矩阵变换位置,并输出变换后的位置和颜色。

3. 图元装配

[图片上传失败...(image-eb8764-1595851022454)]

在顶点着色器程序输出顶点坐标之后,各个顶点被按照绘制命令中的图元类型参数,以及顶点索引数组被组装成一个个图元。图元(Primitive)是三角形、直线或者点精灵等几何对象。图元的每个顶点被发送到顶点着色器的不同拷贝。在图元装配期间,这些顶点被组合为图元

[图片上传失败...(image-684e9e-1595851022454)]

顶点数组首先通过 GL 命令输入到 GL 渲染管线中,此时顶点坐标位于应用程序的本地坐标系;在经过顶点着色器的计算之后,顶点坐标被转化到裁剪坐标系中,这通常通过向顶点着色器传入一个模型视图变换矩阵,然后在顶点着色器中执行坐标变换。

裁剪坐标系被定义在一个视锥体裁剪的空间里,视锥体是游戏场景的一个可视空间,它由6个裁剪平面构成,分别是:近平面,远平面,左平面,右平面,上平面和下平面。

视锥体在 3D 应用程序中通常表现为一个摄像机,其观察点为裁剪坐标系的原点,方向为穿过远近平面的中点。

[图片上传失败...(image-d0d35c-1595851022454)]

处于视锥体以外的图元将被丢弃,如果该图元与视锥体相交则会发生裁剪产生新的图元。值得注意的是透视裁剪是一个比较影响性能的过程,因为每个图元都需要和 6 个面进行相交计算,并产生新的图元。但是一般在x,y方向超出屏幕之外的,则无需产生新的图元,这些顶点能在视口变换的时候被更高效的丢弃。

通过图元装配,所有 3D 的图元已经被转化为屏幕上 2D 的图元。对于每个图元,必须确定图元是否位于视椎体(屏幕上可见的3D空间区域)内。如果没有完全在视锥体内,则可能需要进行裁剪。如果图元完全处于该区域之外,它就会被抛弃。裁剪之后,顶点位置被转换为屏幕坐标。裁剪和淘汰后,将数据传给下一阶段 - 光栅化阶段。

4. 光栅化

[图片上传失败...(image-1d421b-1595851022454)]

在此阶段绘制对应的图元(点精灵、直线或者三角形)。光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理。这些二维片段代表着可在屏幕上绘制的像素。

image

在光栅化阶段,基本图元被转换为供片段着色器使用的片段(Fragment),Fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。

在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

5. 片段着色器

[图片上传失败...(image-71c54d-1595851022454)]

可编程的片段着色器是实现一些高级特效如纹理贴图,光照,环境光,阴影等功能的基础。片段着色器的主要作用是计算每一个片段最终的颜色值(或者丢弃该片段)。

在片段着色器之前的阶段,渲染管线都只是在和顶点,图元打交道。在 3D 图形程序开发中,贴图是最重要的部分,程序可以通过 GL 命令上传纹理数据至 GL 内存中,这些纹理可以被片段着色器使用。片段着色器可以根据顶点着色器输出的顶点纹理坐标对纹理进行采样,以计算该片段的颜色值。

另外,片段着色器也是执行光照等高级特效的地方,比如可以传给片段着色器一个光源位置和光源颜色,可以根据一定的公式计算出一个新的颜色值,这样就可以实现光照特效。

片段着色器片段着色器为片段上的操作实现了通用的可编程方法。
image
对光栅化阶段生成的每个片段执行这个着色器,采用如下输入:
  • 着色器程序——描述片段上所执行操作的片段着色器程序源代码或者可执行文件。
  • 输入变量——光姗化单元用插值为每个片段生成的顶点着色器钧出。
  • 统一变量——片段(或者顶点)着色器使用的不变数据。
  • 采样器——代表片段着色器所用纹理的特殊统一变量类型。
6. 片段测试/逐片段操作

[图片上传失败...(image-6cae55-1595851022454)]

在这个阶段中会对一个片段进行各种测试,来决定它是否可见。如果一个片段成功通过了所有测试,那么它就会被直接绘制到帧缓存中了, 它对应的像素的颜色值会被更新,如果开启了融合模式,那么片段的颜色会与该像素当前的颜色相叠加,形成一个新的颜色值并写入帧缓存中。

片段着色器输出的颜色值,还要经过几个阶段的片段操作,这些操作可能会修改片段的颜色值,或者丢弃该片段,最终的片段颜色值才会被写入到帧缓冲中。

[图片上传失败...(image-aae11e-1595851022454)]

像素所有权测试用来判断帧缓冲区中该位置的像素是否属于当前 OpenGL ES,例如在窗口系统中该位置可能会被其他应用程序窗口遮挡,此时该像素则不会被显示。

在片段测试之后,片段要么被丢弃,要么每个片段对应的颜色,深度,模板值会被写入帧缓冲区,最终呈现在设备屏幕上。帧缓冲区中的颜色值也可以被读回到客户端应用程序中,这样可以实现绘制到纹理的效果。

至此,OpenGL ES 渲染管道最终将每个像素点的颜色,深度,模板等数据输送到帧缓存中(Framebuffer)。

缓存概念

OpenGL ES 部分运行在 CPU 上,部分运行在 GPU 上,为了协调这两部分的数据交换,定义了缓存(Buffers) 的概念。CPU 和 GPU 都有独自控制的内存区域,缓存可以避免数据在这两块内存区域之间进行复制,提高效率。缓存实际上就是指一块连续的 RAM

帧缓存 / 渲染缓存

那么,帧缓存和渲染缓存到底代表什么,又用来做什么呢?

总的来说,帧缓存是接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域。它存储着 OpenGL ES 绘制每个像素点最终的所有信息:颜色,深度和模板值。更通俗点,可以理解成存储屏幕上最终显示的一帧画面的区域。

[图片上传失败...(image-8a5678-1595851022454)]

渲染缓存则存储呈现在屏幕上的渲染图像,它也被称作颜色缓冲区,因为它本质上是存储要显示的颜色。多个纹理对象或多个渲染缓存对象,可通过连接点(attachment points)连接到帧缓存对象上。

可以同时存在很多帧缓存,并且可以通过 OpenGL ES 让 GPU 把渲染结果存储到任意数量的帧缓存中。但是,只有将内容绘制到视窗体提供的帧缓存中,才能将内容输出到显示设备。视图系统提供的帧缓存通常由两个缓存对象组成,一个前端缓存,一个后端缓存。

前帧缓存决定了屏幕上显示的像素颜色。程序的渲染结果通常保存在后帧缓存在内的其他帧缓存,当渲染后的后帧缓存包含一个完成的图像时,前后帧缓存会立即互换,前帧缓存变成新的后帧缓存,后帧缓存变成新的前帧缓存。

[图片上传失败...(image-eecd89-1595851022454)]

但是前后帧我们无法去操纵,它是由系统控制的。我们只能显式的告诉系统,要展示哪个帧缓存了,然后由系统去完成前后帧的切换。

纹理

纹理是一个用来保存图像颜色的元素值的缓存渲染是指将数据生成图像的过程。纹理渲染则是将保存在内存中的颜色值等数据,生成图像的过程。

现实生活中,纹理最通常的作用是装饰我们的物体模型,它就像是贴纸一样贴在物体表面,使得物体表面拥有图案。

但实际上在 OpenGL 中,纹理的作用不仅限于此,它可以用来存储大量的数据。一个典型的例子就是利用纹理存储画笔笔刷的 mask 信息。

坐标系

[图片上传失败...(image-dbf5c1-1595851022454)]

OpenGL 渲染管线整个流程中,涉及了多个坐标系变化,看起来非常繁琐。但是针对 2D 图像处理,我们其实不需要关心这些变化,我们只需要了解标准化设备坐标即可。

标准化设备坐标是一个 x、y 和 z 值在 -1.0 到 1.0 的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略 z 轴,仅处理 2D 图像,z 轴设置为 0.0):

[图片上传失败...(image-c3ca45-1595851022454)]

与通常的屏幕(UIKit)坐标不同,y 轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。

为了方便记忆,可以借助右手左边系。

按照惯例,OpenGL 是一个右手坐标系。简单来说,就是正 x 轴在你的右手边,正 y 轴朝上,而正 z 轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正 z 轴穿过你的屏幕朝向你。坐标系画起来如下:

[图片上传失败...(image-4d923a-1595851022454)]

[图片上传失败...(image-f9d744-1595851022454)]

另外,为了能够把纹理映射到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标,用来标明该从纹理图像的哪个部分采样(采集片段颜色)。之后在图形的其它片段上进行片段插值。

纹理坐标在 x 和 y 轴上,范围为 0 到 1 之间(我们使用的是 2D 纹理图像)。使用纹理坐标获取纹理颜色叫做采样。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上。

[图片上传失败...(image-826f94-1595851022454)]

OpenGL ES 坐标系
image

OpenGL ES 坐标系的范围是 -1 ~ 1,是一个三维的坐标系,通常用 X、Y、Z 来表示。Z 轴的正方向指向屏幕外。在不考虑 Z 轴的情况下,左下角为 (-1, -1, 0),右上角为 (1, 1, 0)。

纹理坐标系
image

纹理坐标系的范围是 0 ~ 1,是一个二维坐标系,横轴称为 S 轴,纵轴称为 T 轴。在坐标系中,点的横坐标一般用 U 表示,点的纵坐标一般用 V 表示。左下角为 (0, 0),右上角为 (1, 1)。

注: UIKit 坐标系的 (0, 0) 点在左上角,其纵轴的方向和纹理坐标系纵轴的方向刚好相反。

iOS 中的 GLKit

在 GLKit 中,苹果对 OpenGL ES 中的一些操作进行了封装,因此我们使用 GLKit 来渲染会省去一些步骤.
GLKit 功能
  • 加载纹理
  • 提供⾼性能的数学运算
  • 提供常见的着色器
  • 提供视图以及视图控制器.
GLKit 是怎么渲染纹理的
1、获取顶点数据
2、初始化 GLKView 并设置上下文
3、加载纹理
4、实现 GLKView 的代理方法
5、开始绘制

GPUImage

简介

一款基于OpenGL ES 2.0的开源图像处理库。在iOS上将OpenGL ES的使用封装成Objective-C接口,可以用来给图像、相机视频、视频等添加滤镜等渲染操作
第一代使用的是 Objective-C,第二代采用 Swift 及 OpenGL 重写
GPUImage3 采用 Metal 代替上一版 OpenGL 重新设计,实现 GPU 对图像和视频的加速处理
GPUImage的性能甚至在很多时候击败了Core Image。
GPUImage最大的特点就是使用简便,它内部封装了许多滤镜,类似亮度滤镜、对比度滤镜、灰度滤镜、双边滤波等等,而且还有许多现成的卡通,黑白版,高斯模糊之类的滤镜效果。可以对stillimage静态图片进行处理,也可以创建camera并随意组合滤镜效果来构建一个摄像头实时滤镜。制作一些常用的滤镜、磨皮美颜效果都很方便。
GPUImage是一个著名的图像处理开源库,可以实现图像的输入、处理、输出等。它可以将GPU加速的滤镜和其他特效应用于图像、摄像头实时视频和视频文件

技术介绍

因为GPUImage是基于OpenGL ES的,如果想深入了解。可以先看下OpenGL ES的介绍。

OpenGL ES准备 https://www.jianshu.com/p/7a58a7a61f4c

回顾下我们之前的OpenGL ES教程,图像在OpenGL ES中的表示是纹理,会在片元着色器里面进行像素级别的处理。

假设我们自定义一个OpenGL ES程序来处理图片,那么会有以下几个步骤:

1、初始化OpenGL ES环境,编译、链接顶点着色器和片元着色器;
2、缓存顶点、纹理坐标数据,传送图像数据到GPU;
3、绘制图元到特定的帧缓存;
4、在帧缓存取出绘制的图像。

  • GPUImageFilter 负责的是第一、二、三步。
    • GPUImageFilter解析

GPUImageFilter和响应链的其他元素实现了GPUImageInput协议,他们都可以提供纹理参与响应链,或者从响应链的前面接收并处理纹理。响应链的下一个对象是target,响应链可能有多个分支(添加多个targets)。

  • GPUImageFramebuffer 负责是第四步。
    • 管理纹理缓存格式、帧缓存的buffer。

四大输入基础类

GPUImage的四大输入基础类,都可以作为响应链的起点。这些基础类会把图像作为纹理,传给OpenGL ES处理,然后把纹理传递给响应链的下一个对象。

GPUImageVideoCamera 摄像头-视频流
GPUImageStillCamera 摄像头-照相
GPUImagePicture 图片
GPUImageMovie 视频

响应链,先要理解帧缓存的概念,这在OpenGL ES教程-帧缓存有提到过。

GPUImage处理画面原理 https://blog.csdn.net/Xoxo_x/article/details/52695032

  • GPUlmage采用链式方式来处理画面,通过addTarget:方法为链条添加每个环节的对象,处理完一 个target,就会把上一个环节处理好的图像数据传递下一个target去处理,称为GPUlmage处理链.
    • 比如:墨镜原理,从外界传来光线,会经过墨镜过滤,在传给我们的眼睛,就能感受到大白天也是乌黑一片,哈哈。

    • 一般的target可分为两类

      • 中间环节的target,-般是 各种filter,是GPUlmageFilter或者是子类.
      • 最终环节的target, GPUlmageView:用于显示到屏幕上,或者GPUlmageMovieWriter:写成视频文件。
  • GPUIlmage处理 主要分为3个环节
    • source(视频、图片源) -> filter (滤镜) -> final target (处理后视频、图片)
    • GPUImaged的Source:都继承GPUImageOutput的子 类,作为GPUlmage的数据源,就好比外界的光线,作为眼睛的输出源
      • GPUlmageVideoCamera: 用于实时拍摄视频
      • GPUlmageStillCamera: 用于实时拍摄照片
      • GPUlmagePicture: 用于处理已经拍摄好的图片,比如png,jpg图片
      • GPUlmageMovie: 用于处理已经拍摄好的视频,比如mp4文件
    • GPUImage的filter:GPUimageFilter类或者子类, 这个类继承自GPUlmageOutput,并且遵守GPUlmagelInput协议,这样既能流进,又能流出,就好比我们的墨镜,光线通过墨镜的处理,最终进入我们眼睛
    • GPUImage的final target:GPUImageView,GPUlmageMovieWriter就好比我们眼睛, 最终输入目标。
  • GPUImage 注意点
    • AVCaptureSession 不要在主线程调用startRunning。这个方法是同步方法,会阻塞当前线程,放在主线程会导致UI卡顿。
    • dispatch_semaphore_t; GPUImage中的属性frameRenderingSemaphore帧渲染信号量**用于处理完一帧后接着处理下一帧

应用场景

实时美颜滤镜

视频合并混音

模糊图片处理

多路视频

水印

本项目应到的
如果涉及到人脸识别,可以使用IFlyFaceDetector配合着GPUImage,效果非常不错
视频处理(合并,裁剪,添加背景音乐,添加水印)
磨皮、美白

示例

GPUImageVideoCamera 视频采集

GPUImageVideoCamera继承自GPUImageOutput,遵循AVCaptureVideoDataOutputSampleBufferDelegateAVCaptureAudioDataOutputSampleBufferDelegate两个协议,采用摄像头数据作为数据源,是数据响应链的源头。

  • 视频捕捉核心类:AVCaptureSession
    • AVCaptureSession用于连接输入和输出的资源。
      • 一个捕捉会话如摄像头和麦克风,输出到一个或多个目的地。session调用startRunning开始数据流的流入流出,调用stopRunning停止数据流流动。
        • 但不要在主线程调用startRunning。这个方法是同步方法,会阻塞当前线程,放在主线程会导致UI卡顿。
        • AVCaptureSessionPreset:session的属性sessionPreset用来自定义一些设置,在session运行的时候动态配置也可以。
    • 输入:AVCaptureDevice
      • AVCaptureDevice为摄像头等物理设备定义了一个接口,并对这些硬件设备定义了大量控制方法,如对焦、曝光等。AVCaptureDevice定义了大量类方法用于访问系统的捕捉设备,最常用的一个方法是defaultDeviceWithMediaType:但一个捕捉设备不能直接添加到AVCaptureSession中,可以将它封装到一个AVCaptureDeviceInput对象中添加。
    • 输出:AVCaptureOutput
      • AVCaptureAudioDataOutput和AVCaptureVideoDataOutput可以直接访问硬件捕捉到的数字样本,他们继承自抽象基类AVCaptureOutput。这两个类可以提供强大的功能,如对音频和视频流进行实时处理。视频的一个新图像帧会被传递到AVCaptureVideoDataOutput,如果需要对视频帧处理加工,需要设置AVCaptureVideoDataOutput的代理方法

      • 获得图像帧。代理的函数会在sampleBufferCallbackQueue中调用,并且必须是一个同步队列,以保证视频帧的顺序是正确的——GPUImage创建了一个cameraProcessingQueue。如果处理速度比采集速度慢,队列会堵塞,等待处理的图像会占住内存,默认setAlwaysDiscardsLateVideoFrames属性为YES,这样就会丢弃由于队列阻塞而不断新增来不及处理的新图像,GPUImage把这一属性改为NO。**

GPUImageFilter https://www.jianshu.com/p/0141e8c4037d
GPUImageFilter继承自GPUImageOutput,遵循GPUImageInput协议,遵循这个协议的对象都可以从响应链的上游接收处理过的纹理并继续处理,下游的处理对象称为上一步的target,响应链的下游可以有多个target(或分支)。

所以,GPUImageFilter就是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象,既可以流入数据,也可以流出数据,GPUImageView, GPUImageMovieWriter是最终输出target,来显示图片或者视频

我的理解是,大家对于美颜比较常见的需求就是磨皮、美白。当然提高饱和度、提亮之类的就根据需求而定
  • GPUImageFilter3个主要职责
    • 初始化OpenGL ES环境,编译、链接顶点着色器和片元着色器;
    • 缓存顶点、纹理坐标数据,传送图像数据到GPU;
    • 绘制图元到特定的帧缓存;
GPUImageFramebuffer

GPUImageFramebuffer的功能是在帧缓存取出绘制的图像。是管理纹理缓存格式、帧缓存的buffer。
GPUImageFilter的成员变量firstInputFramebuffer和
GPUImageOutput的成员变量GPUImageFramebuffer都是GPUImageFramebuffer的实例。

GPUImageMovieWriter

通过GPUImage滤镜处理、录制、保存的思路如图。
视频部分:经过filter的视频帧分两步,一步用于在屏幕预览GPUImageView上显示,另一步用于写入GPUImageMovieWriter。
音频部分:从GPUImageVideoCamera分离的音频直接写入GPUImageMovieWriter。另外,如果需要对音频进行混响、变声等处理,可以从这个节点分支处理写入

  • 保存视频
    • 在私有方法中初始化了AVAssetWriter,传入一个本地存储地址和文件格式。
      assetWriter = [[AVAssetWriter alloc] initWithURL:movieURL fileType:fileType error:&error];
GPUImageView
@property (nonatomic,strong)GPUImageMovie * gpuMovie;//滤镜效果展示movie
@property (nonatomic,strong)GPUImageView * gpuView;//视频预览图层 GPUImageView在响应链的终点,是一个UIView
GPUImageOutput

​ GPUImage中的一个非常重要的基类GPUImageOutput和一个协议GPUImageInput。基本上所有重要的GPUImage处理类都是GPUImageOutput的子类,它实现了一个输出的基本功能。

GPUImageFilterGroup
GPUImageBilateralFilter
GPUImageInput

基本上所有的GPUImage处理类也都遵循GPUImageInput协议。它定义了一个能够接收frameBuffer的接收者所必须实现的基本功能。主要包括:
*接收上一个GPUImageOutput的相关信息;
*接收并处理上一个GPUImageOutput渲染完成的通知;

动画

UIKit动画和Core Animation

经常用到的就是UIKit动画和Core Animation,而这两部分最底层的实现都是基于OpenGL ES,想更详细了解动画算法以及动画具体实现过程的同学可以从先了解OpenGL ES开始。

1. 隐式动画

每个UIView都有一个layer属性,它的类型是CALayer,属于QuartzCore框架。CALayer本身并不包含在UIKit中,它不能响应事件。由于CALayer在设计之初就考虑了它的动画操作功能,CALayer很多属性在修改时都能形成动画效果,这种属性称为“隐式动画属性”。 对每个UIView的非root layer对象属性进行修改时,都会形成隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。

CALayer的隐式动画实际上是自动执行了CATransaction动画,执行一次隐式动画大概是0.25秒。UIView的动画底层也是使用CATranscation实现的。

2. 显式动画

iOS显式动画有两类动画方式: UIKit 和 core animation,

2.1.1 UIKit中执行动画的方法

UIKit动画实质上是对CoreAnimation的封装,提供简洁的动画接口。UIView动画可以设置的动画属性有:

a、大小变化(frame)
b、拉伸变化(bounds)
c、中心位置(center)
d、旋转(transform)
e、透明度(alpha)
f、背景颜色(backgroundColor)
g、拉伸内容(contentStretch)

UIView/layer/UICollectionViewLayoutAttributes 有个属性transform,是CGAffineTransform类型。可以使其在二维界面做旋转、平移、缩放单独或者组合动画!

2.2 core animation: GPU执行

如果你的动画效果很复杂或者UIKit不能实现,可以尝试用Core Animation和layer来实现。你可以直接用Core Animation来自定义layer的动画效果。因为view和layer密切的关系,layer的变化会直接影响到view。用Core Animation实现动画的方法,简单的说有两种,用CATransform3D和CAAnimation的子类

  • UIView动画

    • 简单动画 - 只能控制开始和结束时的效果,然后由系统补全中间的过程
      • animateWithDuration:animations:
    • 关键帧动画 - 有些时候我们需要自己设定若干关键帧,实现更复杂的动画效果
      • animateKeyframesWithDuration:2.0 delay:0.0 options:
    • View 的转换 - 用于进行两个 View 之间通过动画换场
      • transitionWithView:duration:options:animations:completion
    • transform 动画
  • CALayer动画

    上面介绍的 UIView 的几种动画方式,实际上是对底层 CALayer 动画的一种封装。直接使用 CALayer 层的动画方法可以实现更多高级的动画效果。

    • 基本动画(CABasicAnimation)- 用于创建一个 CALayer 上的基本动画效果
      CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
      animation.toValue = @200;
      [self.myView.layer addAnimation:animation forKey:nil];
      
      • KeyPath
      • 属性
    • 关键帧动画(CAKeyframeAnimation)
    • 组动画(CAAnimationGroup) - 可以将一组动画组合在一起,所有动画对象可以同时运行
    • 切换动画(CATransition) - 可以用于 View 或 ViewController 直接的换场动画

    • transform 动画
  • 更高级的动画效果

    • CADisplayLink - 可以让我们以和屏幕刷新率同步的频率(每秒60次)来调用绘制函数,实现界面连续的不停重绘,从而实现动画效果
    • UIDynamicAnimator - 可以创建出具有物理仿真效果的动画

    • CAEmitterLayer - Core Animation 提供的一个粒子发生器系统,可以用于创建各种粒子动画,例如烟雾,焰火等效果。

转场动画

什么是转场动画

比如 在 NavigationController 里 push 或 pop 一个 View Controller,在 TabBarController 中切换到其他 View Controller,以 Modal 方式显示另外一个 View Controller,这些都是转场。

转场动画的本质: 下一场景(子 VC)的视图替换当前场景(子 VC)的视图以及相应的控制器(子 VC)的替换,表现为当前视图消失和下一视图出现。并基于此进行动画,动画的效果可自由发挥。

自定义转场动画

官方支持以下几种方式的自定义转场
  1. UINavigationController 中 push 和 pop
  2. UITabBarController 中切换 Tab
  3. Modal 转场:present 和 dismiss,俗称视图控制器的模态显示和消失,仅限于modalPresentationStyle属性为 FullScreenCustom这两种模式
  4. UICollectionViewController 的布局转场:仅限于 UICollectionViewController 与 UINavigationController 结合的转场方式

以上前三种转场都需要转场代理和动画控制器的帮助才能实现自定义转场动画

转场协议

iOS 7 之后 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活。主要有一下几个协议:

  1. 转场代理(Transition Delegate)
  2. 动画控制器(Animation Controller)
  3. 交互控制器(Interaction Controller)

对于非交互式动画我们只需要实现转场代理动画控制器协议即可,对于交互式动画我们还需要实现交互控制器协议。

转场代理

自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。有如下三种转场代理。

  • UINavigationControllerDelegate
    • UINavigationController 的 delegate 属性遵守该协议。
  • UITabBarControllerDelegate
    • UITabBarController 的 delegate 属性遵守该协议
  • UIViewControllerTransitioningDelegate
    • UIViewController 的 transitioningDelegate 属性遵守该协议
    • Modal 转场的代理由 presentedVC 的transitioningDelegate属性来提供,这与前两种容器控制器的转场不一样,另外,需要将 presentedVC 的modalPresentationStyle属性设置为.Custom或.FullScreen,只有这两种模式下才支持自定义转场,该属性默认值为.FullScreen。当与 UIPresentationController 配合时该属性必须为.Custom。 两者的区别在于FullScreen会移除fromView,而Custom不会。
动画控制器

它是最重要的部分是整个动画逻辑的核心,负责添加视图以及执行动画,遵守UIViewControllerAnimatedTransitioning协议;由我们实现。 该协议要求实现以下方法:

/*返回动画执行时间,一般0.5s就足够了*/
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;

/*核心方法,做一些动画相关的操作*/
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
交互控制器

实现交互效果需要在非交互转场的基础上实现下面两个方法:

  1. 由转场代理提供交互控制器,这是一个遵守<UIViewControllerInteractiveTransitioning>协议的对象,不过系统已经打包好了现成的类UIPercentDrivenInteractiveTransition供我们使用。我们不需要做任何配置,仅仅在转场代理的相应方法中提供一个该类实例便能工作。另外交互控制器必须有动画控制器才能工作。

  2. 交互控制器还需要交互手段的配合,最常见的是使用手势,或是其他事件,来驱动整个转场进程。

    需要注意的地方: 如果在转场代理中提供了交互控制器,而转场发生时并没有方法来驱动转场进程(比如手势),转场过程将一直处于开始阶段无法结束,应用界面也会失去响应:在 NavigationController 中点击 NavigationBar 也能实现 pop 返回操作,但此时没有了交互手段的支持,转场过程卡壳;在 TabBarController 的代理里提供交互控制器存在同样的问题,点击 TabBar 切换页面时也没有实现交互控制。因此仅在确实处于交互状态时才提供交互控制器,可以使用一个变量来标记交互状态,该变量由交互手势来更新状态。

自定义Present转场动画
  • 基本流程

    以视图控制器A跳转到B为例:

    1. 创建动画代理,在事情比较简单时,A自己就可以作为代理
    2. 设置B的transitioningDelegate为步骤1中创建的代理对象
    3. 调用presentViewController:animated:completion:并把参数animated设置为true
    4. 系统会找到代理中提供的Animator,由Animator负责动画逻辑
    • 注解
      • Animator:它是实现了UIViewControllerAnimatedTransitioning协议的对象,用于控制动画的持续时间和动画展示逻辑,代理可以为present和dismiss过程分别提供Animator,也可以提供同一个Animator
自定义UINavigationController转场动画
  • UINavigationController自定义转场动画大题流程是和Present 一样的,很多都可以类比过来.

    • 与present/dismiss不同的时,现在视图控制器实现的是UINavigationControllerDelegate 协议,让自己成为navigationController 的代理。这个协议类似于此前的UIViewControllerTransitioningDelegate 协议
    • 至于animator,就和此前没有任何区别了。一个封装得很好的animator,不仅能在present/dismiss时使用,甚至还可以在push/pop时使用
交互式(Interactive)转场动画
  • 所谓的交互式动画,通常是基于手势驱动,产生一个动画完成的百分比来控制动画效果。整个动画不再是一次性、连贯的完成,而是在任何时候都可以改变百分比甚至取消。这需要一个实现了UIPercentDrivenInteractiveTransition协议的交互式动画控制器和animator协同工作。这看上去是一个非常复杂的任务,但UIKit已经封装了足够多细节,我们只需要在交互式动画控制器和中定义一个时间处理函数(比如处理滑动手势),然后在接收到新的事件时,计算动画完成的百分比并且调用updateInteractiveTransition来更新动画进度即可。

  • 交互式动画是在非交互式动画的基础上实现的,我们需要创建一个继承自UIPercentDrivenInteractiveTransition 类型的子类,并且在动画代理中返回这个类型的实例对象。

    在这个类型中,监听手势(或者下载进度等等)的时间变化,然后调用percentForGesture方法更新动画进度即可

转场协调器与UIModalPresentationCustom
  • 在进行转场动画的同时,您还可以进行一些同步的,额外的动画。presentedViewpresentingView可以更改自身的视图层级,添加额外的效果(阴影,圆角)。UIKit使用转成协调器来管理这些额外的动画。您可以通过需要产生动画效果的视图控制器的transitionCoordinator属性来获取转场协调器,转场协调器只在转场动画的执行过程中存在

    • 我们还需要使用UIModalPresentationStyle.Custom来代替.FullScreen。因为后者会移除fromViewController,这显然不符合需求。

    • 当present的方式为.Custom时,我们还可以使用UIPresentationController更加彻底的控制转场动画的效果。一个 presentation controller具备以下几个功能:

      1. 设置presentedViewController的视图大小
      2. 添加自定义视图来改变presentedView的外观
      3. 为任何自定义的视图提供转场动画效果
      4. 根据size class进行响应式布局

      您可以认为,. FullScreen以及其他present风格都是swift为我们实现提供好的,它们是.Custom的特例。而.Custom允许我们更加自由的定义转场动画效果。

      UIPresentationController提供了四个函数来定义present和dismiss动画开始前后的操作:

      1. presentationTransitionWillBegin: present将要执行时
      2. presentationTransitionDidEnd:present执行结束后
      3. dismissalTransitionWillBegin:dismiss将要执行时
      4. dismissalTransitionDidEnd:dismiss执行结束后

引用链接

核心绘图——Core Graphics

GPUImage