我们本节开始分析《OpenGL ES应用开发实践指南 Android卷》书中第10章中的粒子系统的实现原理,搞清楚其中的代码逻辑,代码下载请点击:Opengl ES Source Code,该Git库中的particles Module就是我们本节要分析的目标,先看下本节最终实现的结果。
最终运行在真机上的效果非常炫,三个红绿蓝粒子系统不断的发射新的粒子,所有粒子由于重力上升到一定高度后开始下降,而且颜色发亮,非常漂亮,下面让我们看一下它到底是怎么实现的。
首先,我们来看一下ParticlesActivity类中的代码,非常简单,判断当前设备是否支持Opengl ES2.0,如果支持,则调用glSurfaceView.setEGLContextClientVersion(2)将版本设置为2.0,然后构造一个Render渲染类,调用setRenderer设置为glSurfaceView的渲染器。
接下来看一下ParticlesRenderer类,首先必须实现android.opengl.GLSurfaceView.Renderer,重写父类定义的onSurfaceCreated、onSurfaceChanged、onDrawFrame三个方法,然后在每个方法中添加实现逻辑,三个方法的回调意图也非常清晰,onSurfaceCreated就是当GLSurfaceView创建完成后的回调,到这里显示系统分配给当前View的Surface才有效,才可以执行绘图工作;onSurfaceChanged表示Surface发生变化时的回调,最明显的就是当前Activity退出再进入,Surface可见性变化,就会回调该方法;onDrawFrame表示需要绘制一帧,这里就和Vsync垂直同步信号相关了,显示器一般是60FPS帧率,大家可以看下我之前Vsync相关的博客。
private final Context context;
private final float[] projectionMatrix = new float[16];
private final float[] viewMatrix = new float[16];
private final float[] viewProjectionMatrix = new float[16];
/*
// Maximum saturation and value.
private final float[] hsv = {0f, 1f, 1f};*/
private ParticleShaderProgram particleProgram;
private ParticleSystem particleSystem;
private ParticleShooter redParticleShooter;
private ParticleShooter greenParticleShooter;
private ParticleShooter blueParticleShooter;
/*private ParticleFireworksExplosion particleFireworksExplosion;
private Random random;*/
private long globalStartTime;
private int texture;
以上是ParticlesRenderer类中定义的所有成员变量,首先是三个float数组,长度全部为16,这三个数组是进行Matrix矩阵运算需要的,首先调用MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width / (float) height, 1f, 10f)获取到一个透视投影矩阵,45表示视角为45度,(float) width / (float) height表示缩放率,这里请一定注意,不能直接用width和height相除,因为我们如果是竖屏的话,宽度比高度小,结果得到的是0,不会有任何绘制,1和10表示z轴的可视范围从-1到-10,大家看下MatrixHelper中该方法的实现就会明白了;其次调用setIdentityM(viewMatrix, 0)得到一个4*4单位矩阵,如果大家不明白单位矩阵的话,请百度搜索搞明白。为什么是4*4呢?因为矩阵中一般运算都是四个分量,比如顶点位置属性(x、y、z、w),w分量其实非常有用,书中有很详细的解释;接着调用translateM(viewMatrix, 0, 0f, -1.5f, -5f)将viewMatrix单位矩阵进行平移,-1.5f表示Y方向1.5个单位,负值也就是向下,-5f表示Z方向5个单位,负值表示离视点(屏幕)越远,这里需要注意,因为我们创建透视投影矩阵时,指定的Z轴最近距离是一个单位,最远距离是10个单位,只有在这个范围内的顶点才会被绘制,如果Z轴的值超出这个范围,也不会有任何东西被绘制,大家可以试一下;最后调用multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0)将两个矩阵相乘,第一个参数viewProjectionMatrix是存储输出结果的,看到这里,大家就明白这三个float数组的作用了吧,其实就是得到两个矩阵,然后执行乘法运算,把结果存储在第三个矩阵中。
接下来是ParticleShaderProgram particleProgram是自定义的一个着色器程序,ParticleSystem particleSystem是自定义的粒子系统,redParticleShooter、greenParticleShooter、blueParticleShooter是三个粒子发射器,等下分析完当前类,我们逐个分析这三个自定义的类。globalStartTime记录一个时间戳,texture是纹理ID。
构造方法很简单,我们就不说了,继续看onSurfaceCreated,代码如下:
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
// Enable additive blending
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
particleProgram = new ParticleShaderProgram(context);
particleSystem = new ParticleSystem(10000);
globalStartTime = System.nanoTime();
final Vector particleDirection = new Vector(0f, 0.5f, 0f);
final float angleVarianceInDegrees = 5f;
final float speedVariance = 1f;
/*
redParticleShooter = new ParticleShooter(
new Point(-1f, 0f, 0f),
particleDirection,
Color.rgb(255, 50, 5));
greenParticleShooter = new ParticleShooter(
new Point(0f, 0f, 0f),
particleDirection,
Color.rgb(25, 255, 25));
blueParticleShooter = new ParticleShooter(
new Point(1f, 0f, 0f),
particleDirection,
Color.rgb(5, 50, 255));
*/
redParticleShooter = new ParticleShooter(
new Point(-1f, 0f, 0f),
particleDirection,
Color.rgb(255, 50, 5),
angleVarianceInDegrees,
speedVariance);
greenParticleShooter = new ParticleShooter(
new Point(0f, 0f, 0f),
particleDirection,
Color.rgb(25, 255, 25),
angleVarianceInDegrees,
speedVariance);
blueParticleShooter = new ParticleShooter(
new Point(1f, 0f, 0f),
particleDirection,
Color.rgb(5, 50, 255),
angleVarianceInDegrees,
speedVariance);
/*
particleFireworksExplosion = new ParticleFireworksExplosion();
random = new Random(); */
texture = TextureHelper.loadTexture(context, R.drawable.particle_texture);
}
glClearColor的作用就是清屏,传入的参数就是RGBA分量,四个0表示什么也没有,就是黑色,如果要用白色清屏,那RGB肯定都要设置成1了;glEnable(GL_BLEND)表示启用Blend混合,Blend混合是将源色和目标色以某种方式混合生成特效的技术。混合常用来绘制透明或半透明的物体。在混合中起关键作用的α值实际上是将源色和目标色按给定比率进行混合,以达到不同程度的透明。α值为0则完全透明,α值为1则完全不透明。混合操作只能在RGBA模式下进行,颜色索引模式下无法指定α值。物体的绘制顺序会影响到OpenGL的混合处理,说的通俗点,比如我们下落的粒子和刚生成的粒子重叠,两个粒子的颜色就会混合在一起,大家可以关闭混合就会看到明显的效果;glBlendFunc(GL_ONE, GL_ONE)就是叠加混合,Opengl还提供了其他很多的混合算法,大家可以自己去研究。接着创建着色器程序和粒子系统,粒子系统的参数10000表示最大粒子容量为10000,如果超出的话,就会从头存储,后面看到这里的代码就会明白,然后给globalStartTime赋值,也就是记录当前时间,接下来创建Vector向量,X和Z轴的值都为0,只有Y轴为正,该值会影响到粒子往上飞。然后定义角度为5度,速度为1,接着构造三个粒子发射器,第一个参数表示粒子发射器的位置,当前坐标系统,(0,0,0)为屏幕正中心,X轴正向向右,Y轴正向向上,Z轴正向向外(朝着我们的眼睛),所以三个粒子发射器的定位就是左中(-1,0,0)、中心(0,0,0)、右中(1,0,0),大家看下实际的效果粒子的Y轴位置不在中间,这是因为使用透视投影矩阵和平移的结果导致的,第二个参数方向向量都一样,然后是RGB颜色,接着是角度和速度;该方法最后一句texture = TextureHelper.loadTexture(context, R.drawable.particle_texture)表示加载纹理,我们跟进去看一下该方法的实现,源码如下:
public static int loadTexture(Context context, int resourceId) {
final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not generate a new OpenGL texture object.");
}
return 0;
}
final BitmapFactory.Options op