8.3、粒子
在计算机图形中,粒子通常指的是任意的有颜色的或者有纹理的几何对象,这个几何对象可以用在3D视域中的一个单独的位置来定义。应用通常要每次渲染数百或者数千个粒子,这让高效渲染变得非常重要。应用会快速重设粒子的位置以产生逼真的动画。图8-5显示的是例子OpenGLES_Ch8_3所演示的多个

粒子效果之一。测试一下这个例子,感受一下动画效果。
与所有其他GPU渲染的图形一样,可以使用点、线段,或者三角形来实现粒子效果。使用由很多三角形组成的几何球体来实现屏幕上的每个粒子,这样的实现粒子效果的方式要付出非常高昂的计算代价。从概念上讲,球体可以做出理想的粒子效果,因为它们是使用一个单独的点来定位的,即球体的中心位置,同时它们还具有径向对称性,这意味着不管从哪一个视点观看,它们的图形都是相同的。
幸运的是,这个球面粒子效果可以使用与视平截体的近面和远面平行的纹理矩形来创建。每次渲染场景时,都是使用视平截体来定义3D可见视域的,如5.6节所述。与近面和远面平行的矩形是将要在8.4节讲解的公告牌类型。不过,OpenGLES还包含一个叫做点精灵(pointsprites)的功能,这个甚至要比用两个三角形定义的公告牌矩形渲染起来更高效。
当你用glDrawArraysO或者glDrawElements()函数指定GL POINTS模式后,OpenGL ES 2.0就会渲染点精灵。点精灵会产生以这个点精灵的位置为中心的正方形内的每个像素颜色渲染缓存位置的片元。点精灵正方形的长和宽等于在像素颜色渲染缓存坐标系中的当前点精灵的尺寸。不幸的是,自定义Shading Language程序需要控制每个点精灵的尺寸。
例子OpenGLES_Ch8_3创建了AGLKPointParticleEffect类,这个类封装了所需的自定义Shading Language程序,并提供了控制粒子效果的其他选项。每个粒子都有一个初始位置、速度和尺寸。每个粒子的力向量都会随着时间改变粒子的速度。模拟的全局重力也会影响每个粒子的速度。最后,每个粒子都会随着时间褪色,直到完全透明,并且每个粒子都有一个寿命,超过这个寿命后,这个粒子就不再绘制了。
下面的代码声明了AGLKPointParticleEffect类的接口。
//
// AGLKPointParticleEffect.h
// OpenGLES_Ch8_3
//
#import <GLKit/GLKit.h>
/////////////////////////////////////////////////////////////////
// Default gravity acceleration vector matches Earth's
// {0, (-9.80665 m/s/s), 0} assuming +Y up coordinate system
extern const GLKVector3 AGLKDefaultGravity;
@interface AGLKPointParticleEffect : NSObject <GLKNamedEffect>
@property (nonatomic, assign) GLKVector3 gravity;
@property (nonatomic, assign) GLfloat elapsedSeconds;
@property (strong, nonatomic, readonly) GLKEffectPropertyTexture
*texture2d0;
@property (strong, nonatomic, readonly) GLKEffectPropertyTransform
*transform;
- (void)addParticleAtPosition:(GLKVector3)aPosition
velocity:(GLKVector3)aVelocity
force:(GLKVector3)aForce
size:(float)aSize
lifeSpanSeconds:(NSTimeInterval)aSpan
fadeDurationSeconds:(NSTimeInterval)aDuration;
- (void)prepareToDraw;
- (void)draw;
@end
AGLKPointParticleEffect的工作方式与GLK BaseEffect和到目前为止出现的其他“effects”类似。texture2d0 属性定义了用于每个粒子的纹理。transform 属性存储了由transform.projectionMatrix和transform.modelviewMatrix所定义的视点。两个新属性 gravity和elapsedSeconds用于控制支配粒子运动的物理引擎。gravity 属性默认与地球的重力加速度相匹配: -9.80665 (下落物体的速度每秒增加9.8米/秒)。在准备一个用于绘图的AGLKPointParticleEffect实例之前,必须要设置elapsedSeconds属性。粒子的当前位置是基于连续增加的elapsedSeconds属性计算出来的。
与其他‘ effects”一样,你必须要设置属性,调用“-prepareToDraw” 方法,然后“调用“-draw”方法。下面摘录自OpenGLES_Ch8_3ViewController 的‘ -update”方法的代码会在场景重绘前更新elapsedSeconds属性。
- (void)update
{
NSTimeInterval timeElapsed = self.timeSinceLastResume;
self.particleEffect.elapsedSeconds = timeElapsed;
}
接着,OpenGLES_Ch8_3ViewController 的“-glkView:drawInRect:" 方法会配置粒子效果的视点来匹配基本效果,并绘制粒子。
// Draw particles
self.particleEffect.transform.projectionMatrix =
self.baseEffect.transform.projectionMatrix;
self.particleEffect.transform.modelviewMatrix =
self.baseEffect.transform.modelviewMatrix;
[self.particleEffect prepareToDraw];
[self.particleEffect draw];
测试各种选项是感受粒子之间的相互作用力和速度的最好方式。OpenGLES_Ch8_3ViewController 已经实现了4个用于学习粒子配置选项的方法:“-spawnFireRing”、 “-spawnPulse”、“ -spawnSparkle”和“ -spawnBallCannon”。例如,“ -spawnBallCanon” 方法会使用下面的代码来创建粒子效果,这个效果中的粒子会在X轴方向上以任意的速度发射并进入场景。一旦启动这些粒子就会以普通的重力下落,并会在3.2秒寿命的最后半秒淡去。
// Turn on gravity
self.particleEffect.gravity = AGLKDefaultGravity;
float randomXVelocity = -0.5f + 1.0f *
(float)random() / (float)RAND_MAX;
[self.particleEffect
addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.9f)
velocity:GLKVector3Make(randomXVelocity, 1.0f, -1.0f)
force:GLKVector3Make(0.0f, 9.0f, 0.0f)
size:4.0f
lifeSpanSeconds:3.2f
fadeDurationSeconds:0.5f];
粒子的Shading Language程序
AGLKPointParticleEffect会使用自定义顶点和片元着色器来部分地展示ShadingLanguage程序的能力范围。这里的物理计算与计算机图形的基础计算一样,使用的都 是线性代数矢量和矩阵计算。例子OpenGLES_Ch8_3的所有粒子物理模拟都是在GPU中计算的。
注意 除了物理模拟外,有一个叫做“通用GPU”的趋势,这个趋势是利用GPU来 执行类似音频处理、雷达信号处理、蛋白质分子分析等非图形应用领域的算法。 GPU执行线性代数运算的速度通常要比通用CPU的速度快。
/////////////////////////////////////////////////////////////////
// Type used to define particle attributes
typedef struct
{
GLKVector3 emissionPosition;
GLKVector3 emissionVelocity;
GLKVector3 emissionForce;
GLKVector2 size;
GLKVector2 emissionTimeAndLife;
}
AGLKParticleAttributes;
顶点着色器是使用每个每顶点属性和每个统-变量值来执行物理运算的,这里的统一变量值是指用于重力、逝去时间、纹理取样标识符、model-view 矩阵与投影矩阵的结合的统一变量值。使用完全基于初始位置(粒子创建后就不再改变)、初始速度、力和;逝去时间的经典牛顿物理方程式来重新计算每个粒子的位置并存储在内置的glPosition变量中。每个粒子的尺寸都存储在另一个内置变量gl_PointSize 中。如下粗体代码显示的就是所需的赋值代码:
//
// ParticleShader.vsh
//
//
/////////////////////////////////////////////////////////////////
// VERTEX ATTRIBUTES
/////////////////////////////////////////////////////////////////
attribute vec3 a_emissionPosition;
attribute vec3 a_emissionVelocity;
attribute vec3 a_emissionForce;
attribute vec2 a_size;
attribute vec2 a_emissionAndDeathTimes;
/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4 u_mvpMatrix;
uniform sampler2D u_samplers2D[1];
uniform highp vec3 u_gravity;
uniform highp float u_elapsedSeconds;
/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying lowp float v_particleOpacity;
void main()
{
highp float elapsedTime = u_elapsedSeconds -
a_emissionAndDeathTimes.x;
// Mass is assumed to be 1.0, so acceleration = force (a = f/m)
// v = v0 + at : v is current velocity; v0 is initial velocity;
// a is acceleration; t is elapsed time
highp vec3 velocity = a_emissionVelocity +
((a_emissionForce + u_gravity) * elapsedTime);
// s = s0 + 0.5 * (v0 + v) * t : s is current position;
// s0 is initial position;
// v0 is initial velocity;
// v is current velocity;
// t is elapsed time
highp vec3 untransformedPosition = a_emissionPosition +
0.5 * (a_emissionVelocity + velocity) * elapsedTime;
gl_Position = u_mvpMatrix * vec4(untransformedPosition, 1.0);
gl_PointSize = a_size.x / gl_Position.w;
// if emission life > elapsed time then non-zero with maximum
// opacity of 1.0; otherwise 0.0. Fades over a_size.y seconds
v_particleOpacity = max(0.0, min(1.0,
(a_emissionAndDeathTimes.y - u_elapsedSeconds) /
max(a_size.y, 0.00001)));
}
每顶点的点大小会被除以像素颜色渲染缓存坐标中的粒子位置的w部分。w大体相当于粒子与平截体近面之间的距离。除以w模拟了粒子退向远处时的基于透视的收缩。顶点着色器还计算了每个粒子的半透明度并使用可变变量v_particleOpacity来传递这个值到片元着色器中。因此,在片元着色器中并没有很多工作要做。
//
// ParticleShader.fsh
//
//
/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4 u_mvpMatrix;
uniform sampler2D u_samplers2D[1];
uniform highp vec3 u_gravity;
uniform highp float u_elapsedSeconds;
/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying lowp float v_particleOpacity;
void main()
{
lowp vec4 textureColor = texture2D(u_samplers2D[0],
gl_PointCoord);
textureColor.a = textureColor.a * v_particleOpacity;
gl_FragColor = textureColor;
}
片元颜色是使用内置的texture2D()丽数求出的,同时使用了由统一变量usamplers2D指定的取样器和由另一个内置的ShadingLanguage魔法位所提供的纹理坐标。内置只读的gl PointCoord 变量是一个两元素矢量,每个元素都在0.0到1.0的范围之内,并且对应于当前正在被渲染的点精灵内的片元的{s, t} 纹理位置。把用于确定gl_FragColor 的textureColor的半透明度值乘以v_particleOpacity 来实现粒子的半透明。
AGLKPointParticleEffect的局限
所有使用AGLKPointParticleEffect实例绘制的粒子都有相同的纹理。为了渲染包含不同纹理的粒子的场景,必须要使用多个AGLKPointParticleEffect 实例。
当使用GPU计算顶点位置时会表现出一个明显的缺陷。当你测试例子OpenGLES_Ch8_3时,你可能会注意到一些类似某些粒子上的黑色轮廓的可视瑕疵。这些瑕疵是由粒子渲染的顺序导致的。每个粒子都会与像素颜色渲染缓存中的现存内容相融合。第一个渲染的粒子会与背景颜色相融合,但是接下来的粒子会与背景颜色以及前面渲染的粒子相融合。避免瑕疵的唯一方式是根据粒子与平截体近面的距离排序粒子,并按照从远 到近的顺序渲染粒子。
在OpenGL ES 2.0 Shading Language程序中是无法排序顶点的。顶点通常是按照它们提交给GPU的顺序处理的。因此,可能需要在AGLKPointParticleEffect类中用CPU 计算粒子的位置,从后向前排序粒子,并向GPU提交正确排序的当前粒子位置,不过这个还是留给读者作为练习吧。在下一节将会讲解的公告牌就是从后向前排序的,可以借鉴公告牌来修改AGLKPointParticleEffect类。甚至可以使用公告牌代替点精灵来实现粒子效果,但是点精灵的效率更高。第12章中的OpenGLES_Ch12_1例子演示了一个 基于公告牌的粒子效果实现方式。