12.2、一切如故

本书前面的例子完全是在视图和控制器子系统中演示3D概念的。这些简单的例子要么完全缺少一个模型子系统,要么有一个不完整的模型子系统,比如封装在视图控制器中的一个三角形或者一个简单的网格。随着应用复杂性的增加,把代码分割成不同的MVC子系统的迫切性也会增加。几十年的使用证明了MVC结构对于组织复杂应用的价值。类似UIView、GLKView、UIViewController 和GLKViewController的CocoaTouch类的名字强调了它们的作用。当把一个全功能的iOS应用所需的所有部件汇集在一起时,几乎总是使用MVC结构才能够实现最好的代码组织形式。

接下来,简单地介绍了组成例子OpenGLES_Ch12_1的类。这里描述的作用、关系和交互会阐明每种技术在你自己的应用中可能会使用的方式。

12.2.1 控制器子系统:

例子OpenGLES_Ch12_1中的控制器子系统仅仅由两个类组成,这两个类的实现都会尽可能地使用适量的代码,两个类一共大约由 1200行代码组成。

1. OpenGLES_Ch12_1AppDelegate

与第10章的例子类似,例子OpenGLES_Ch12_1会使用苹果的Core Data框架来加载地形数据和3D模型。Core Data的存在是为了简化模型子系统内的复杂的对象和关系的存储。OpenGLES_Ch12_1AppDelegate 类包含了与Core Data进行交互所需的所有代码。

类OpenGLES_Ch12_1AppDelegate 还会使用在第7章介绍,并在第9章继续探讨的方法来从一个.modelplist文件中加载一个火箭推进悬空车的3D模型。第7章讲解了紧凑型二进制“ .modelplist”文件格式以及它的优点。类OpenGLES_Ch12_1AppDelegate会让应用的其余部分与模型对象的存储细节分隔开来。不同的OpenGLES_Ch12_1AppDelegate实现会使用完全不同的技术来加载地形和模型数据,同时不会对例子OpenGLES_Ch12_1的其余部分产生影响。

2. OpenGLES_Ch12_1ViewController

类OpenGLES_Ch12_1ViewController管理着模型和视图子系统对象的周期性更新。天空盒实现了完整的地形效果所需的广阔远景。粒子为渲染场景添加了交互性,有助于表现火箭车悬空于地面的效果。天空盒和粒子特效是在第8章讲解的。

类OpenGLES_Ch12_1ViewController 还会跟踪显示刷新率,并会在刷新率明显下降时,试着适时地减少绘图细节,就像在旧式iOS设备或者iPad和iPhone模拟器上所做的一样。在每帧中渲染复杂的地形网格和数以千计的粒子会加重GPU的负担。类OpenGLES_Ch12_1ViewController会降低绘制简化的地形网格时的阀值距离,并会在刷新率低于20Hz时中断粒子的渲染。根据分析以及苹果公司的OpenGLES性能检测应用,当运行在iPad2,上时,例子OpenGLES_Ch12_1并不会受限于GPU的性能。

OpenGLES_Ch12_1ViewController 会把方法整理成组,这些组包括:“Load time configuration”、“ Device orientation”、“ View lifecycle” 、“Accessors ”、“GLK ViewDelegate updating”、“GLK ViewDelegate drawing”、‘Camera delegate”和“Responding to gestures and actions”。 这些组涵盖了GLKViewController子类所起的所有作用,除了Camera delegate。对于所有的方法来说,哪一个方法要归于哪一类中并没有特定的原因,但是 典型的基于GLKit的应用大概会包含所有的这些方法组。

12.2.2 模型子系统

地形、火箭车和物理效果的模拟是在模型子系统内实现的。在Xcode工程OpenGLES_Ch12_1内的一个Core Data“Entity Relationship”(实体关系) 文档(OpenGLES_Ch12_1.xcdata-modeld)中记录了地形和模型位置的信息。Core Data实现了地形和模型位置的存储,以 便在例子OpenGLES_Ch12_1启动时可以很容易地加载信息。这个Xcode工程还包含两个由Xcode根据Core Data文档生成的类,TETerrain 和TEModelPlacement。通常,没 有必要为了封装Core Data“ Entities'(实体)而生成类。在例子OpenGLES_Ch12_1中这样做主要是为了实现Objective-C类别的创建,通过创建类别来添加与地形和模型进行交互的方法。

