目录
- 粒子和粒子系统
- 实践:喷泉效果
- 遇到的问题
- 资料
- 收获
通过该篇的实践实现如下效果
一、什么是粒子和粒子系统
如何定义粒子?
一个粒子有位置信息(x,y,z)、运动方向、颜色、生命值(开始和结束的时间)等属性
什么粒子系统?
通过渲染绘制出大量 位置、形状、方向、颜色不同的物体(粒子),从而形成大量粒子运动的效果。
明确了概念我们来逐步实现烟花效果
二、实践:喷泉效果
面对一个比较大或者没有尝试过的项目或内容,害怕、怯懦时常出现,这时要理清目标,抓住主线,运用结构化思维,拆解流程,然后逐步实现每个环节,解决每个环节以中的问题,这也是打怪升级的过程,下面一起来享受这个过程吧。
目标:了解和运用粒子系统,实现比较炫酷的效果,从粒子喷泉着手。
流程拆解
- 梳理粒子特性(坐标、颜色、运动矢量、开始时间)以及 重力和阻力的影响、持续时间
- 粒子发生器如何发射粒子(反射点、方向、数量)
- 编写着色器glsl代码
- 编写Program
- 编写GLSurfaceView的Render进行着色器加载、编译、链接、使用、渲染
下面开始我们的具体实践
2.1. 定义粒子系统对象
定义粒子的属性以及提供添加粒子的方法
public class ParticleSystem {
//位置 xyz
private final int POSITION_COMPONENT_COUNT = 3;
//颜色 rgb
private final int COLOR_COMPONENT_COUNT = 3;
//运动矢量 xyz
private final int VECTOR_COMPONENT_COUNT = 3;
//开始时间
private final int PARTICLE_START_TIME_COMPONENT_COUNT = 1;
private final int TOTAL_COMPONENT_COUNT = POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT
+ VECTOR_COMPONENT_COUNT + PARTICLE_START_TIME_COMPONENT_COUNT;
//步长
private final int STRIDE = TOTAL_COMPONENT_COUNT * VertexArray.BYTES_PER_FLOAT;
//粒子游标
private int nextParticle;
//粒子计数
private int curParticleCount;
//粒子数组
private final float[] particles;
//最大粒子数量
private final int maxParticleCount;
//VBO
private final VertexArray vertexArray;
public ParticleSystem(int maxParticleCount) {
this.particles = new float[maxParticleCount * TOTAL_COMPONENT_COUNT];
this.maxParticleCount = maxParticleCount;
this.vertexArray = new VertexArray(particles);
}
/**
* 添加粒子到FloatBuffer
*
* @param position 位置
* @param color 颜色
* @param direction 运动矢量
* @param particStartTime 开始时间
*/
public void addParticle(Geometry.Point position, int color, Geometry.Vector direction, float particStartTime) {
final int particleOffset = nextParticle * TOTAL_COMPONENT_COUNT;
int currentOffset = particleOffset;
nextParticle++;
if (curParticleCount < maxParticleCount) {
curParticleCount++;
}
//重复使用,避免内存过大
if (nextParticle == maxParticleCount) {
nextParticle = 0;
}
//填充 位置坐标 xyz
particles[currentOffset++] = position.x;
particles[currentOffset++] = position.y;
particles[currentOffset++] = position.z;
//填充 颜色 rgb
particles[currentOffset++] = Color.red(color) / 255;
particles[currentOffset++] = Color.green(color) / 255;
particles[currentOffset++] = Color.blue(color) / 255;
//填充 运动矢量
particles[currentOffset++] = direction.x;
particles[currentOffset++] = direction.y;
particles[currentOffset++] = direction.z;
//填充粒子开始时间
particles[currentOffset++] = particStartTime;
//把新增的粒子添加到顶点数组FloatBuffer中
vertexArray.updateBuffer(particles, particleOffset, TOTAL_COMPONENT_COUNT);
}
}
2.2. 定义粒子发射器对象
public class ParticleShooter {
//发射粒子的位置
private final Geometry.Point position;
//发射粒子的颜色
private final int color;
//发射粒子的方法
private final Geometry.Vector direction;
public ParticleShooter(Geometry.Point position, int color, Geometry.Vector direction) {
this.position = position;
this.color = color;
this.direction = direction;
}
/**
* 调用粒子系统对象添加粒子
*
* @param particleSystem
* @param currentTime
*/
public void addParticles(ParticleSystem particleSystem, float currentTime) {
particleSystem.addParticle(position, color, direction, currentTime);
}
}
2.3. 编写顶点和片元着色器
uniform float u_Time;
attribute vec3 a_Position;
attribute vec3 a_Color;
attribute vec3 a_Direction;
attribute float a_PatricleStartTime;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main(){
v_Color = a_Color;
//粒子已经持续时间 当前时间-开始时间
v_ElapsedTime = u_Time - a_PatricleStartTime;
//重力或者阻力因子,随着时间的推移越来越大
float gravityFactor = v_ElapsedTime * v_ElapsedTime / 9.8;
//当前的运动到的位置 粒子起始位置+(运动矢量*持续时间)
vec3 curPossition = a_Position + (a_Direction * v_ElapsedTime);
//减去重力或阻力的影响
curPossition.y -= gravityFactor;
//把当前位置通过内置变量传给片元着色器
gl_Position = vec4(curPossition,1.0);
gl_PointSize = 10.0;
}
precision mediump float;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main(){
//粒子颜色随着颜色的推移变化
gl_FragColor = vec4(v_Color/v_ElapsedTime, 1.0);
}
2.4. 编写Program封装着色器
public class ParticleShaderProgram {
private final String U_TIME ="u_Time";
private final String A_POSITION="a_Position";
private final String A_COLOR="a_Color";
private final String A_DIRECTION="a_Direction";
private final String A_PATRICLE_START_TIME="a_PatricleStartTime";
private final int program;
private final int uTimeLocation;
private final int aPositionLocation;
private final int aColorLocation;
private final int aDirectionLocation;
private final int aPatricleStartTimeLocation;
public ParticleShaderProgram(Context context) {
//生成program
String vertexShaderCoder = ShaderHelper.loadAsset(context.getResources(), "particle_vertex_shader.glsl");
String fragmentShaderCoder = ShaderHelper.loadAsset(context.getResources(), "particle_fragment_shader.glsl");
this.program = ShaderHelper.loadProgram(vertexShaderCoder,fragmentShaderCoder);
//获取uniform 和attribute的location
uTimeLocation = GLES20.glGetUniformLocation(program,U_TIME);
aPositionLocation = GLES20.glGetAttribLocation(program,A_POSITION);
aColorLocation = GLES20.glGetAttribLocation(program,A_COLOR);
aDirectionLocation = GLES20.glGetAttribLocation(program,A_DIRECTION);
aPatricleStartTimeLocation = GLES20.glGetAttribLocation(program,A_PATRICLE_START_TIME);
}
/**
* 设置 始终如一的Uniform变量
* @param curTime
*/
public void setUniforms(float curTime){
GLES20.glUniform1f(uTimeLocation,curTime);
}
public int getProgram() {
return program;
}
public int getaPositionLocation() {
return aPositionLocation;
}
public int getaColorLocation() {
return aColorLocation;
}
public int getaDirectionLocation() {
return aDirectionLocation;
}
public int getaPatricleStartTimeLocation() {
return aPatricleStartTimeLocation;
}
public void useProgram(){
GLES20.glUseProgram(program);
}
}
2.5. 编写Render加载、渲染
public class ParticlesRender implements GLSurfaceView.Renderer {
private final Context mContext;
private ParticleShaderProgram mProgram;
private ParticleSystem mParticleSystem;
private long mSystemStartTimeNS;
private ParticleShooter mParticleShooter;
public ParticlesRender(Context context) {
this.mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0f,0f,0f,0f);
mProgram = new ParticleShaderProgram(mContext);
//定义粒子系统 最大包含1w个粒子,超过最大之后复用最前面的
mParticleSystem = new ParticleSystem(10000);
//粒子系统开始时间
mSystemStartTimeNS = System.nanoTime();
//定义粒子发射器
mParticleShooter = new ParticleShooter(new Geometry.Point(0f, -0.9f, 0f),
Color.rgb(255, 50, 5),
new Geometry.Vector(0f, 0.3f, 0f));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//当前(相对)时间 单位秒
float curTime = (System.nanoTime() - mSystemStartTimeNS)/1000000000f;
//粒子发生器添加粒子
mParticleShooter.addParticles(mParticleSystem,curTime);
//使用Program
mProgram.useProgram();
//设置Uniform变量
mProgram.setUniforms(curTime);
//设置attribute变量
mParticleSystem.bindData(mProgram);
//开始绘制粒子
mParticleSystem.draw();
}
}
ParticleSystem添加bindData和draw方法,如下
public class ParticleSystem {
...
public void bindData(ParticleShaderProgram program) {
int dataOffset = 0;
vertexArray.setVertexAttributePointer(dataOffset,
program.getaPositionLocation(),
POSITION_COMPONENT_COUNT, STRIDE);
dataOffset +=POSITION_COMPONENT_COUNT;
vertexArray.setVertexAttributePointer(dataOffset,
program.getaColorLocation(),
COLOR_COMPONENT_COUNT, STRIDE);
dataOffset +=COLOR_COMPONENT_COUNT;
vertexArray.setVertexAttributePointer(dataOffset,
program.getaDirectionLocation(),
VECTOR_COMPONENT_COUNT, STRIDE);
dataOffset +=VECTOR_COMPONENT_COUNT;
vertexArray.setVertexAttributePointer(dataOffset,
program.getaPatricleStartTimeLocation(),
PARTICLE_START_TIME_COMPONENT_COUNT, STRIDE);
}
public void draw() {
GLES20.glDrawArrays(GLES20.GL_POINTS,0,curParticleCount);
}
}
VertexArray添加方法 setVertexAttributePointer 用于给顶点着色器的Attribute属性的变量赋值
public class VertexArray {
...
public void setVertexAttributePointer(int dataOffset, int location, int count, int stride) {
floatBuffer.position(dataOffset);
GLES20.glVertexAttribPointer(location,count,GLES20.GL_FLOAT,false,stride,floatBuffer);
GLES20.glEnableVertexAttribArray(location);
floatBuffer.position(0);
}
}
2.6. 在GLSurfaceView中使用Render
public class ParticleActivity extends Activity{
private GLSurfaceView glSurfaceView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_particle_layout);
glSurfaceView = findViewById(R.id.glSurfaceView);
glSurfaceView.setEGLContextClientVersion(2);
glSurfaceView.setRenderer(new ParticlesRender(this));
}
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
}
@Override
protected void onPause() {
super.onPause();
glSurfaceView.onPause();
}
}
效果如下
是不是和预期的效果差别很大,别着急,只要在正确的路上,遇到问题就分析解决,最重要的是坚持前行。
三、问题
可以看到有如下几个问题
- 粒子发射没有方向变化
- 新发射的和下落的重叠
下面我们来一一解决。
问题1. 随机改变发射粒子的方向
粒子的发射方向是有发射器决定的,我们现在是一个固定的向上发送 Vector(0f, 0.3f, 0f)。
mParticleShooter = new ParticleShooter(new Geometry.Point(0f, -0.9f, 0f),
Color.rgb(255, 50, 5),
new Geometry.Vector(0f, 0.3f, 0f));
public class ParticleShooter {
...
public void addParticles(ParticleSystem particleSystem, float currentTime) {
particleSystem.addParticle(position, color, direction, currentTime);
}
...
}
如果想改变这个为随机方向,我们需要修改这个这个direction
我们通过随机数以及矩阵变换来实现方向的随机
public class ParticleShooter {
...
private float[] rotationMatrix = new float[16];
private final Random random = new Random();
final float angleVarianceInDegrees = 20f;
public void addParticles(ParticleSystem particleSystem, float currentTime) {
Matrix.setRotateEulerM(rotationMatrix, 0,
(random.nextFloat() -0.5f) * angleVarianceInDegrees,
(random.nextFloat()-0.5f) * angleVarianceInDegrees,
(random.nextFloat()-0.5f) * angleVarianceInDegrees);
float[] tmpDirectionFloat = new float[4];
Matrix.multiplyMV(tmpDirectionFloat,0,
rotationMatrix,0,
new float[]{direction.x,direction.y,direction.z,1f},0);
Geometry.Vector newDirection = new Geometry.Vector(tmpDirectionFloat[0], tmpDirectionFloat[1], tmpDirectionFloat[2]);
particleSystem.addParticle(position, color, newDirection, currentTime);
}
...
}
效果如下:
问题2. 重叠覆盖
通过修改粒子的发射方向,看起来重叠效果也没有了,真的是这样吗,还是不容易观察到了?
为了验证问题我们同一时刻多加些粒子,即同时发送多批粒子粒子系统。
修改也比较简单,加个for循环即可。
public void addParticles(ParticleSystem particleSystem, float currentTime, int count) {
for (int i = 0; i < count; i++) {
Matrix.setRotateEulerM(rotationMatrix, 0,
(random.nextFloat() - 0.5f) * angleVarianceInDegrees,
(random.nextFloat() - 0.5f) * angleVarianceInDegrees,
(random.nextFloat() - 0.5f) * angleVarianceInDegrees);
float[] tmpDirectionFloat = new float[4];
Matrix.multiplyMV(tmpDirectionFloat, 0,
rotationMatrix, 0,
new float[]{direction.x, direction.y, direction.z, 1f}, 0);
Geometry.Vector newDirection = new Geometry.Vector(tmpDirectionFloat[0], tmpDirectionFloat[1], tmpDirectionFloat[2]);
particleSystem.addParticle(position, color, newDirection, currentTime);
}
}
我们给这个count传20,效果如下
可以看到重叠覆盖又出现了,为什么会出现这种情况?
同一时刻,同一位置,既有下落的粒子,又有上升的粒子,如果下落的粒子后渲染,就会覆盖上升的粒子。
那么该如何解决呐?
OpenGL提供了累加混合技术 GL_BLEND_,公式如下
输出 = (源因子 * 源片段)+(目标因子 * 目标片段)
具体实现为,在Render的onSurfaceCrate中设置
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0f,0f,0f,0f);
GLES20.glEnable(GLES20.GL_BLEND);
//采用累加混合 输出= (GLES20.GL_ONE * 源片段)+(GLES20.GL_ONE * 目标片段)
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
....
}
效果如下:
可以看到没有再出现重叠覆盖的情况了。
还有一个问题是,每个粒子都是一个点,大小通过gl_PointSize设置为10,看到的是正方形粒子。能不能修改为圆形或者指定样式呐?_
问题3. 把点修改为纹理图片
下面我们就通过纹理图片来把每个点绘制为一个点精灵
关于纹理的使用如果不熟悉,请先阅读[音视频开发之旅(12) OpenGL ES之纹理]
首先 修改片元着色器,添加2D纹理
precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main(){
//粒子颜色随着颜色的推移变化
//gl_FragColor = vec4(v_Color/v_ElapsedTime, 1.0);
//改为:通过内置函数texture2D和原来的fragcolor相乘
gl_FragColor = vec4(v_Color/v_ElapsedTime, 1.0) * texture2D(u_TextureUnit, gl_PointCoord);
}
然后修改Program,解析sample2D以及赋值
public class ParticleShaderProgram {
...
private final String U_TEXTURE_UNIT ="u_TextureUnit";
private final int uTextureUnit;
public ParticleShaderProgram(Context context) {
...
uTextureUnit = GLES20.glGetUniformLocation(program,U_TEXTURE_UNIT);
...
}
public void setUniforms(float curTime, int textureId){
GLES20.glUniform1f(uTimeLocation,curTime);
//激活纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//绑定纹理id
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);
//赋值
GLES20.glUniform1i(uTextureUnit,0);
}
....
}
最后在Render中定义textureId以及在onDrawFrame中传值
public class ParticlesRender {
...
private int mTextureId;
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
...
mTextureId = TextureHelper.loadTexture(mContext, R.drawable.particle_texture);
...
@Override
public void onDrawFrame(GL10 gl) {
...
//设置Uniform变量
mProgram.setUniforms(curTime,mTextureId);
...
}
}
...
}
效果如下:
最后我们修改顶点着色器中定义的点的大小以及运动矢量
gl_PointSize = 25.0;
//定义粒子发射器
mParticleShooter = new ParticleShooter(new Geometry.Point(0f, -0.9f, 0f),
Color.rgb(255, 50, 5),
new Geometry.Vector(0f, 0.8f, 0f));
就是开篇展示的效果
这篇就到这里了,通过实践,一步步实现最终的喷泉效果。
下一篇我们继续来学习实践粒子系统,实现烟花空中爆炸的效果。
四、资料
《OpenGL ES 3.0 编程指南》
《OpenGL ES应用开发实践指南》
[粒子系统--烟花 [OpenGL-Transformfeedback]]
[Android制作粒子爆炸特效]
[OpenGL进阶(六)-粒子系统]
[【OpenGL】Shader实例分析(七)- 雪花飘落效果]
五、收获
- 了解粒子属性和粒子系统
- 通过任务拆解逐步实现喷泉效果
- 解决遇到的问题(重力、发射方向、重叠覆盖、点精灵)
感谢你的阅读。
下一篇我们继续来学习实践粒子系统,实现烟花空中爆炸的效果。欢迎关注公众号“音视频开发之旅”,一起学习成长。
欢迎交流