一、开始
在之前的4篇文章里面,我们完成了一个桌上冰球的游戏,接下来我们要做一个喷射的烟花系统,我们要做出来的效果是这个样子的:
我们分两部分来完成,首先我们先做出喷射的效果,然后我们再去着重优化喷射的每个粒子的绘制。
二、喷射效果的实现
要完成喷射效果,我们的整体思路是这个样子的:
- 先实现不断往上移动的粒子系统
- 然后我们让往上的粒子随机的有一些角度
- 我们给这些粒子的移动向量加上一个重力加速度的衰减变量,让粒子的速度逐渐减慢最终像反方向移动
那现在就让我们一步一步去实现:
1.先实现不断往上移动的粒子系统
我们可以使用之前代码的一些工具类,所以Copy之前的代码,然后新建一个ParticlesRenderer.java
类实现Renderer.java
接口,在MainActivity.java
中移除监听事件,然后把这个Renderer设置给GLView。
我们思考一下,一个不断往上移动的粒子系统要如何去实现,要知道一个粒子如何移动,我们需要知道他的原始位置,粒子创建的时间,移动的方向向量,这样我们就可以根据当前时间去推算出一个粒子当前应该在的位置,现在我们就用代码去实现,新建一个particle_vertex_shader.glsl
文件,实现如下代码:
uniform mat4 u_Matrix;
uniform float u_Time;
attribute vec3 a_Position;
attribute vec3 a_Color;
attribute vec3 a_DirectionVector;
attribute float a_ParticleStartTime;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main() {
v_Color = a_Color;
v_ElapsedTime = u_Time - a_ParticleStartTime;
vec3 currentPosition = a_Position + (v_ElapsedTime * a_DirectionVector);
gl_Position = u_Matrix * vec4(currentPosition , 1.0);
gl_PointSize = 10.0;
}
然后我们新建一个particle_fragment_shader.glsl
文件实现如下代码:
precision mediump float;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main() {
gl_FragColor = vec4(v_Color / v_ElapsedTime , 1.0);
}
v_Color / v_ElapsedTime
是为了让粒子随着时间的流逝逐渐变的黯淡,现在Shader程序完成了,我们需要在Java里面封装一下,我们先在ShaderProgram.java
中添加如下声明:
protected static final String U_TIME = "u_Time";
protected static final String A_DIRECTION_VECTOR = "a_DirectionVector";
protected static final String A_PARTICLE_START_TIME = "a_ParticleStartTime";
然后新建一个ParticleShaderProgram.java
类,实现如下代码:
public class ParticleShaderProgram extends ShaderProgram{
private int mUMatrixLocation;
private int mUTimeLocation;
private int mAPositionLocation;
private int mAColorLocation;
private int mADirectionVectorLocation;
private int mAParticleStartTimeLocation;
public ParticleShaderProgram(Context context) {
super(context, R.raw.particle_vertex_shader, R.raw.particle_fragment_shader);
mUMatrixLocation = glGetUniformLocation(mProgram , U_MATRIX);
mUTimeLocation = glGetUniformLocation(mProgram , U_TIME);
mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
mAColorLocation = glGetAttribLocation(mProgram , A_COLOR);
mADirectionVectorLocation = glGetAttribLocation(mProgram , A_DIRECTION_VECTOR);
mAParticleStartTimeLocation = glGetAttribLocation(mProgram , A_PARTICLE_START_TIME);
}
public void setUniforms(float[] matrix , float elapsedTime){
glUniformMatrix4fv(mUMatrixLocation , 1 , false , matrix , 0);
glUniform1f(mUTimeLocation , elapsedTime);
}
public int getAPositionLocation(){
return mAPositionLocation;
}
public int getAColorLocation(){
return mAColorLocation;
}
public int getADirectionVectorLocation(){
return mADirectionVectorLocation;
}
public int getAParticleStartTimeLocation(){
return mAParticleStartTimeLocation;
}
}
然后我们在objects
包下新建ParticleSystem.java
类,去实现我们的粒子系统:
public class ParticleSystem {
private static final int POSITION_COMPONENT_COUNT = 3;
private static final int COLOR_COMPONENT_COUNT = 3;
private static final int VECTOR_COMPONENT_COUNT = 3;
private static final int PARTICLE_START_TIME_COMPONENT_COUNT = 1;
private static final int TOTAL_COMPONENT_COUNT = POSITION_COMPONENT_COUNT
+ COLOR_COMPONENT_COUNT
+ VECTOR_COMPONENT_COUNT
+ PARTICLE_START_TIME_COMPONENT_COUNT;
private static final int STRIDE = TOTAL_COMPONENT_COUNT * Constants.BYTE_PRE_FLOAT;
private final float[] mParticles;
private final VertexArray mVertexArray;
private final int mMaxParticleCount;
private int mCurrentParticleCount;
private int mNextParticle;
public ParticleSystem(int maxParticleCount){
mParticles = new float[maxParticleCount * TOTAL_COMPONENT_COUNT];
mVertexArray = new VertexArray(mParticles);
mMaxParticleCount = maxParticleCount;
}
public void addParticle(Geometry.Point position , int color , Geometry.Vector directionVection ,
float startTime){
final int particleOffset = mNextParticle * TOTAL_COMPONENT_COUNT;
int currentOffset = particleOffset;
mNextParticle++;
if (mCurrentParticleCount < mMaxParticleCount){
mCurrentParticleCount++;
}
if (mNextParticle == mMaxParticleCount){
mNextParticle = 0;
}
mParticles[currentOffset++] = position.x;
mParticles[currentOffset++] = position.y;
mParticles[currentOffset++] = position.z;
mParticles[currentOffset++] = Color.red(color) / 255f;
mParticles[currentOffset++] = Color.green(color) / 255f;
mParticles[currentOffset++] = Color.blue(color) / 255f;
mParticles[currentOffset++] = directionVection.x;
mParticles[currentOffset++] = directionVection.y;
mParticles[currentOffset++] = directionVection.z;
mParticles[currentOffset++] = startTime;
//更新在native的数据
mVertexArray.updateBuffer(mParticles , particleOffset , TOTAL_COMPONENT_COUNT);
}
public void bindData(ParticleShaderProgram particleShaderProgram){
mVertexArray.setVertexAttribPointer(0 ,
particleShaderProgram.getAPositionLocation() , POSITION_COMPONENT_COUNT , STRIDE);
mVertexArray.setVertexAttribPointer(POSITION_COMPONENT_COUNT ,
particleShaderProgram.getAColorLocation() , COLOR_COMPONENT_COUNT , STRIDE);
mVertexArray.setVertexAttribPointer(POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT,
particleShaderProgram.getADirectionVectorLocation() , VECTOR_COMPONENT_COUNT , STRIDE);
mVertexArray.setVertexAttribPointer(POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT + VECTOR_COMPONENT_COUNT,
particleShaderProgram.getAParticleStartTimeLocation() , PARTICLE_START_TIME_COMPONENT_COUNT , STRIDE);
}
public void draw(){
glDrawArrays(GL_POINTS, 0, mCurrentParticleCount);
}
}
粒子系统完成了,接下来我们还需要一个喷泉去使用我们的粒子系统,在objects
包下再新建一个类ParticleShooter.java
类,实现如下代码:
public class ParticleShooter {
private final Geometry.Point mPosition;
private final Geometry.Vector mDirectionVector;
private final int mColor;
public ParticleShooter(Geometry.Point position , Geometry.Vector directionVector , int color){
mPosition = position;
mDirectionVector = directionVector;
mColor = color;
}
public void addParticles(ParticleSystem particleSystem , float currentTime , int count){
for (int i = 0; i < count; i++) {
particleSystem.addParticle(mPosition , mColor , mDirectionVector , currentTime);
}
}
}
最后去修改我们的Renderer就实现了第一步了:
public class ParticlesRenderer implements GLSurfaceView.Renderer{
private Context mContext;
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
private final float[] mViewProjectionMatrix = new float[16];
private ParticleShaderProgram mParticleShaderProgram;
private ParticleSystem mParticleSystem;
private ParticleShooter mRedShooter;
private ParticleShooter mGreenShooter;
private ParticleShooter mBlueShooter;
private float mGlobleStartTime;
public ParticlesRenderer(Context context){
mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(0f ,0f , 0f, 0f);
mParticleShaderProgram = new ParticleShaderProgram(mContext);
mParticleSystem = new ParticleSystem(10000);
mGlobleStartTime = System.nanoTime();
final Geometry.Vector particleDirection = new Geometry.Vector(0f, 0.5f, 0f);
mRedShooter = new ParticleShooter(new Geometry.Point(-1 , 0 , 0) ,
particleDirection , Color.rgb(255, 50, 5));
mGreenShooter = new ParticleShooter(new Geometry.Point(0 , 0 , 0) ,
particleDirection , Color.rgb(25, 255, 25));
mBlueShooter = new ParticleShooter(new Geometry.Point(1 , 0 , 0) ,
particleDirection , Color.rgb(5, 50, 255));
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
glViewport(0 , 0 , width , height);
MatrixHelper.perspectiveM(mProjectionMatrix, 45, (float) width
/ (float) height, 1f, 10f);
setIdentityM(mViewMatrix, 0);
translateM(mViewMatrix, 0, 0f, -1.5f, -5f);
multiplyMM(mViewProjectionMatrix, 0, mProjectionMatrix, 0,
mViewMatrix, 0);
}
@Override
public void onDrawFrame(GL10 gl10) {
glClear(GL_COLOR_BUFFER_BIT);
float currentTime = (System.nanoTime() - mGlobleStartTime) / 1000000000f;
mRedShooter.addParticles(mParticleSystem , currentTime , 5);
mGreenShooter.addParticles(mParticleSystem , currentTime , 5);
mBlueShooter.addParticles(mParticleSystem , currentTime , 5);
mParticleShaderProgram.useProgram();
mParticleShaderProgram.setUniforms(mViewProjectionMatrix , currentTime);
mParticleSystem.bindData(mParticleShaderProgram);
mParticleSystem.draw();
}
}
运行一下,你就能看到下面的效果了:
2.加上随机角度
我们知道粒子的移动是方向向量决定的,所以我们的变动要加到方向向量上,我们也需要给我的随机变动加上一个范围,主要是改动ParticleShooter.java
类:
public class ParticleShooter {
private final Geometry.Point mPosition;
private final Geometry.Vector mDirectionVector;
private final int mColor;
private final float mAngleVariance;
private final float mSpeedVariance;
private final Random mRandom = new Random();
private float[] mRotationMatrix = new float[16];
private float[] mDirection = new float[4];
private float[] mResultVector = new float[4];
public ParticleShooter(Geometry.Point position , Geometry.Vector directionVector , int color ,
float angleVariance , float speedVariance){
mPosition = position;
mDirectionVector = directionVector;
mColor = color;
mAngleVariance = angleVariance;
mSpeedVariance = speedVariance;
mDirection[0] = directionVector.x;
mDirection[1] = directionVector.y;
mDirection[2] = directionVector.z;
}
public void addParticles(ParticleSystem particleSystem , float currentTime , int count){
for (int i = 0; i < count; i++) {
setRotateEulerM(mRotationMatrix, 0,
(mRandom.nextFloat() - 0.5f) * mAngleVariance,
(mRandom.nextFloat() - 0.5f) * mAngleVariance,
(mRandom.nextFloat() - 0.5f) * mAngleVariance);
multiplyMV(
mResultVector, 0,
mRotationMatrix, 0,
mDirection, 0);
float speedAdjustment = 1f + mRandom.nextFloat() * mSpeedVariance;
Geometry.Vector thisVector = new Geometry.Vector(
mResultVector[0] * speedAdjustment,
mResultVector[1] * speedAdjustment,
mResultVector[2] * speedAdjustment);
particleSystem.addParticle(mPosition , mColor , thisVector , currentTime);
}
}
}
然后修改Renderer里面的代码:
final float angleVarianceInDegrees = 5f;
final float speedVariance = 1f;
mRedShooter = new ParticleShooter(new Geometry.Point(-1 , 0 , 0) ,
particleDirection , Color.rgb(255, 50, 5) , angleVarianceInDegrees , speedVariance);
mGreenShooter = new ParticleShooter(new Geometry.Point(0 , 0 , 0) ,
particleDirection , Color.rgb(25, 255, 25) , angleVarianceInDegrees , speedVariance);
mBlueShooter = new ParticleShooter(new Geometry.Point(1 , 0 , 0) ,
particleDirection , Color.rgb(5, 50, 255) , angleVarianceInDegrees , speedVariance);
再运行一遍,你就能看到下面的效果了:
3.加上衰减
我们只需要在shader程序里面算出来一个合适的值,然后一直衰减点的y值就可以了,在particle_vertex_shader.glsl
文件中加上如下代码:
float gravityFactor = v_ElapsedTime * v_ElapsedTime / 8.0;
currentPosition.y -= gravityFactor;
然后运行一遍就能看到如下效果了:
三、优化粒子的显示
不知道大家有没有感觉我们上面的烟花有点怪怪的,就是在烟花多得地方应该是比较亮的,但是我们上面的没有那种效果,那我们要如何去实现那种效果呢?其实很简单,只需要在onSurfaceCreated()
方法中调用如下两个方法就可以了:
glEnable(GL_BLEND);
glBlendFunc(GL_ONE , GL_ONE);
第一行代码的意思就是打开Blend效果,下面的方法是一个输出效果的方程,原文是这样解释的:
output = (source factor * source fragment) + (destination factor * destination fragment)
In OpenGL, blending works by blending the result of the fragment shader with the color that’s already there in the frame buffer.
个人理解就是把原来在这里的颜色和将要绘制在这里的颜色按一定的比例混合显示,理解可能有误,大家可以自己查资料理解。
加上这两行代码之后的效果是这样的:
接下来我们就要优化下粒子的形状了,现在的正方形是不是感觉不舒服,我们先修改成圆形吧!这个时候我们需要用到一个叫做gl_PointCoord的东西,我们把particle_fragment_shader.glsl
修改如下:
precision mediump float;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main() {
float xDistance = 0.5 - gl_PointCoord.x;
float yDistance = 0.5 - gl_PointCoord.y;
float distanceFromCenter =
sqrt(xDistance * xDistance + yDistance * yDistance);
if (distanceFromCenter > 0.5) {
discard;
} else {
gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0);
}
}
gl_PointCoord的解释原文是这样说的:
For each point, when the fragment shader is run, we’ll get a two-dimensional gl_PointCoord coordinate with each component ranging from 0 to 1 on each axis, depending on which fragment in the point is currently being rendered.
代码里面的意思就是以(0.5,0.5)为圆心,0.5为半径之内的圆的fragment是绘制的,圆之外不绘制,这样就绘制出一个圆点了。运行下看看吧!
最后我们用一张图片去绘制我们的点,如果大家忘了如何绘制图片可以看看前面的文章。我们先把我们的particle_fragment_shader.glsl
文件修改成下面的样子:
precision mediump float;
varying vec3 v_Color;
varying float v_ElapsedTime;
uniform sampler2D u_TextureUnit;
void main() {
gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0)
* texture2D(u_TextureUnit, gl_PointCoord);
}
意思就是用原来的颜色去画现在的图。
然后我们需要去修改ParticleShaderProgram.java
文件,我们需要先声明我们要用到的位置:
private int mUTextureUnitLocation;
然后再初始化的方法中去赋值:
mUTextureUnitLocation = glGetUniformLocation(mProgram , U_TEXTURE_UNIT);
最后需要修改setUniforms()
方法,添加一个参数int textureId
然后添加如下代码:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D , textureId);
glUniform1i(mUTextureUnitLocation , 0);
最后我们在Renderer中加载Texture并给Program设置使用:
int textureId = TextureHelper.loadTexture(mContext , R.drawable.particle_texture);
mParticleShaderProgram.setUniforms(mViewProjectionMatrix , currentTime , textureId);
最后运行看看!是不是好看很多了。
这一部分主要是对之前知识的回顾,大家可以好好整理下!
项目代码在这里:https://github.com/KevinKmoo/Particles
能力有限,自己读书的学习所得,有错误请指导,轻虐!
转载请注明出处。----by kmoo