生成的TETerrain和TEModelPlacement类形成了模型子系统的核心,但是本节中介绍的其他几个类也是需要的。例如,TECart类封装了悬空车的相关信息。TECart 实 例是在控制器子系统中由OpenGLES_Ch12__1AppDelegate 按需创建的,这个实例会履行模型控制器的主要责任,即加载或者创建模型子系统对象。其余的模型子系统类会被用于TETerrain、TEModelPlacement 和TECart类的实现中。例如,TETerrain 会使用UtilityTextureInfo来存储地形纹理数据,使用UtilityMesh和UtilityModel来存储3D模型数据。TECart会使用在AGLKCollision和AGLKFilters中声明的函数来实现物理效果。

1. AGLKCollision

在AGLKCollision.h和AGLKCollision.m文件中包含着非常有用的用于检测3D对象之间的碰撞的函数。尤其是下面的AGLKRayDoesIntersectTriangle()函数会计算一条 光线与一个三角形之间的交点。回想一下,地形网格是由数以千计的三角形组成的。给定一个在地形的X和Z位置的对象,以及一条从这个对象上直接投射下来的光线,通过AGLKRayDoesIntersectTriangle()函数引用返回的intersectionPoint提供了这条光线与一个地形三角形表面的交点。AGLKRayDoesIntersectTriangle() 函数用于让火箭车看起来悬空在地形表面之上。

extern BOOL AGLKRayDoesIntersectTriangle(
   GLKVector3 rayDirection,
   GLKVector3 pointOnRay,
   GLKVector3 trianglePointA,
   GLKVector3 trianglePointB,
   GLKVector3 trianglePointC,
   GLKVector3 *intersectionPoint);

如果在这条光线与三角形之间找到一个交点,那么AGLKRayDoesIntersectTriangle()函数会返回YES,如果找不到交点就返回NO。结果NO表明一个对象不在地形之上; 也就是说这个对象驶出了地形的边界。

光线是由在这条光线上的任意一点pointOnRay以及用来指定这条光线的方向的一个矢量rayDirection组成的。火箭车的位置和Y轴的负方向共同定义了一条从火箭车投射出来的光线。任意三点,trianglePointA、trianglePointB 和trianglePointC定义一个三角形。由三个角组成的地形网格三角形用于计算光线与地形的交点。

2. AGLKFilters

在AGLKFilters.h和AGLKFilters.m文件中包含着用于低通滤波的函数,低通滤波是在第6章讲解的。在例子OpenGLES_Ch12_1的多个地方使用了低通滤波函数,以实 现流畅的动画效果所需的增量变化。例如,当一个悬空车转向时,逐渐转向要比直接跳转到一个新方向的效果更好。逐渐转向是使用AGLK Vector3LowPassFilter()过滤函数实 现的,具体会增量更新代表火箭车的当前朝向的矢量,直到与新目标方向一致。


extern GLKVector3 AGLKVector3LowPassFilter (
    GLfloat fraction,
    GLKVector3 target,// target value to approach
    GLKVector3 current) ;// current value

目标矢量指定了一个新的值。当前矢量指定了当前值。参数fraction 用于指定这个函数返回的矢量与目标矢量或者当前矢量之间的接近程度。例如,如果fraction等于0.0,那么返回值与当前值相同。如果fraction等于1.0,那么返回的值与目标值相同。当fraction的值介于0.0与1.0之间时,返回的矢量会对应于目标矢量跟当前矢量之间的一个矢量。

动画效果是随着时间变化值而产生的,具体是通过使用一个逐渐变化的fraction参数调用一个过滤函数来实现的。fraction参数通常是通过使用自动画开始起的逝去时间, 除以完成这个动画所需的总时间计算出来的。例如,一个火箭车只有在从开始转向起所逝去的时间的总数量,等于或者超过为转向所分配的时间量时,才会完成转向。

3. AGLKFrustum

第5章介绍了平截体的数学概念,角锥体形状描述了被坐标系变换置为可见的空间范围。第9章介绍了AGLKFrustum数据类型和利用它执行计算的函数。AGLKFrustum存储了编码在标准的投影和model-view变换矩阵中的相同信息。AGLKFrustum.h 文件中声明了用来初始化AGLKFrustum数据类型的、在AGLKFrustum和矩阵之间来回转换的,以及决定点或者球是否在平截体内的函数。在模型子系统中声明的AGLKFrustum类型是一个纯粹的数学结构,无论特定视图子系统的实现是怎么样的,这个结构都是有效的。在UtilityCamera类中和模型子系统的模拟中都使用了AGLKFrustum数据类型。

4. TE Terrain (modelAdditions)

