8.4、公告牌
公告牌是一直面向观察者的纹理矩形。通常有两个变种:第-种,圆柱形公告牌,会面向观察者但是还会与“上”矢量保持平行。第二种,球形公告牌,就像点精灵一样总是会与平截体的近面平行。

图8-6显示的是使用圆柱形公告牌渲染的树。图8-6展示了高细节树木的假象,只有右下角的树显得像纸一样薄。当观察者沿着“上”矢量俯视公告牌时,图8-6中的圆柱形公告牌就会露出破绽。

图8-7显示的是球形公告牌。其中的很多公告牌都呈现出逼真的假象,但是近景中的树看起来就像是躺在地面上而不是站在地面上。位于图8-7中心的树的树干甚至完全消失了。当公告牌与近面平行时,一些树的树千会最终渲染到地面以下。
例子OpenGLES_Ch8_4通过在树木之上来回飞动视点来演示公告牌效果。通过切换用户界面开关,可以在变化视点的过程中 对比球形和圆柱形公告牌。两种公告牌都不 能通用于所有情况。
公告牌的几个属性让它的效率远远低于点精灵。每个公告牌至少需要四个顶点,并且每次视点变化后,这些顶点的位置必须要被重新计算并重新发送给GPU。幸运的是,计算公告牌顶点位置并不是太难,或者说并不会耗费太多的计算量。
图8-8阐明了定义视点的视线向量(look-at vector)与, 上向量之间的关系。上述信息用于使公告牌朝向观察者所需的顶点位置计算。回想一下第4章,两个向量的向量积定义了垂直于这两个向量的第三个向量。上向量与视线向量之间的向量积叫做“左向量”,参见图8-8。 对你来说,图8-8中的这个左向量可能看起来是指向右侧的,但是从看向视线向量的眼睛的视点来看,它在左边。请设想一下观察者看向纸张外部时的 情景。

左向量定义了一个平行于视平截体的近面和远面的水平方向。但是,上向量与视线向量之间的角度没有必要是90度,并且没有定义近面和远面的垂直线。图8-9显示的是整个视平截体。“Frustum Up Vector”是视线向量和左向量的向量积。

提供一个位置、一个大小和左向量,我们就可以为圆柱形公告牌计算顶点位置。图8-10显示了原点在底部中心的圆柱形公告牌的几何形状,这个图可以帮助可视化用于计算定义公告牌边界的顶点的方程式。

标准左向量是标准上向量和标准实现向量的向量积。
GLKVector3 leftVector = GLKVector3CrossProduct(
GLKVector3Normalize (upVector ) ,
GLKVector3Normalize ( lookAtVector) ) ;
左向量是标准化的,这意味着它的长度是1.0。公告牌宽度的一半乘以左向量会产生一个长度是所需的长度同时方向与左向量相同的新矢量。左下顶点位置是通过添加这个新矢量到公告牌的底部中心来建立的。
GLKVector3 leftBottomPosition =
GLKVector3Add(GLKVector3MultiplyScalar(
rightVector, size.x * -0.5f), position);
右顶点位置的距离是相同的,但是方向相反。
GLKVector3 rightBottomPosition =
GLKVector3Add(GLKVector3MultiplyScalar(
rightVector, size.x * 0.5f), position);
从底部顶点位置处沿着上向量的方向向上平移公告牌高度的距离就是顶部顶点的位置
GLKVector3 leftTopPosition =
GLKVector3Add(leftBottomPosition,
GLKVector3MultiplyScalar(upUnitVector, size.y));
GLKVector3 rightTopPosition =
GLKVector3Add(rightBottomPosition,
GLKVector3MultiplyScalar(upUnitVector, size.y));
球行公告牌的顶点是使用平截体上向量而不是场景上向量计算出来的,参见图8-11.例子OpenGLES_Ch8_4会计算平截体上向量,并临时重赋值上向量来保持所有其他的用于寻找顶点的位置的方程式相同。
if(self.shouldRenderSpherical)
{ // Recalculate up vector so that particles will be
// parallel to frustum's near plane.
upUnitVector = GLKVector3CrossProduct(
lookDirection,
rightVector);
}s

