Core Animation 3D介绍(第1部分)

尊重原创 转自:http://codingobjc.com/blog/2013/06/11/core-animation-3djie-shao-di-1bu-fen/

在本教程中,我将向你介绍Core Animation中用于绘制3D图形的一些技术。
我要告诉你的好消息是:我们不必直接使用OpenGL,仅仅用Core Animation就可以很容易的实现一些3D效果。但是,也别太高兴了,因为”用Core Animation来制作一个复杂的3D游戏”也并不是一个好主意。
这个教程分两部分。第一部分,我们先讨论下一些3D原理知识,然后运用这些概念来创建一些简单的3D场景。然后在第二部分中,我们将使用Core Animation来制作一个类似于旋转木马的3D场景特效。
最终app的预览效果如下:(译注: 原文中这里的视频被墙,因此这里只简单的提供一个图片预览,你可以直接下载例子代码运行即可以看到最终app的效果)

好,准备好了吗?我们开始写代码吧!
首先,下载文章后面的代码。如果你要自己创建项目的话,要记得添加QuartzCore框架。

###3D和矩阵相关的数学知识(一点点…别担心,只有一点点)
要在一个3D空间中绘图,除了标准2D坐标(X和Y)概念外,我们还需要引入一个深度(depth)的概念,也就是要加入第三个坐标轴–Z轴。
这样,在空间中,我们只需要简单的改变物体的X、Y和Z坐标,就可以在垂直方向、水平方向或深度方向上移动物体。

在2D或3D空间中,对一个物体执行平移、缩放或旋转这些操作时需要使用矩阵运算。
你可以将矩阵想象成一个多维数组。比如,在3D空间中我们使用一个像下面这种格式的4X4矩阵:

[X][0][0][0][0][Y][0][0][0][0][Z][0][0][0][0][1]

把这个矩阵和物体的每一个坐标点(又称顶点)相乘,我们可以得到物体的一个变换(transformation)效果。
严格点讲,前面这个矩阵是用来执行缩放操作的。其中的X、Y、Z值代表每个轴上的缩放值。
如果你要进行其他的变换操作,比如旋转或者平移,你需要将矩阵换成其他矩阵格式(scheme)。

不要紧张!你不需要知道其他更多原理知识,而且也不必直接进行这些操作。Core Animation会为你完成这些操作,虽然你不知道它是如何做到的,但是它确实会为你完成这些操作,所以不必害怕。
当然,就我个人来说,如果我知道它背后是如何工作的(至少知道一些它的基本原理),我会对我的代码更有自信。因此,如果你想了解更多矩阵相关的知识,建议你读一读这篇文章

###3D变换(Transformations)
现在基本上,你已经知道矩阵的作用了,也知道了3D空间是如何构成的。来,我们用Core Animation做一些3D的东西吧。
打开TB_3DIntro->viewController.m文件。
我在里面列出了6个分别以A、B、C、D、E、F开头的函数。每一个函数对应了一种不同的3D场景效果。
我们先来看看由A_singlePlan函数创造的场景吧。
用这个函数,我们画了一个平面,平面绕Y轴旋转了45度。
首先,我们创建了一个CALayer,用它来作容器层(当然,并不是一定要这样做,只是我更喜欢不直接在self.view的layer上进行工作)。