modelAdditions类别向CoreData生成的类TETerrain添加了方法。添加的这些方法简化了地形相关的计算。例如,与在CoreData中用来存储值的单位不同, “-widthMeters”方法会返回以米为单位的整个地形的宽度。“-calculatedHeightAtXPosM- eters:zPosMeters:surfaceNormal:”方法会使用在AGLKCollision.h中声明的函数来返回在地形的X和Z位置处的高度。这个方法还会通过引用返回一个指定了在碰撞点处的地形的坡度矢量。坡度在物理模拟中具有重要作用,有助于确定重力对于火箭车的影响力。

5. TECart

每个TECart实例都有对应的一个3D模型、一个3D位置和一个速度,并会存储任意“推进”加速的当前量。还有多个其他的属性是基于位置和速度按需计算出来的,比如用于定义火箭车前进方向的一个矢量。火箭车会向它的可选委托对象报告自身 位置的改变,以给予它的委托一个影响这个改变的机会。例如,在控制器子系统中的OpenGLES_Ch12_1AppDelegate 实例充当了火箭车的委托,它会限制火箭车的位置以防止,火箭车跑出地形的边缘。在CocoaTouch应用中,使用控制器对象作为模型和视图对象的委托是非常普遍的。

TECart实例实现了在“- (void)updateWithController:(id)controller”方法中的物理模拟。下面在TECart.h文件中声明的TECartControllerProtocol协议指定了控制器必须要实现的方法。

@protocol TECartControllerProtocol

- (NSTimeInterval)timeSinceLastUpdate;
- (TETerrain *)terrain;
- (UtilityBillboardParticleManager *)particleManager;
- (TECart *)playerCart;

@end

TECartControllerProtocol协议的“ -timeSinceLastUpdate”和-terrain 方法可用于计算火箭车在地形表面上的位置。“.particleManager”方法没有在模型子系统内使用, 但是当TECart的“-(void)emitParticlesWithController:(id )controller”方法被视图子系统内的TECart (viewAdditions) 类别覆写后,这个方法就可以用于实现粒子效果。TECart 使用“-playerCart”方法来决定火箭车是受玩家控制还 是受人工智能控制。

6. TEParticleEmitter

TEParticleEmitter类存储了一个位置属性并声明了一个用于控制粒子发生器的Objective-C块类型。对于火箭车来说,粒子发生器的位置是相对于火箭车的位置定义的。在火箭车的3D模型中包含的一个小的三角金字塔形的3D模型指定了粒子发生器的位置。在类似谷歌SketchUp的3D模型编辑工具中,一般使用一个小的3D模型来显示粒子发生器的位置。

TEParticleEmitter实例是在“ -(id)initWithModel:(UtilityModel *)aModel”方法中初始化的。aModel参数的几何中心指定了粒子发生器的位置。实际的粒子生成器位于类似如下类型的块中:

typedef void (^TEParticleEmitterBlock)(
   GLKVector3 position,
   UtilityBillboardParticleManager *manager,
   NSTimeInterval elapsedTime,
   id owner);

通过向TEParticleEmitter的“-updateWithParticleEmitterBlock:manager:elapsedTime: owner:”方法传递块,能够灵活地实现多个粒子效果。使用块可以在不改变TEParticle-Emitter类本身的情况下变化粒子发生逻辑。;

7. UtilityBillboardParticle

每个UtilityBillboardParticle实例都会存储position、velocity、 force、 initialSize、 finalSize、lifeSpanSeconds、fadeDurationSeconds, minTextureCoords和max TextureCoords属性。这些属性都可以用来产生动画:改变位置会移动粒子。通过velocity和force属性可以实现基于逝去时间和物理学来计算新位置的方式。粒子会从初始大小通过逐渐变换而长大或者缩小为最终大小。当一个粒子 走完它的生命周期后,就不必再绘制这个粒子了。粒子 透明度会根据faceDurationSeconds的值在完全不透明(1.0) 和完全透明(0.0) 之间变化。如果faceDurationSeconds等于0.0,那么这个粒子永远不会褪色。如果fadeDurationSeconds大于lifeSpanSeconds,那么这个粒子在它第次被绘制时是半透明的。

UtilityBillboardParticle还会按需计算distanceSquared、isAlive、 lifeRemainingSeconds、size和opacity属性,以提供粒子的当前状态。当这个粒子被通过"-updateWith- ElapsedTime:frustum:”方法而更新时,以及当这个粒子被用于根据视平截体眼睛位置解平方而来的有符号距离而排序粒子的计算时,就需要计算distanceSquared属性。 粒子渲染是在视图子系统内实现由UtilityBillboardParticleManager 类执行的。视图 子系统需要排序粒子以产生正确的渲染结果,具体内容将在“UtilityBillboardParticle Effect和Utility Bilboard Particle Shader”一节中讲解。:

