Android OpenGL粒子
首先申明下,本文为笔者学习《OpenGL ES应用开发实践指南》的笔记,并加入笔者自己的理解和归纳总结。
粒子就是被画作一组点的简单物体。
1、着色器文件
(1) 顶点着色器,particle_vertex_shader.glsl文件其中u_Time代表程序运行时间,a_ParticleStartTime代表粒子运行时间。
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 + (a_DirectionVector * v_ElapsedTime);
gl_Position = u_Matrix * vec4(currentPosition, 1.0);
gl_PointSize = 10.0;
}
(2) 片段着色器,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);
}
2、ParticleProgram类
ParticleProgram类封装了着色器,继承Program类。setCurrentTime方法设置程序开始时间。public class ParticleProgram extends Program {
private final static String U_MATRIX = "u_Matrix";
private final static String U_TIME = "u_Time";
private final static String A_POSITION = "a_Position";
private final static String A_COLOR = "a_Color";
private final static String A_DIRECTION_VECTOR = "a_DirectionVector";
private final static String A_PARTICLE_START_TIME = "a_ParticleStartTime";
private int uMatrixLocation, uTimeLocation;
private int aPositionLocation, aColorLocation, aDirectionVectorLocation, aParticleStartTimeLocation;
public ParticleProgram(Context context) {
super(context, R.raw.particle_vertex_shader, R.raw.particle_fragment_shader);
uMatrixLocation = GLES20.glGetUniformLocation(mProgramId, U_MATRIX);
uTimeLocation = GLES20.glGetUniformLocation(mProgramId, U_TIME);
aPositionLocation = GLES20.glGetAttribLocation(mProgramId, A_POSITION);
aColorLocation = GLES20.glGetAttribLocation(mProgramId, A_COLOR);
aDirectionVectorLocation = GLES20.glGetAttribLocation(mProgramId, A_DIRECTION_VECTOR);
aParticleStartTimeLocation = GLES20.glGetAttribLocation(mProgramId, A_PARTICLE_START_TIME);
}
@Override
public void setUniform(float[] projectionMatrix) {
GLES20.glUseProgram(mProgramId);
GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
}
public void setCurrentTime(float elapsedTime) {
GLES20.glUniform1f(uTimeLocation, elapsedTime);
}
public int getPositionLocation() {
return aPositionLocation;
}
public int getColorLocation() {
return aColorLocation;
}
public int getDirectionVectorLocation() {
return aDirectionVectorLocation;
}
public int getParticleStartTimeLocation() {
return aParticleStartTimeLocation;
}
}
3、Particle类
Particle类封装了顶点数据,包括顶点的xyz数据、颜色的rgb数据、移动的xyz数据和粒子开始时间。public class Particle extends Resource {
private final static int POSITION_COMPONENT_COUNT = 3;
private final static int COLOR_COMPONENT_COUNT = 3;
private final static int VECTOR_COMPONENT_COUNT = 3;
private final static int PARTICLE_START_TIME_COMPONENT_COUNT = 1;
private final static 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 * VertexArray.BYTES_PER_FLOAT;
private int maxParticleCount, currentParticleCount, nextParticle;
private float[] particles;
public Particle(int maxParticleCount) {
super(new float[maxParticleCount * TOTAL_COMPONENT_COUNT]);
this.particles = new float[maxParticleCount * TOTAL_COMPONENT_COUNT];
this.maxParticleCount = maxParticleCount;
}
public void addParticle(Geometry.Point position, int color, Geometry.Vector direction,
float particleStartTime) {
final int particleOffset = nextParticle * TOTAL_COMPONENT_COUNT;
int currentOffset = particleOffset;
nextParticle++;
if (currentParticleCount < maxParticleCount) {
currentParticleCount++;
}
// 如果到队尾,从0开始
if (nextParticle == maxParticleCount) {
nextParticle = 0;
}
// 开始位置
particles[currentOffset++] = position.x;
particles[currentOffset++] = position.y;
particles[currentOffset++] = position.z;
// 开始颜色
particles[currentOffset++] = Color.red(color) / 255f;
particles[currentOffset++] = Color.green(color) / 255f;
particles[currentOffset++] = Color.blue(color) / 255f;
// 移动位置
particles[currentOffset++] = direction.x;
particles[currentOffset++] = direction.y;
particles[currentOffset++] = direction.z;
// 当前创建时间
particles[currentOffset++] = particleStartTime;
// 修改顶点数据
mVertextArray.updateBuffer(particles, particleOffset, TOTAL_COMPONENT_COUNT);
}
public void bindData(ParticleProgram program) {
int dataOffset = 0;
bindData(program.getPositionLocation(), dataOffset, POSITION_COMPONENT_COUNT, STRIDE);
dataOffset += POSITION_COMPONENT_COUNT;
bindData(program.getColorLocation(), dataOffset, COLOR_COMPONENT_COUNT, STRIDE);
dataOffset += COLOR_COMPONENT_COUNT;
bindData(program.getDirectionVectorLocation(), dataOffset, VECTOR_COMPONENT_COUNT, STRIDE);
dataOffset += VECTOR_COMPONENT_COUNT;
bindData(program.getParticleStartTimeLocation(), dataOffset,
PARTICLE_START_TIME_COMPONENT_COUNT, STRIDE);
}
@Override
public void draw() {
super.draw();
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, currentParticleCount);
}
}
VertexArray类中updateBuffer方法
public void updateBuffer(float[] vertexData, int start, int count) {
floatBuffer.position(start);
floatBuffer.put(vertexData, start, count);
floatBuffer.position(0);
}
4、绘制着色器
(1) ParticleShooter类,粒子喷泉public class ParticleShooter {
private final Geometry.Point position;
private final Geometry.Vector direction;
private final int color;
ParticleShooter(Geometry.Point position, Geometry.Vector direction, int color) {
this.position = position;
this.direction = direction;
this.color = color;
}
public void addParticles(Particle particle, float currentTime, int count) {
for (int i = 0; i < count; i++) {
particle.addParticle(position, color, direction, currentTime);
}
}
}
(2) OpenGLParticleShaderRender类
private class OpenGLParticleShaderRender implements GLSurfaceView.Renderer {
private Particle mParticle;
private ParticleProgram mParticleProgram;
private ParticleShooter mRedParticleShooter, mGreenParticleShooter, mBlueParticleShooter;
private long mGlobalStartTime;
private float[] projectionMatrix = new float[16];
private float[] viewlMatrix = new float[16];
private float[] viewProjectionMatrix = new float[16];
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.1f);
mParticleProgram = new ParticleProgram(OpenGLParticleShaderActivity.this);
mParticle = new Particle(10000);
mGlobalStartTime = System.nanoTime();
final Geometry.Vector particleDirection = new Geometry.Vector(0f, 0.5f, 0f);
mRedParticleShooter = new ParticleShooter(new Geometry.Point(-1, 0, 0),
particleDirection, Color.rgb(250, 50, 5));
mGreenParticleShooter = new ParticleShooter(new Geometry.Point(0, 0, 0),
particleDirection, Color.rgb(25, 255, 25));
mBlueParticleShooter = new ParticleShooter(new Geometry.Point(1, 0, 0),
particleDirection, Color.rgb(5, 50, 255));
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置视图尺寸
GLES20.glViewport(0, 0, width, height);
// 创建透视投影
Matrix.perspectiveM(projectionMatrix, 0, 45, (float)width / (float)height, 1f, 10f);
// 定义模型矩阵
Matrix.setIdentityM(viewlMatrix, 0);
Matrix.translateM(viewlMatrix, 0, 0f, -1.5f, -5f);
Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewlMatrix, 0);
}
@Override
public void onDrawFrame(GL10 gl) {
// 清空屏幕
GLES20.glClear(GL10.GL_COLOR_BUFFER_BIT);
float currentTime = (System.nanoTime() - mGlobalStartTime) / 1000000000f;
mRedParticleShooter.addParticles(mParticle, currentTime, 5);
mGreenParticleShooter.addParticles(mParticle, currentTime, 5);
mBlueParticleShooter.addParticles(mParticle, currentTime, 5);
mParticleProgram.setUniform(viewProjectionMatrix);
mParticleProgram.setCurrentTime(currentTime);
mParticle.bindData(mParticleProgram);
mParticle.draw();
}
}
显示如下
5、扩散粒子
我们将变化每个粒子的速度,让每个粒子喷泉有更多的变化。(1) ParticleShooter类中添加角度和速度变量
private final float angleVariance;
private final float speendVariance;
private final Random random = new Random();
private float[] rotationMatrix = new float[16];
private float[] directionVector = new float[4];
private float[] resultVector = new float[4];
Matrix.setRotateEulerM方法创建一个旋转矩阵,用angleVariance的一个随机量改变发射角度。Matrix.setRotateEulerM(rotationMatrix, 0,
(random.nextFloat() - 0.5f) * angleVariance,
(random.nextFloat() - 0.5f) * angleVariance,
(random.nextFloat() - 0.5f) * angleVariance);
调整方向向量,每个分量都与speendVariance相乘。float speedAdjustment = 1f + random.nextFloat() * speendVariance;
Geometry.Vector thisDirection = new Geometry.Vector(
resultVector[0] * speedAdjustment,
resultVector[1] * speedAdjustment,
resultVector[2] * speedAdjustment);
ParticleShooter类
public class ParticleShooter {
private final Geometry.Point position;
private final Geometry.Vector direction;
private final int color;
private final float angleVariance;
private final float speendVariance;
private final Random random = new Random();
private float[] rotationMatrix = new float[16];
private float[] directionVector = new float[4];
private float[] resultVector = new float[4];
ParticleShooter(Geometry.Point position, Geometry.Vector direction, int color) {
this.position = position;
this.direction = direction;
this.color = color;
angleVariance = 0;
speendVariance = 0;
}
ParticleShooter(Geometry.Point position, Geometry.Vector direction, int color,
float angleVariance, float speendVariance) {
this.position = position;
this.direction = direction;
this.color = color;
this.angleVariance = angleVariance;
this.speendVariance = speendVariance;
directionVector[0] = direction.x;
directionVector[1] = direction.y;
directionVector[2] = direction.z;
}
public void addParticles(Particle particle, float currentTime, int count) {
for (int i = 0; i < count; i++) {
// particle.addParticle(position, color, direction, currentTime);
// 创建一个旋转矩阵,用angleVariance的一个随机量改变发射角度
Matrix.setRotateEulerM(rotationMatrix, 0,
(random.nextFloat() - 0.5f) * angleVariance,
(random.nextFloat() - 0.5f) * angleVariance,
(random.nextFloat() - 0.5f) * angleVariance);
Matrix.multiplyMV(resultVector, 0, rotationMatrix, 0, directionVector, 0);
// 调整发射速度
float speedAdjustment = 1f + random.nextFloat() * speendVariance;
Geometry.Vector thisDirection = new Geometry.Vector(
resultVector[0] * speedAdjustment,
resultVector[1] * speedAdjustment,
resultVector[2] * speedAdjustment);
particle.addParticle(position, color, thisDirection, currentTime);
}
}
}
(2) OpenGLParticleShaderRender类,替代ParticleShooter
// mRedParticleShooter = new ParticleShooter(new Geometry.Point(-1, 0, 0),
// particleDirection, Color.rgb(250, 50, 5));
//
// mGreenParticleShooter = new ParticleShooter(new Geometry.Point(0, 0, 0),
// particleDirection, Color.rgb(25, 255, 25));
//
// mBlueParticleShooter = new ParticleShooter(new Geometry.Point(1, 0, 0),
// particleDirection, Color.rgb(5, 50, 255));
final float angleVarianceInDegrees = 5f;
final float speedVariance = 1f;
mRedParticleShooter = new ParticleShooter(new Geometry.Point(-1, 0, 0),
particleDirection, Color.rgb(250, 50, 5), angleVarianceInDegrees, speedVariance);
mGreenParticleShooter = new ParticleShooter(new Geometry.Point(0, 0, 0),
particleDirection, Color.rgb(25, 255, 25), angleVarianceInDegrees, speedVariance);
mBlueParticleShooter = new ParticleShooter(new Geometry.Point(1, 0, 0),
particleDirection, Color.rgb(5, 50, 255), angleVarianceInDegrees, speedVariance);
显示如下
6、添加重力
通过给顶点着色器加入一个重力调整因子,来给粒子加入重力效应。顶点着色器,particle_vertex_shader.glsl文件
void main()
{
v_Color = a_Color;
v_ElapsedTime = u_Time - a_ParticleStartTime;
vec3 currentPosition = a_Position + (a_DirectionVector * v_ElapsedTime);
float gravityFactor = v_ElapsedTime * v_ElapsedTime / 9.8;
currentPosition.y -= gravityFactor;
gl_Position = u_Matrix * vec4(currentPosition, 1.0);
gl_PointSize = 10.0;
}
显示如下
7、混合粒子
就向我们看烟花表演,粒子越多,它们就应该越亮。模仿这个效果的方法之一是使用累加混合技术。在onSurfaceCreated里添加如下代码
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
显示如下
8、自定义点的外形
粒子被渲染为小的四方形,其每边的像素都等于gl_PointSize的值。我们可以使用另一个特殊的OpenGL变量gl_PointCoord自定义这些点的外形。对gl_PointCoord来说,其片段的位置范围都是[0, 1],因此,把点的圆心放在[0.5,0.5],只要距离这个点小于0.5,就是需要渲染的部分。
片段着色器,particle_fragment_shader.xml文件
void main()
{
/*
gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0);
*/
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);
}
}
显示如下
9、纹理粒子
(1) 首先修改片段着色器precision mediump float;
uniform sampler2D u_TextUnit;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main()
{
gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0)
* texture2D(u_TextUnit, gl_PointCoord);
}
(2) ParticleProgram类,获取u_TextUnit
private final static String U_TEXT_UNIT = "u_TextUnit";
private int uTextUnitLocation;
uTextUnitLocation = GLES20.glGetUniformLocation(mProgramId, U_TEXT_UNIT);
获取textureID,可参考前面private int mTextureId;
mTextureId = loadTexture(context, R.drawable.particle_texture);
最后在setMatrix方法中加载纹理
public void setMatrix(float[] projectionMatrix) {
GLES20.glUseProgram(mProgramId);
GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
// 把活动的纹理单元设置为纹理单元0
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// 把纹理绑定到这个单元
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
// 把被选择的纹理单元传递给片段着色器中的
GLES20.glUniform1i(uTextUnitLocation, 0);
}
显示如下