-(void)A_singlePlane{//Create the containerCALayer*container=[CALayerlayer];container.frame=CGRectMake(0,0,640,300);[self.view.layeraddSublayer:container];

然后我们创建了另一个CALayer,用它代表一个平面。

//Create a PlaneCALayer*purplePlane=[selfaddPlaneToLayer:containersize:CGSizeMake(100,100)position:CGPointMake(250,150)color:[UIColorpurpleColor]];

我写了一个简单的辅助函数,用它将平面直接添加到容器层上,然后返回这个新建的平面层。代码非常简单:

-(CALayer*)addPlaneToLayer:(CALayer*)containersize:(CGSize)sizeposition:(CGPoint)pointcolor:(UIColor*)color{//Initialize the layerCALayer*plane=[CALayerlayer];//Define position,size and colorsplane.backgroundColor=[colorCGColor];plane.opacity=0.6;plane.frame=CGRectMake(0,0,size.width,size.height);plane.position=point;plane.anchorPoint=CGPointMake(0.5,0.5);plane.borderColor=[[UIColorcolorWithWhite:1.0alpha:0.5]CGColor];plane.borderWidth=3;plane.cornerRadius=10;//Add the layer to the container layer[containeraddSublayer:plane];returnplane;}

最后,我们用CATransform3D来添加变换。
啊???你一定又想问CATransform3D是什么东西?按住Cmd键点击这个类型名称,你可以发现它是一个结构体,使用了一种很”火星”的语法来表示一个矩阵。

structCATransform3D{CGFloatm11,m12,m13,m14;CGFloatm21,m22,m23,m24;CGFloatm31,m32,m33,m34;CGFloatm41,m42,m43,m44;};typedefstructCATransform3DCATransform3D;

旋转变换部分的代码也相当简单:

//Apply transform to the PLANECATransform3Dt=CATransform3DIdentity;t=CATransform3DRotate(t,45.00f*M_PI/180.0f,0,1,0);purplePlane.transform=t;}

先用单位矩阵CATransform3DIdentity来初始化一个变换(transformation),然后用CATransform3DRotate函数给变换乘上一个旋转矩阵。
CATransform3DRotate这个函数的参数分别表示矩阵,旋转的角度(以弧度为单位)和3个坐标轴上的变换系数。这个例子中,X和Z轴都没受到影响,只对Y轴有影响,物体会在Y轴上旋转45度。
下图是运行结果。

呃……你可以看到,它还不是3D的!我们只是在X轴方向上将一个正方形压扁了。
这是由于我们还没有设置视点的值(perspective value)。通常,在绘制3D场景的时候会将场景进行正射投影(Orthographic projection)处理,由此会产生一个扁平的场景。换句话说,在正射投影后你无法看到Z轴上的深度感。
要给我们的场景加上空间深度感,我们必须修改变换矩阵的m34参数。这个参数决定了视点的值。
现在,来看看B_singlePlanePerspective这个函数,这函数展示了视点的作用。
这个函数和前一个函数只有变换部分的代码有所不同:

//Apply transform to the PLANECATransform3Dt=CATransform3DIdentity;//Add perspective!!!t.m34=1.0/-500;t=CATransform3DRotate(t,degToRad(45.0),0,1,0);purplePlane.transform=t;}

你可以看到,我们直接给矩阵的m34属性赋了一个值。在这里,我不会深入的从数学上解释这个值是如何起作用的。但是我可以告诉你的是,这个值越接近0,视点就越深。
下面是两种不同视点值的效果:

###3D变换的顺序问题(Transformations chain)
我们可以将多个矩阵相乘从而将多种变换应用到一个物体上。比如,如果我们想将平移和旋转变换应用到一个物体上,我们可以直接将两个变换矩阵相乘:

TransformMatrix=TranslateMtx*RotateMtx

数学上,一般情况下乘法都可以使用交换律:

TranslateMtx*RotateMxt=RotateMtx*TranslateMtx

但是矩阵乘法不适用于交换律。AxB的结果可能和BxA的结果不一样。要记住这一点!
接下来C_transformationsChain这个例子中,我们会将2个变换效果按不同的先后顺序应用到2个不同的物体上。
以下是主要代码:

//Apply transformation to the PLANESCATransform3Dt=CATransform3DIdentity;//Purple plane: Perform a rotation and then a translationt=CATransform3DRotate(t,45.0f*M_PI/180.0f,0,0,1);t=CATransform3DTranslate(t,100,0,0);purplePlane.transform=t;//reset the transform matrixt=CATransform3DIdentity;//Red plane: Perform translation first and then the rotationt=CATransform3DTranslate(t,100,0,0);t=CATransform3DRotate(t,45.0f*M_PI/180.0f,0,0,1);redPlane.transform=t;

运行结果如下:

看到了吗,不同的变换顺序完全导致了不同的效果。
我们重点下看一下紫色的那块。旋转变换改变了它的坐标轴方向,然后我们又将它沿X轴进行了平移,此时它的X轴方向已经和红色平面的X轴方向不一致了。
变换步骤示意图如下:

###图层层次(layer hierarchies)
到目前为止,我们都是将变换直接应用在这些平面上。在3D场景中,经常需要创建一些相互之间有层次结构的物体。这个时候,只需要将变换应用到根层次上,就可以使整个层次结构中的物体整体具有这个变换效果。这种做法非常有用。
接下来,我们来看看D_multiplePlanes这个例子。
我们在容器层上添加了4个平面。
当没有任何变换效果时看起来是这样的:

如果我们给每一个平面都加上一个Y轴上的旋转变换,我们会得到4个独立的旋转效果:

但是,如果我们只是在容器层上应用旋转变换,我们会得到一个完全不同的场景效果:

这种效果,是相机(camera)位置发生改变带来的结果。我们没有移动每个平面,只是改变了视点的位置。
此函数主要的变换部分代码如下,包含了分别应用于各个平面和应用于容器层的两种变换效果的代码:

//Transformation CATransform3Dt=CATransform3DIdentity;BOOLapplyToContainer=NO;//Apply the transformation to each PLANEif(!applyToContainer){t.m34=1.0/-500.0;t=CATransform3DRotate(t,degToRad(60.0),0,1,0);purplePlane.transform=t;t=CATransform3DIdentity;t.m34=1.0/-500.0;t=CATransform3DRotate(t,degToRad(60.0),0,1,0);redPlane.transform=t;t=CATransform3DIdentity;t.m34=1.0/-500.0;t=CATransform3DRotate(t,degToRad(60.0),0,1,0);orangePlane.transform=t;t=CATransform3DIdentity;t.m34=1.0/-500.0;t=CATransform3DRotate(t,degToRad(60.0),0,1,0);yellowPlane.transform=t;}//Apply the transformation to the CONTAINERelse{CATransform3Dt=CATransform3DIdentity;t.m34=1.0/-500;t=CATransform3DRotate(t,degToRad(60.0),0,1,0);container.transform=t;}

###CATransformLayer
到目前为止我们所见到的代码都能正常工作,但说实话,作为3D层次结构的根,CALayer不是正确的选择。
函数E_multiplePlanesZAxis展示了为什么。
这个例子中,我们创建4个XY坐标相同只有Z坐标不同的平面。紫色平面最近,黄色平面最远。

//Apply transforms to the PLANESt=CATransform3DIdentity;t=CATransform3DTranslate(t,0,0,-10);purplePlane.transform=t;t=CATransform3DIdentity;t=CATransform3DTranslate(t,0,0,-50);redPlane.transform=t;t=CATransform3DIdentity;t=CATransform3DTranslate(t,0,0,-90);orangePlane.transform=t;t=CATransform3DIdentity;t=CATransform3DTranslate(t,0,0,-130);yellowPlane.transform=t;

在旋转这些平面前,我们先让容器层执行了一个旋转变换。

//Apply transform to the CONTAINERCATransform3Dt=CATransform3DIdentity;t.m34=1.0/-500;t=CATransform3DRotate(t,80.0f*M_PI/180.0f,0,1,0);container.transform=t;

你也许希望看到下面这种结果:

但是,实际上会得到这种结果:

这是因为CALayer不能处理3D层次结构的深度,它只能将场景处理成相同的Z层次。
为了修正这个问题,我们需要用一个CATransformLayers来做根层对象。
函数F_multiplePlanesZAxis修正了这个问题:

//Create the container as a CATransformLayerCATransformLayer*container=[CATransformLayerlayer];container.frame=CGRectMake(0,0,640,300);[self.view.layeraddSublayer:container];

CATransformLayer是一个特殊的layer。与CALayer的不同之处在于,CATransformLayer本身不会被渲染到屏幕上,只有它的子图层才会被渲染到屏幕上。所以它的一些属性,比如backgroundColor、contents、border等等都没有什么用。要记住这点。
到这里,这个教程的第一部分就完了。建议你实际运行一下这些函数,也可以试试我没有讲到的CATransform3DScale,试试用它做一个缩放变换看。
如果你有任何问题,可以在twitter上找到我(@bitwaker)。
本教程代码下载
译自:Think
& Build

文章目录
|