虽然现在还没有实现,但有效的公告牌属性可以用于计算公告牌与模型子系统内的其他对象之间的碰撞。例如,在一个射击游戏中,基于公告牌的粒子可以被用于表现子弹或者激光束。公告牌和其他对象之间的碰撞可以被模型子系统内的物理模拟器侦测到。

8. UtilityTexturelnfo

UtilityTextureInfo类封装了存储在二进制“.modelplist”文件中的原始纹理数据。在模型子系统内,UtilityTextureInfo 只提供了简单的存储。这个类被视图子系统内的UtilityTextureInfo(viewAdditions)类别扩展而实现了UtilityTextureInfo 实例和GLKTextureInfo实例间的转换,以在GLKit和OpenGL ES中使用。

9. UtilityMesh

UtilityMesh类首先是在第6章介绍的,用于管理大型数量的几何顶点数据,以及命令处理数据的子范围。网格是3D图形中要用到的关键数据结构。网格定义了3D模型的形状。虽然地形数据是使用TETerrain类而不是UtilityMesh类来表示的,但地形使用的仍然是网格概念的一个变体。 在AGLKCollision.h中声明地用来检测3D对象之间的碰撞的函数,也能够用于检测任意三角形组(包括网格)之间的碰撞。

用于渲染网格的方法是由在视图子系统中实现的UtilityMesh(viewAdditions)类别提供的。通常在模型子系统中实现抽象的、几何学的、数学的方法以及数据存储,然后在视图子系统内扩展对于这些数据的操作。无论渲染怎么执行,这些几何运算都是有效的,并且支持用于类似碰撞检测的非图形计算。使用在视图子系统中实现的类别可以从通用几何中分离出特殊的渲染方法。例如,组成一个网格的几何数据可以被显示为一个表中的坐标值。使用表的视图子系统可以用一个类别来扩展UtilityMesh类,这个类别可以包含为Cocoa Touch UITableView对象提供数据的方法,而不是用于3D渲染的方法。

10. UtilityModel

每个UtilityModel实例会存储一个mesh属性,这个mesh属性用于定义一个或者多个3D模型的几何图形。UtilityModel还会存储一个name属性,用于指定3D模型所需的特定网格子集。计算出来的axisAlignedBoundingBox属性提供了一个边界框,这个边界框容纳了一个模型所包含的所有网格顶点。属性doesRequireLighting指定了是否要逐个使用OpenGL ES灯光计算来渲染3D模型。

第6章介绍了UtilityModel类以及它与UtilityMesh类之间的关系。网格对大型数量顶点数据的存储做了优化,具体是使用一个适合向GPU控制的内存转移的格式来存储。在一次操作中向GPU发送所有的网格数据就是一个性能优化。然后3D模 型会协调已经存储在GPU控制的内存中的网格顶点子集的渲染。在视图子系统内的UtilityModel (view Additions)类别实现了对于UtilityModel实例的渲染。

12.2.3 视图子系统

视图子系统包含了用于显示地形、模型,以及类似按钮的用户界面对象的代码。很多在模型子系统中声明的类是使用在视图子系统内的类别来扩展出特殊的渲染能力的。为了理解模型和视图子系统之间的去耦合和分离,必须要问一个关键的问题:“是否能够基于相同的模型子系统来实现完全不同的视图子系统,是否能够在不影响视图子系统的情况下改变模型子系统的实现?”例子OpenGLES_Ch12_1证明这些是可以办到的。

例如,模型子系统可以在不影响视图子系统的情况下,使用一个数据存储结构来代替Core Data。视图子系统可以在不影响模型子系统的情况下,被完全地从工程中省略掉;在例子OpenGLES_Ch12_1中的模型子系统不含有对于视图子系统的引用或者依赖。

在本节讲解的类和类别演示了怎么为去耦合性的模型子系统所提供的抽象几何图形数据实现相对复杂并且全功能的3D渲染。视图子系统中的代码仍然适用于很多由纹理3D模型、公告牌和粒子组成的3D应用。

1. UtilityEffect

第10章介绍了用于加载、编译、连接和验证OpenGL ES 2.0 Shading Language程序的UtilityEffect类。UtilityEffect 遵守GLKit的GLKNamedEffect协议。UtilityEffect会实现两个方法,“-bindAttribLoactions”和“ -configureUniformLocations",当这两个方法被直接调用时会生成异常。UtilityEffect 的子类必须要实现这两个方法。