例子OpenGLES_Ch8_4包含一个简单的UtilityBillBoard类来为每个公告牌存储位置、大小和纹理坐标。
//
// UtilityBillboard.h
//
//
#import <GLKit/GLKit.h>
@interface UtilityBillboard : NSObject
@property (assign, nonatomic, readonly)
GLKVector3 position;
@property (assign, nonatomic, readonly)
GLKVector2 minTextureCoords;
@property (assign, nonatomic, readonly)
GLKVector2 maxTextureCoords;
@property (assign, nonatomic, readonly)
GLKVector2 size;
@property (assign, nonatomic, readonly)
GLfloat distanceSquared;
- (id)initWithPosition:(GLKVector3)aPosition
size:(GLKVector2)aSize
minTextureCoords:(GLKVector2)minCoords
maxTextureCoords:(GLKVector2)maxCoords;
- (void)updateWithEyePosition:(GLKVector3)eyePosition
lookDirection:(GLKVector3)lookDirection;
@end
UtilitbBillboard实现了“-updateWidthEyePosition:lookDirection:” 方法来使用GLKVector3-DotProduct()丽数计算公告牌与眼睛位置之间的一个有符号的距离。这个有符号的距离用于从远到近对比排序公告牌。当公告牌的位置在观察者的后面时这个有符号的距离就是负值。利用这一点可以实现一个简单的优化:给定的一个根据距离排序的公告牌数组,当第一次碰到距离是负值的公告牌时就可以安全地停止绘制更多的公告牌。数组中剩下 的公告牌都是不可见的,因为它们都在观察者的后面。
/////////////////////////////////////////////////////////////////
// Apply Newtonian physics and filters to modify the receiver's
// position and velocity.
- (void)updateWithEyePosition:(GLKVector3)eyePosition
lookDirection:(GLKVector3)lookDirection;
{
const GLKVector3 vectorFromEye = GLKVector3Subtract(
eyePosition, position_);
distanceSquared_ = GLKVector3DotProduct(vectorFromEye,
lookDirection);
}
Utility BillboardManager类跟踪公告牌并渲染它们的方式与第7章的UtilityModel-Manager类跟踪并渲染模型的方式大致相同。通过调用“.addBillboard:”方法来添加要 管理的公告牌。在绘制公告牌之前要调用UtilityBillboardManager类的“-updateWithEye Position:lookDirection:”方法。
UtilityBillboardManager会更新其管理的所有公告牌并按从远到近的顺序排序这些公告牌。UtilityBillboardManager 的“ drawWithEyePosition:lookDirection:upVector:”方 法会使用圆柱形或者球形数学算法来重新计算每个公告牌中的每个三角形的顶点,具体使用哪种数学算法取决于UtilityBillboardManager的shouldRenderSpherical属性,然后会使用glDrawArrays()函数来绘制所有的可视公告牌。
//
// UtilityBillboardManager.h
//
//
#import <GLKit/GLKit.h>
@class UtilityBillboard;
@interface UtilityBillboardManager : NSObject
@property (strong, nonatomic, readonly)
NSArray *sortedBillboards;
@property (assign, nonatomic, readwrite)
BOOL shouldRenderSpherical;
- (void)updateWithEyePosition:(GLKVector3)eyePosition
lookDirection:(GLKVector3)lookDirection;
- (void)addBillboard:(UtilityBillboard *)aBillboard;
- (void)addBillboardAtPosition:(GLKVector3)aPosition
size:(GLKVector2)aSize
minTextureCoords:(GLKVector2)minCoords
maxTextureCoords:(GLKVector2)maxCoords;
@end
排序可以利用Cocoa Touch的内置功能。NSArray 类提供了所需的“sortedArrayUsing-Function:context:”方法。UtilityBillboard提供了一个叫做UtilityCompareBillboard- Distance()的适合与“-sortedArrayUsingFunction:context:” 一起使用的函数。
/////////////////////////////////////////////////////////////////
//
- (void)updateWithEyePosition:(GLKVector3)eyePosition
lookDirection:(GLKVector3)lookDirection;
{
// Make sure lookDirection is a unit vector
lookDirection = GLKVector3Normalize(lookDirection);
for(UtilityBillboard *currentBillboard in
self.sortedBillboards)
{
[currentBillboard updateWithEyePosition:eyePosition
lookDirection:lookDirection];
}
// Sort from furthest to nearest with dead particles and
// particles behind the viewer ordered before all others.
// Note: dead particles are available for reuse.
[self.mutableSortedBillboards
sortUsingFunction:UtilityCompareBillboardDistance
context:NULL];
}
浏览一下UtilityBillboardManager.m中的UtilityBillboardManager类的实现。这个类的代码并不多并且重用了很多在前面的类似UtilityMesh的类中使用的用于排序和管理顶点属性的技术。公告牌是组织网格和模型背后的几何图形的最普遍方式之一。但是,由于需要不断地更新顶点位置,公告牌会消耗大量的CPU和GPU之间宝贵的内存带宽。在GPU硬件中创建和实现的点精灵可以减少对公告牌的需求。
8.5 小结
特效会在尽量不增加GPU处理负载的情况下增加场景的视觉丰富性。每个特效都有自己的局限性。天空盒必须要放置在正确的位置范围内,以便视点不会过于扭曲纹理以至于损坏最终的渲染图像。使用点精灵实现的粒子效果是非常高效的,但是GPU无法从远到近排序粒子,而不排序的话,有些半透明的粒子在渲染时就会出现瑕疵。公告牌会增加场景细节,但是与天空盒类似,它也不能适用于所有情况。可以通过约束视点 来避免暴露公告牌的深度缺失。
对于嵌人式图形应用来说,性能是至关重要的,并且特效通常是在保持良好性能的情况下提供一些令人印象深刻的结果。下一章会讲解优化技术,就是在特效不够时提高性能。