6.1、场景内移动:例子OpenGLES_Ch6_1
动画通常包含两种情况,对象相对于用户的视点移动或者用户的视点相对于对象的位置变化。第5章中的例子OpenGLES_Ch5_6 演示了改变渲染场景中的视点的一个方 法。这个例子会在正射投影和透视投影之间切换。与很多3D图像概念相似,视点存在于一个抽象的数学领域中。实际上,当前的OpenGL ES视点永远不会改变。矩阵定义了几何对象(如三角形)是怎么映射到像素颜色渲染缓存中的片元位置的。改变矩阵进而改变映射,最终创建了视点变化的错觉。其实,观察者并没有移动,只是计算出来的每个可视对象的位置相对于观察者发生了变化。
本章的第一个例子OpenGLES_Ch6_1模拟了一个游乐园碰碰车。碰碰车在墙壁之间来回弹或者相互碰撞。图6-1用两个屏幕截图展示了一个场景的不同视点。第一个屏幕截图显示的是观察者在碰碰车上方向下看的效果。第二个屏幕截图显示了从一个活动中的碰碰车中的视点所观察到的场景。

应用运行时,在示例OpenGLES_Ch6_1中,位置或者法向量等顶点属性都不会发生变化。碰碰车在溜冰场内来回弹的动画完全是由矩阵所代表的坐标系随着时间变化产生的。事实上,每辆车都处在它自已的本地坐标系的{0,0,0}位置。当碰碰车的本地坐标系相对于用来渲染其他碰碰车和溜冰场的坐标系发生变化时,碰碰车就好像在场景内来回移动。
例子OpenGLES_Ch6_1是基于之前章节中介绍的多个概念建立的。当碰碰车彼此相对移动或者相对于观察者移动的时候就产生了动画。当用户切换到“乘坐在车上”(Ride in Car)模式时就产生了视点自身的动画。
6.1.1 看向一个特定的3D位置
OpenGLES_Ch6_1 使用GLKit的GLKMatrix4MakeLookAt()函数来计算一个几乎可以代表任意所需视点的变换矩阵。GLKMatrix4MakeLookAt() 函数与实用函数gluLookAt()相似,gluLookAt() 函数来自桌面版OpenGL,但并不是OpenGL ES的一部分。GLKMatrix4MakeLookAt() 等便利函数重新创建了OpenGL ES丢弃的很多功能,这些函数是在iOS应用中使用GLKit的最好的原因之一。
GLKMatrix4MakeLookAt()有6个参数,参见下面的函数声明。前三个参数指定观察者眼睛的{x,y,z}位置,接下来的3个参数指定观察者正在看向的{x,y,z}位置。GLKMatrix4MakeLookAt()函数会计算并返回一个model-view矩阵,这个矩阵会对齐从眼睛的位置到看向的位置之间的矢量与当前视域的中心线。如果眼睛的位置与看向的位置相同,函数GLKMatrix4MakeLookAt()就不会产生有效的结果。第5章介绍过视域和model-view矩阵。
GLKMatrix4 GLKMatrix4MakeLookAt (
float eyex, float eyeY, float eyez,
float lookatX, float lookatY, float lookatz,
float upX, float upY, float upZ);
GLKMatrix4MakeLookAt()函数的最后三个参数指定了“ 上”方向矢量的{x, y, z}元素。改变“上”方向与倾斜观察者头部的效果相同。
注意
“上” 方向可以是任意矢量,但是GLKMatrix4MakeLookAt()的实现所使用的数学计算不能产生一个有效的直接顺着“上”矢量看的视点。这个限制的存在是因为当直接向“上”或者向“下”看时,GLKMatrix4MakeLookAt()函数使用的数学计算会试图计算90°角的正切,而这在数学上是未定义的。这个“未定义”的 现象还发生在现实世界中,例如,当机械陀螺仪碰到“万象结锁定”并且产生摇摆不可靠的数据时。但是,存在一个巧妙的数学解决方案一四元法, 第11章将做简短介绍,网址https://en.wikipedia.org/wiki/Quaternion也有相关介绍。GLKit包含了一个GLKQuaternion数据类型和一些用来操作它的函数。维基百科讲述了William Rowan Hamilton 爵士在1843 年发现四元法的故事,这是在数学史上最有名和最有趣的故事之一。
OpenGLES_Ch6_1 的OpenGLES_Ch6_1ViewController 使用命名为eyePosition和lookAtPosition的属性保存当前的眼睛位置和看向的位置。控制器的“-glkView:drawInRect:”方法使用如下代码设置当前的model-view矩阵。
// set the modelview matrix to match current eye and look-at
//positions
self.baseEffect.transform.modelviewMatrix =
GLKMatrix4MakeLookAt(
self.eyePosition.x,
self.eyePosition.y,
self.eyePosition.z,
self.lookAtPosition.x,
self.lookAtPosition.y,
self.lookAtPosition.z,
0,1,0);
在3D应用中,活动中的视点叫做“第一人称”视点,因为它呈现了一个观察者站在应用环境内部时的场景。另一种视点称为“第三人称”,它模拟了从一个活动之外的有利位置从上往下看的视图。添加一个新的方法“-updatePointOfView” 到例子的OpenGLES_Ch6_1ViewController来设置“目标”眼睛位置和看向的位置,如下代码所示。目标位置在动画中起了一个重要的作用,下一节会详细讲解。
(void)updatePointofView {
if(!self.shouldUseFirstPersonPOV) {
self.targetEyePosition = GLKVector3Make(6.00,1.75,0.0);
self.targetLookAtPosition = GLKVector3Make(0,0.5,0);
} else {
SceneCar *viewerCar= [cars lastobject];
self.targetEyePosition = GLKVector3Make(
viewerCar.position.x,
viewerCar.position.y+0.45f,
viewerCar.position.z);
self.targetLookAtPosition = GLKVector3Add(
eyePosition,
viewerCar.velocity);
}
第三人称视点会把观察者的眼睛置于溜冰场的侧上部并看向溜冰场中央稍微向上的位置。第三人称眼睛和看向的位置是任意角度的并且不会发生变化。相比之下,第一人称视点会随着观察者所乘坐的碰碰车而移动和转向。眼睛位置被设置为碰碰车当前位置的正上方,看向位置是在碰碰车前面的碰碰车行驶方向上的一个点。在前面的代码中,viewerCar:velocity是-一个用于指定碰碰车行驶方向的带有方向的矢量,并且会指定碰碰车移动的速度。把viewerCar.velocity与eyePosition相加会计算出碰碰车前方的一个位置。
6.1.2 使用时间
类似从第三人称视点瞬间转向第一人称视点的不和谐视觉变化会让用户失去方向感。OpenGLES_Ch6_1ViewController 的“ -update”方法为用户变化渲染视点提供了一个平滑的过渡动画。这个动画是使用一个低通滤波器来逐渐减少当前视点与用户选择的视点之间的差异产生的。低通滤波器会反复逐渐地改变计算出来的值,并且必须调用很多次才能产生一个明显的效果。之所以叫做“低通”是因为对于正在被过滤的值来说,低频的、长期的变化会有一个明显的影响,而高频变化的影响甚微。下面来自OpenGLES_Ch6_1的SceneCar.m文件的代码实现了低通滤波器。
// This function returns a value between target and current. Call
// this function repeatedly to asymptotically return values closer
// to target: "ease in" to the target value.
GLfloat SceneScalarSlowLowPassFilter ( NSTimeInterval elapsed, GLfloat target, GLfloat current ) {
return current + (4.0 * elapsed * (target - current));
}
GLKVector3 SceneVector3SlowLowPassFilter ( NSTimeInterval elapsed,GLKVector3 target,GLKVector3 current ) {
return GLKVector3Make(
SceneScalarSlowLowPassFilter(elapsed, target.x, current.x),
SceneScalarSlowLowPassFilter (elapsed, target.y, current.y),
SceneScalarSlowLowPassFilter(elapsed, target.z, current.z));
}
低通滤波器是通过反复调用来工作的,每次调用会返回一个更接近“目标”值的新的当前值。每当调用OpenGLES_Ch6_1ViewController 的“-update" 方法时,也会调用低通滤波器函数并且当前视点变得更加接近目标视点,直至当前视点最终与目标视点一致。
与低通滤波器类似的函数会让动画更流畅。OpenGLES_Ch6_1也会在改变碰碰车的方向时使用过滤器。当碰碰车从墙壁弹回并转向时,碰碰车并不会瞬间转向新的方向,而是先让目标方向变为新的方向,然后碰碰车的当前方向逐步更新直到与目标方向一致。几乎所有的3D模拟都受益于这样或者那样的过滤器。
OpenGLES_Ch6_1向之前章节中开发的类所在的组添加了五个新类: SceneMesh、SceneModel、SceneCar、SceneCarModel 和SceneRinkModel。SceneMesh 类的存在是为了管理大量的顶点数据以及GPU控制的内存数据的坐标转换。网格(mesh)就是共享顶点或者边,同时用于定义3D图形的三角形的一个集合。SceneModel 类会绘制全部或 者部分的网格。一个单独的模型可能由多个网格组成,多个模型可能共用相同的网格。模型代表了汽车、山脉或人物等3D对象,这些3D对象的形状是由网格定义的。模型聚合了描绘3D对象所需的所有网格。SceneCar类封装了每个碰碰车的当前位置、速度、颜色、偏航角和模型。偏航(yaw) 是来自轮船和航空的一个术语,代表了围绕垂直轴的旋转度,在这里是围绕Y轴。偏航定义了碰碰车的方向并且会随着时间变化而让碰 碰车面向它移动的方向。SceneModel的子SceneCarModel封装了一个碰碰车形状的网格。每个SceneCar实例都使用同-一个SceneCarModel实例。如果想要碰碰车拥有不同的外形,比如有的看起来像卡车,有的看起来像飞机,那么就需要每个SceneCar实例使用一个不同的模型。最后SceneModel的子类SceneRinkModel封装了代表溜冰场的墙壁和地面的网格。
SceneCar实现了“-drawWithBaseEffect:” 方法,这个方法会设置当前材质的颜色以匹配碰碰车的颜色,平移model-view坐标系到碰碰车的当前位置,旋转坐标系以匹 配碰碰车的当前偏航角,并绘制碰碰车模型。平移和旋转已经在第5章中讲过。
- (void)drawWithBaseEffect:(GLKBaseEffect * )anEffect {
GLKMatrix4 savedModelviewMatrix = anEffect.transform.modelviewMatrix;
GLKVector4 savedDiffuseColor = anEffect.material.diffuseColor;
GLKVector4 savedAmbientColor = anEffect。material.ambientColor ;
anEffect.transform.modelviewMatrix = GLKMatrix4Translate(savedModelviewMatrix, position.x, position.y, position.z);
anEffect.transform.modelviewMatrix = GLKMatrix4Rotate ( anEffect.transform.modelviewMatrix,self.yawRadians, 0.0,1.0, 0.0);
}
// Set the model's material color
anEffect.material.diffuseColor = self.color;
anEffect.material.ambientColor = self.color;
[anEffect prepareToDraw];
[model draw];
anEffect.transform.modelviewMatrix = savedModelviewMatrix;
anEffect.material.diffuseColor = savedDiffuseColor;
anEffect.material.ambientColor = savedAmbientColor;
本章中的所有例子都会重用SceneMesh类,并且除了一个之外其余所有的例子都会使用SceneModel类。在每个例子中的视图控制器都会分配并初始化SceneMesh和SceneModel实例。控制器的“ -update”方法会被自动地按照显示刷新率调用,并且这个方法会更新变量以间接地控制绘图坐标系。然后,控制器的“-glkView:drawInRect:'方法会发送绘制模型和网格的消息。