UtilityEffect的“-(BOOL)loadShaders WithN ame:(NSString * )aShaderName'方法会加载并编译顶点和片元ShadingLanguage程序,使用由aShaderName参数指定 的根名,以及后缀/扩展名“.vsh” 和“.fsh"。“-loadShadersWithName:”方法会按需调用“-bindAttribLocations”和“configureUniformLocations”函数,如果碰到任何问题就返回NO,如果成功就返回YES。

在例子OpenGLES_Ch12_1中的UtilityModelEffect、UtilityPickTerrainEffect、 Utility-TerrainEffect和UtilityBillboardParticleEffect类都是UtilityEffect的子类。它们实现了 “-bindAttribLocations”和“-ConfigureUniformLocations”方法,以绑定属性并配置特定于它们各自的Shading Language程序的统一值存储。

2. UtilityModelEffect和UtilityModelShader

UtilityModelEffect类会协调UtilityModelShader.vsh顶点Shading Language 程序和UtilityModelShader.fsh片元Shading Language程序的执行。UtilityModelShader 程序是相对简单的,并且只使用一个纹理、一个环境光和一个定向漫反射光来渲染提交的几何图形数据。因为简单所以可以使用传统OpenGL灯光计算的一个子集来有效地渲染3D模型。第3章讲解了纹理。第4章讲解了环境和漫反射光的灯光方程。

UtilityModelShader. fsh Shading Language程序会丢弃所有接近透明的片元。丢弃它们可以防止对于深度渲染缓存的无效更改,当片元重叠时,深度渲染缓存会决定新生成 的片元是否要替换先前生成的片元。例如,假如没有丢弃,当为一个模型生成的一个接近透明的片元,处于为另一个模型生成的一一个不透明片元的前面时,那么接近透明的片元会阻止不透明的片元对最终像素颜色渲染缓存内容的影响,从而导致一个错误的渲染结果。在这种情况下,正确的结果应该是能够透过接近透明的片元看到不透明的片元,但是又不能完全显示不透明的片元。第5章介绍了深度渲染缓存,第3章讲解了用来影响片元颜色的操作。把接近透明的片元丢弃掉可以避免问题,并且不会降低场景的渲染质量,因为接近透明的片元是很难察觉到的。

3. UtilityPick TerrainEffect和UtilityPick TerrainShader

第10章讲解了UtilityPickTerrainEffect 类以及相关的用来渲染伪色彩地形和模型的UtilityPick TerrainShader Shading Language程序。场景会被渲染人一个由UtilityPickTerrainEffect实例控制的像素颜色渲染缓存中。地形的X和Z位置坐标会被编码人片元的红色和绿色颜色分量中。例如,由地形的{100,50,75}位置生成的片元颜色是浮点RGB颜色{100.0 / terrainWidth, 75.0 / terrainLength, 0.0}。在像素颜色渲染缓存的特定位置的结果像素颜色指定了对应于这个位置的地形坐标。3D模型索引是与编码在像素颜色渲染缓存内的像素的蓝色分量中的伪色彩一起渲染的。当从像素颜色渲染缓存中读回一个像素颜色时,如果这个颜色有一个非零的蓝色分量,那么这个蓝色分量 的值就是用来查询这个像素颜色所对应模型的一个索引。

在例子OpenGLES_Ch12_1中并没有使用UtilityPickTerrainEffect类,但是保留了 第10章所实现的UtilityPickTerrainEffect类。在OpenGLES_Ch12_1中利用拾取的代码就留给读者来完成吧。例如,触摸事件可以被用于实现从玩家的火箭车向触摸位置发射导弹。如果玩家能够通过估计当导弹到达时火箭车的所在位置来消灭计算机控制的火箭车的话,这个模拟程序会变得更像一个游戏。玩家可能还要躲避计算机控制的火箭车发 来的导弹。

4. UtilityTerrainEffect和Utility TerrainShader

第10章讲解了UtilityTerrainEffect类以及相关的用来渲染纹理地形的UtilityTerrain-Shader Shading Language程序。这个Shading Language程序会把4个纹理混合在起来计算地形几何体所产生的片元的颜色。每个纹理会被施加不同的纹理变换矩阵和混合权重,这样做可以实现多种类型的最终渲染结果。第3章讲解了纹理变换。用来混合纹理的权重会与预计算的灯光强度值一起存储在第 5个纹理中。换句话说,灯光被烘焙进了第5个纹理中,因此这个UtilityTerrainShader Shading Language程序就没有必要执行灯光计算了。

地形渲染会应用一些在本书中用过的最复杂的Shading Language程序,但是即便这样,在第一代iOS设备上,仍然可以有很好的性能表现。如果使用早前不支持Shading Language程序的OpenGLES版本的话,想要获得同等的性能表现和渲染结果几乎是不可能的。

