目錄
- 粒子和粒子系統(tǒng)
- 實(shí)踐:噴泉效果
- 遇到的問題
- 資料
- 收獲
通過該篇的實(shí)踐實(shí)現(xiàn)如下效果
一、什么是粒子和粒子系統(tǒng)
如何定義粒子面徽?
一個粒子有位置信息(x,y,z)佩研、運(yùn)動方向荔茬、顏色、生命值(開始和結(jié)束的時間)等屬性
什么粒子系統(tǒng)有额?
通過渲染繪制出大量 位置般又、形狀、方向巍佑、顏色不同的物體(粒子)茴迁,從而形成大量粒子運(yùn)動的效果。
明確了概念我們來逐步實(shí)現(xiàn)煙花效果
二句狼、實(shí)踐:噴泉效果
面對一個比較大或者沒有嘗試過的項(xiàng)目或內(nèi)容笋熬,害怕、怯懦時常出現(xiàn)腻菇,這時要理清目標(biāo)胳螟,抓住主線,運(yùn)用結(jié)構(gòu)化思維筹吐,拆解流程糖耸,然后逐步實(shí)現(xiàn)每個環(huán)節(jié),解決每個環(huán)節(jié)以中的問題丘薛,這也是打怪升級的過程嘉竟,下面一起來享受這個過程吧。
目標(biāo):了解和運(yùn)用粒子系統(tǒng)洋侨,實(shí)現(xiàn)比較炫酷的效果舍扰,從粒子噴泉著手。
流程拆解
- 梳理粒子特性(坐標(biāo)希坚、顏色边苹、運(yùn)動矢量、開始時間)以及 重力和阻力的影響裁僧、持續(xù)時間
- 粒子發(fā)生器如何發(fā)射粒子(反射點(diǎn)个束、方向慕购、數(shù)量)
- 編寫著色器glsl代碼
- 編寫Program
- 編寫GLSurfaceView的Render進(jìn)行著色器加載、編譯茬底、鏈接沪悲、使用、渲染
下面開始我們的具體實(shí)踐
2.1. 定義粒子系統(tǒng)對象
定義粒子的屬性以及提供添加粒子的方法
public class ParticleSystem {
//位置 xyz
private final int POSITION_COMPONENT_COUNT = 3;
//顏色 rgb
private final int COLOR_COMPONENT_COUNT = 3;
//運(yùn)動矢量 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;
//粒子游標(biāo)
private int nextParticle;
//粒子計(jì)數(shù)
private int curParticleCount;
//粒子數(shù)組
private final float[] particles;
//最大粒子數(shù)量
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 運(yùn)動矢量
* @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++;
}
//重復(fù)使用阱表,避免內(nèi)存過大
if (nextParticle == maxParticleCount) {
nextParticle = 0;
}
//填充 位置坐標(biāo) 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;
//填充 運(yùn)動矢量
particles[currentOffset++] = direction.x;
particles[currentOffset++] = direction.y;
particles[currentOffset++] = direction.z;
//填充粒子開始時間
particles[currentOffset++] = particStartTime;
//把新增的粒子添加到頂點(diǎn)數(shù)組FloatBuffer中
vertexArray.updateBuffer(particles, particleOffset, TOTAL_COMPONENT_COUNT);
}
}
2.2. 定義粒子發(fā)射器對象
public class ParticleShooter {
//發(fā)射粒子的位置
private final Geometry.Point position;
//發(fā)射粒子的顏色
private final int color;
//發(fā)射粒子的方法
private final Geometry.Vector direction;
public ParticleShooter(Geometry.Point position, int color, Geometry.Vector direction) {
this.position = position;
this.color = color;
this.direction = direction;
}
/**
* 調(diào)用粒子系統(tǒng)對象添加粒子
*
* @param particleSystem
* @param currentTime
*/
public void addParticles(ParticleSystem particleSystem, float currentTime) {
particleSystem.addParticle(position, color, direction, currentTime);
}
}
2.3. 編寫頂點(diǎn)和片元著色器
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;
//粒子已經(jīng)持續(xù)時間 當(dāng)前時間-開始時間
v_ElapsedTime = u_Time - a_PatricleStartTime;
//重力或者阻力因子殿如,隨著時間的推移越來越大
float gravityFactor = v_ElapsedTime * v_ElapsedTime / 9.8;
//當(dāng)前的運(yùn)動到的位置 粒子起始位置+(運(yùn)動矢量*持續(xù)時間)
vec3 curPossition = a_Position + (a_Direction * v_ElapsedTime);
//減去重力或阻力的影響
curPossition.y -= gravityFactor;
//把當(dāng)前位置通過內(nèi)置變量傳給片元著色器
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);
}
/**
* 設(shè)置 始終如一的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);
//定義粒子系統(tǒng) 最大包含1w個粒子捶枢,超過最大之后復(fù)用最前面的
mParticleSystem = new ParticleSystem(10000);
//粒子系統(tǒng)開始時間
mSystemStartTimeNS = System.nanoTime();
//定義粒子發(fā)射器
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);
//當(dāng)前(相對)時間 單位秒
float curTime = (System.nanoTime() - mSystemStartTimeNS)/1000000000f;
//粒子發(fā)生器添加粒子
mParticleShooter.addParticles(mParticleSystem,curTime);
//使用Program
mProgram.useProgram();
//設(shè)置Uniform變量
mProgram.setUniforms(curTime);
//設(shè)置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 用于給頂點(diǎn)著色器的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();
}
}
效果如下
是不是和預(yù)期的效果差別很大,別著急烂叔,只要在正確的路上谨胞,遇到問題就分析解決,最重要的是堅(jiān)持前行蒜鸡。
三胯努、問題
可以看到有如下幾個問題
- 粒子發(fā)射沒有方向變化
- 新發(fā)射的和下落的重疊
下面我們來一一解決。
問題1. 隨機(jī)改變發(fā)射粒子的方向
粒子的發(fā)射方向是有發(fā)射器決定的逢防,我們現(xiàn)在是一個固定的向上發(fā)送 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);
}
...
}
如果想改變這個為隨機(jī)方向,我們需要修改這個這個direction
我們通過隨機(jī)數(shù)以及矩陣變換來實(shí)現(xiàn)方向的隨機(jī)
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. 重疊覆蓋
通過修改粒子的發(fā)射方向忘朝,看起來重疊效果也沒有了灰署,真的是這樣嗎,還是不容易觀察到了局嘁?
為了驗(yàn)證問題我們同一時刻多加些粒子溉箕,即同時發(fā)送多批粒子粒子系統(tǒng)。
修改也比較簡單悦昵,加個for循環(huán)即可肴茄。
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,效果如下
可以看到重疊覆蓋又出現(xiàn)了但指,為什么會出現(xiàn)這種情況寡痰?
同一時刻,同一位置棋凳,既有下落的粒子拦坠,又有上升的粒子,如果下落的粒子后渲染剩岳,就會覆蓋上升的粒子贪婉。
那么該如何解決吶?
OpenGL提供了累加混合技術(shù) GL_BLEND_卢肃,公式如下
輸出 = (源因子 * 源片段)+(目標(biāo)因子 * 目標(biāo)片段)
具體實(shí)現(xiàn)為疲迂,在Render的onSurfaceCrate中設(shè)置
@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 * 目標(biāo)片段)
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE);
....
}
效果如下:
可以看到?jīng)]有再出現(xiàn)重疊覆蓋的情況了。
還有一個問題是莫湘,每個粒子都是一個點(diǎn)尤蒿,大小通過gl_PointSize設(shè)置為10,看到的是正方形粒子幅垮。能不能修改為圓形或者指定樣式吶腰池?_
問題3. 把點(diǎn)修改為紋理圖片
下面我們就通過紋理圖片來把每個點(diǎn)繪制為一個點(diǎn)精靈
關(guān)于紋理的使用如果不熟悉,請先閱讀[音視頻開發(fā)之旅(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);
//改為:通過內(nèi)置函數(shù)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) {
...
//設(shè)置Uniform變量
mProgram.setUniforms(curTime,mTextureId);
...
}
}
...
}
效果如下:
最后我們修改頂點(diǎn)著色器中定義的點(diǎn)的大小以及運(yùn)動矢量
gl_PointSize = 25.0;
//定義粒子發(fā)射器
mParticleShooter = new ParticleShooter(new Geometry.Point(0f, -0.9f, 0f),
Color.rgb(255, 50, 5),
new Geometry.Vector(0f, 0.8f, 0f));
就是開篇展示的效果
這篇就到這里了,通過實(shí)踐呵萨,一步步實(shí)現(xiàn)最終的噴泉效果奏属。
下一篇我們繼續(xù)來學(xué)習(xí)實(shí)踐粒子系統(tǒng),實(shí)現(xiàn)煙花空中爆炸的效果潮峦。
四囱皿、資料
《OpenGL ES 3.0 編程指南》
《OpenGL ES應(yīng)用開發(fā)實(shí)踐指南》
[粒子系統(tǒng)--煙花 [OpenGL-Transformfeedback]]
[Android制作粒子爆炸特效]
[OpenGL進(jìn)階(六)-粒子系統(tǒng)]
[【OpenGL】Shader實(shí)例分析(七)- 雪花飄落效果]
五、收獲
- 了解粒子屬性和粒子系統(tǒng)
- 通過任務(wù)拆解逐步實(shí)現(xiàn)噴泉效果
- 解決遇到的問題(重力忱嘹、發(fā)射方向嘱腥、重疊覆蓋、點(diǎn)精靈)
感謝你的閱讀拘悦。
下一篇我們繼續(xù)來學(xué)習(xí)實(shí)踐粒子系統(tǒng)齿兔,實(shí)現(xiàn)煙花空中爆炸的效果。歡迎關(guān)注公眾號“音視頻開發(fā)之旅”础米,一起學(xué)習(xí)成長分苇。
歡迎交流