5. UtilityBillboardParticleEffect和UtilityBilboardParticleShader

UtilityBillboardParticleEffect类以及相关的UtilityBillboardParticleShader Shading Language程序会渲染半透明粒子。粒子需要一个稍微不寻常的顶点属性。

typedef struct
{
  GLKVector3 position;
  GLKVector2 textureCoords;
  GLfloat opacity;
}
BillboardVertex;

每个顶点的position和纹理坐标textureCoords属性比较简单,并且与类似UtilityModel-Effect的其他效果中使用的顶点属性相似。但是,公告牌粒子顶点存储了一个opacity 属性,而没有使用顶点法向量属性或者顶点颜色属性。透明度用在Shading Language程序中,用于控制公告牌片元与像素颜色渲染缓存中的现存颜色之间的混合。这个opacity属性相当于红绿蓝和透明度(RGBA)颜色中的透明度分量。这种只存储顶点的透明度而不是全部颜色的方法,既可以节省内存的消耗,又可以提高运行时的性能。

因为粒子是半透明的,所以它们需要在其他的类似地形、模型和天空盒的不透明对象被渲染后再渲染。这样做可以让在像素颜色渲染缓存中的最终颜色描绘透过公告牌粒子观察对象的画面。这些粒子也必须要基于跟观察者的距离按照从远到近的顺序来绘制。否则,就不可能透过近处的公告牌粒子看到远处的公告牌粒子。图12-3显示了期望的渲染结果。对于公告牌粒子的顶点属性以及按照与观察者距离排序的粒子的管理是 在UtilityBillbordParticleManager类中实现的。

图 12-3

6. UtilityBillboardParticleManager

每个UtilityBillboardParticleManager类都会维持UtilityBillboardParticle实例以及与之对应的顶点数据的数组。每次一个UtilityBillboardParticleManager准备好绘制时,它都会基于每个粒子与平截体眼睛位置之间的距离排序UtilityBillboardParticle实例的数组。

- (void)prepareToDrawwithCamera:(UtilityCamera *)aCamera;

UtilityBillboardParticleManager会收集包含在aCamera的平截体内的存活粒子的顶点属性,然后以一个单独的大型三角形集合来提交用于渲染的顶点属性。存活粒子是isAlive属性为YES的粒子。在公告牌经过了lifeSpanSeconds所指定的时间后, Utility BillboardParticle的isAlive属性会变成NO。

“-addParticle:"” 方法会向UtilityBillboardParticleManager实例所维持的公告牌数组 中添加一个粒子。

/////////////////////////////////////////////////////////////////
// Adds aParticle to the end of the particles array. If a
// dead bilboard is available, then one dead billboard is also
// removed so the total number of particles does not increase.
- (void)addParticle:(UtilityBillboardParticle *)aParticle;

为了简化创建新的UtilityBillboardParticle实例并把它添加到管理器过程中的步骤,UtilityBillboardParticleManager提供了如下能够一次完成这个过程的方法。

- (void)addParticleAtPosition:(GLKVector3)aPosition
   velocity:(GLKVector3)aVelocity
   force:(GLKVector3)aForce
   initialSize:(GLKVector2)anInitialSize
   finalSize:(GLKVector2)aFinalSize
   lifeSpanSeconds:(NSTimeInterval)aSpan
   fadeDurationSeconds:(NSTimeInterval)aDuration
   minTextureCoords:(GLKVector2)minCoords
   maxTextureCoords:(GLKVector2)maxCoords;

7. UtilityModelManager

UtilityModelManager类封装了一个网格、一个纹理,以及一个能够使用部分的网格和纹理来渲染的3D模型的集合。第7章介绍了UtilityModelManager类并讲解了可以用于从二进制数据中加载网格和3D模型的方法。3D模型是由UtilityModelManager、UtilityModel、UtilityMesh、UtilityModelEffect, 以及UtilityModelShader Shading Language程序共同处理的,从以二进制数据的形式加载它们开始,一直到像素颜色渲染缓存内的最终渲染结果。

UtilityModelManager充当了3D模型和网格之间的总管理者或者总协调者。对于UtilityModelManager的“-prepareToDraw” 方法的调用还会通过用网格顶点数据配置当前OpenGLES上下文的方法来准备要绘制的网格。方法“-modelNamed:”会返回请求的模型,返回的模型可以接着通过准备一个用于绘图的UtilityModelEffect来渲染,然 后调用3D模型的“-draw”方法。

8. UtilityCamera

第10章介绍了UtilityCamera类,以封装一个视平截体并提供用来移动和旋转虚拟3D世界中的摄像机的便利方法。每当摄像机的位置发生变化时,摄像机也会通知一个可选的委托对象。这个委托有对摄像机的变化做出影响的机会。第10章中的例子使用了委托来保持摄像机在地形的.上面。例子OpenGLES_Ch12_1没有明确地限制摄像机的位置,因为这个例子会一直让摄像机在玩家所控制的火箭车的附近,而火箭车被它们自己的委托限制在了地形的上面。

9. UtilityMesh (viewAdditions)

类别UtilityMesh (viewAdditions)扩展了模型子系统内的UtilityMesh类。下面是这个类别所添加的方法。

@interface UtilityMesh (viewAdditions)

- (void)prepareToDraw;
- (void)prepareToPick;
- (void)drawCommandsInRange:(NSRange)aRange;
- (void)drawBoundingBoxStringForCommandsInRange:
   (NSRange)aRange;

@end

-prepareToDraw”方法会使用网格顶点属性来设置当前OpenGLES上下文,以用于接下来的渲染。“-prepareToPick”方法与“ . prepareToDraw”方法类似,但是当使用UtilityPickTerrainEffect渲染时,“. prepareToPick”方法会设置当前OpenGL ES上下文 去使用一个顶点属性的子集。‘-drawCommandsInRange: ”方法会调用glDrawElements()函数一次或者多次以渲染由网格顶点属性所定义的三角形。参数aRange通常会指定用 来标识一个3D模型所使用的顶点属性的顶点索引的一个子集,但是理论上可以一次绘制多个3D模型。最后,“-drawBoundingBoxStringForCommandsInRange:"方法有时可以帮助可视化调试3D模型的渲染。这个方法会沿着一个盒子的边绘线,这个盒子围住了指定范围的命令所引用的顶点。就像“-drawCommandsInRange:"方法,这个指定的 范围通常只包含一个3D模型,因此最终的盒子会形象地围住这个模型。

10. UtilityModel (viewAdditions)

类别UtilityModel (view Additions)会向模型子系统内的UtilityModel类添加一个额外的方法。这个添加的方法会调用关联网格的“-drawCommandsInRange:" 方法,这个 方法指定了适合模型的命令范围。

/////////////////////////////////////////////////////////////////
// This method draws the receiver using the receiver's
// UtilityMesh and a UtilityModelEffect that have both already 
// been prepared for drawing.
- (void)draw
{
   [self.mesh drawCommandsInRange:NSMakeRange(
      indexOfFirstCommand_, numberOfCommands_)];
}

11. TETerrain (viewAdditions)和TETerrainTile

地形网格由于太大而无法在一次glDrawElements()函数调用中绘制完成,绘制整个网格的话会连看不到的部分也绘制了,这明显是一种浪费。因此,地形网格会被分割为 封装在TETerrainTile类中的瓦片。每个瓦片会引用组成封装在TETerrain实例中的整个地形的地形网格顶点的一个小型子集。第10章介绍过TETerrainTile类、TETerrain类、 以及TETerrain (viewAdditions)类别。

除了地形网格本身,TETerrain 类的实例会引用TEModelPlacement实例。每个模型位置指定了地形内的一个3D模型的名字、位置和方向。例子OpenGLES_Ch12_1会 使用模型位置来绘制树木、灌木、岩石和建筑物。没有理由绘制位于看不到的瓦片中的3D模型。由于这个原因,TETerrain (viewAdditions)类别提供了一个只绘制在指定的瓦片内的模型的方法。

- (void)drawModelsWithinTiles:(NSArray *)tiles
   withCamera:(UtilityCamera *)aCamera
   modelEffect:(UtilityModelEffect *)aModelEffect
   modelManager:(UtilityModelManager *)modelManager

传人“-drawModelsWithinTiles: withCamera:modelEffect:modelManager:' ”方法的Utility-ModelManager对象会使用指定的UtilityModelEffect 来协调3D模型的渲染,利用指定 UtilityCamera提供的变换矩阵。相似的方法会自己绘制瓦片,使用一个特定的摄像机和UtilityTerrainEffect类。

12. TECart (viewAdditions)

类别TECart (viewAdditions)向模型子系统内的TECart类添加了“- (void)drawWithEffect:(UtilityModelEffect *)anEffect;“方法

// Draw the receiver using anEffect.
- (void)drawWithEffect:(UtilityModelEffect *)anEffect;
{
   GLKVector3 position = self.position;

   // Move cart to position
   anEffect.modelviewMatrix = GLKMatrix4Translate(
      anEffect.modelviewMatrix, 
      position.x, 
      position.y, 
      position.z);

   // Transform to cart's orientation
   anEffect.modelviewMatrix = GLKMatrix4Multiply(
      anEffect.modelviewMatrix,
      GLKMatrix4Transpose(self.orientationMatrix));

   [anEffect prepareModelview];

   [self.model draw];
}

火箭车的方向是在模型子系统内计算的,用于让火箭车悬空在地形上,即使地形是倾斜的,火箭车也会按需倾斜。火箭车的方向是相对于火箭车自身的,是由它的orientationMatrix属性定义的,但这个矩阵并不是渲染一个火箭车模型时用来修改UtilityModelEffect的变换矩阵的。相反,在UtilityModelEffect的自身坐标系中必须要使用一个相应的方向矩阵。为了把火箭车的自身坐标系转换成UtilityModelEffect的坐标系,就有必要逆转火箭车的orientationMatrix。“-drawWithEffect:” 方法就是利用这一事实来实现均匀的坐标系缩放的,计算代价较小的GLKMatrix4Transpose(函数与较复杂的GLKMatrix4Invert()函数是等效的。使用转置函数而不用逆函数的这个小优化带来了一个不明显的限制,即火箭车模型不应该被不均匀地缩放。不要试图渲染一个压扁的火箭车。

TECart (viewAdditions) 类别的实现覆写了“ -(void) emitParticlesWithController:(id)controller”方法,这个方法是在模型子系统的TECart总实现中定义的。在视图子系统内对这个方法的新的实现会使用火箭车的particleEmitter属性和UtilityBillboardParticleManager来创建粒子模拟火箭排气管、飞尘和烟雾, 参见图12-4。

13. UtilityTexturelnfo (viewAdditions)

UtilityTextureInfo类封装了一个属性字典,包括用于定义纹理图片的二进制数据属性,但是这个属性不适合直接在OpenGL ES中使用。UtilityTextureInfo (viewAdditions)类别提供了name和target属性,用于OpenGLES确定接下来的渲染所需的一个纹理。第3章讲解了纹理的名字和目标。为了生成所需的名字和目标,而在幕后创建了一个新的GLKTextureInfo实例,并用UtilityTextureInfo的图像数据初始化了这个实例。然后 就可以从GLKTextureInfo实例中获得名字和目标。

图 12-4

为了从UtilityTextureInfo实例创建GLKTextureInfo实例而用下面的类别扩展了GLKTextureInfo实例。向框架类添加方法的能力是Objective-C编程语言最强大的功能之一。

/////////////////////////////////////////////////////////////////
// Returns a GLKTextureInfo instance initialized based upon
// information provided in aDictionary.
+ (GLKTextureInfo *)textureInfoFromUtilityPlistRepresentation:
   (NSDictionary *)aDictionary
{
   GLKTextureInfo *result = nil;

   const size_t imageWidth = (size_t)[[aDictionary 
      objectForKey:@"width"] unsignedIntegerValue];
   const size_t imageHeight = (size_t)[[aDictionary 
      objectForKey:@"height"] unsignedIntegerValue];

   // The imageData property is expected to be a Tiff image
   UIImage *image = [UIImage imageWithData:
      [aDictionary objectForKey:@"imageData"]];

   if(nil != image && 0 != imageWidth && 0 != imageHeight)
   {  // Create GLKTextureInfo corresponding to image.
      NSError *error;
      result = 
         [GLKTextureLoader textureWithCGImage:[image CGImage] 
            options:[NSDictionary dictionaryWithObjectsAndKeys:
               [NSNumber numberWithBool:YES], 
               GLKTextureLoaderGenerateMipmaps, 
               [NSNumber numberWithBool:NO], 
               GLKTextureLoaderOriginBottomLeft, 
               [NSNumber numberWithBool:NO], 
               GLKTextureLoaderApplyPremultiplication, 
               nil] 
            error:&error];

      if(nil == result)
      {
         NSLog(@"%@", error);
      }
      else
      {
         glTexParameteri(GL_TEXTURE_2D, 
            GL_TEXTURE_MAG_FILTER, 
            GL_LINEAR);
         glTexParameteri(GL_TEXTURE_2D, 
            GL_TEXTURE_MIN_FILTER, 
            GL_NEAREST_MIPMAP_LINEAR);
         glTexParameteri(
            GL_TEXTURE_2D, 
            GL_TEXTURE_WRAP_S, 
            GL_REPEAT);
         glTexParameteri(
            GL_TEXTURE_2D, 
            GL_TEXTURE_WRAP_T, 
            GL_REPEAT);
      }
   }

   return result;
}

results matching ""

    No results matching ""