該原創文章首發於微信公眾號:位元組流動
OpenGL ES 粒子(Particles)
NDK OpenGL ES 3.0 開發(十三):例項化(Instancing)一文中我們瞭解到 OpenGL ES 例項化(Instancing)是一種只呼叫一次渲染函式就能繪製出很多物體的技術,可以實現將資料一次性傳送給 GPU ,避免了 CPU 多次向 GPU 下達渲染命令,提升了渲染效能。
而粒子系統本質上是通過一次或者多次渲染繪製出大量位置、形狀或者顏色不同的物體(粒子),形成大量粒子運動的視覺效果。所以,粒子系統天然適合用OpenGL ES 例項化(Instancing)實現。
定義粒子,通常一個粒子有一個生命值,生命值結束該粒子消失,還有描述粒子在(x, y, z)三個方向的位置(偏移)和運動速度,以及粒子的顏色等屬性。本文中粒子的定義:
struct Particle {
GLfloat dx,dy,dz;//offset 控制粒子的位置
GLfloat dxSpeed,dySpeed,dzSpeed;//speed 控制粒子的運動速度
GLubyte r,g,b,a; //r,g,b,a 控制粒子的顏色
GLfloat life; //控制粒子的生命值
Particle()
{
dx = 0.0f;
dy = 0.0f;
dz = 0.0f;
r = static_cast<GLubyte>(1.0f);
g = static_cast<GLubyte>(1.0f);
b = static_cast<GLubyte>(1.0f);
a = static_cast<GLubyte>(1.0f);
dxSpeed = 1.0f;
dySpeed = 1.0f;
dzSpeed = 1.0f;
life = 0.0f;
}
};
複製程式碼
渲染粒子需要用到的頂點著色器:
#version 300 es
precision mediump float;
layout(location = 0) in vec3 a_vertex;//頂點座標
layout(location = 1) in vec2 a_texCoord;//紋理座標
layout(location = 2) in vec3 a_offset;//位置偏移
layout(location = 3) in vec4 a_particlesColor;//粒子顏色(照在粒子表面光的顏色)
uniform mat4 u_MVPMatrix;//變換矩陣
out vec2 v_texCoord;
out vec4 v_color;
void main()
{
gl_Position = u_MVPMatrix * vec4(a_vertex - vec3(0.0, 0.95, 0.0) + a_offset, 1.0);
// vec3(0.0, 0.95, 0.0) 是為了使粒子整體向 y 軸負方向有一個偏移
v_texCoord = a_texCoord;
v_color = a_particlesColor;
}
複製程式碼
渲染粒子需要用到的片段著色器:
#version 300 es
precision mediump float;
in vec2 v_texCoord;
in vec4 v_color;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
void main()
{
outColor = texture(s_TextureMap, v_texCoord) * v_color;
}
複製程式碼
屬性a_offset
是粒子的位置偏移,最終確定粒子的位置,屬性a_particlesColor
表示照在粒子表面光的顏色,這兩個屬性均為例項化陣列,因為每個粒子有不同的位置和顏色。
設定屬性a_offset
和 a_particlesColor
為例項化陣列:
glVertexAttribDivisor(0, 0);
glVertexAttribDivisor(1, 0);
glVertexAttribDivisor(2, 1);
glVertexAttribDivisor(3, 1);
複製程式碼
glVertexAttribDivisor(0, 0);
表示例項化繪製時對 index =0 的屬性不更新;glVertexAttribDivisor(2, 1);
用於指定 index = 2 的屬性為例項化陣列,1 表示每繪製一個例項,更新一次陣列中的元素。
因為每次例項化渲染粒子時,都要更新 a_offset
和 a_particlesColor
例項化陣列,所以設定其對應的 VBO 為動態型別 GL_DYNAMIC_DRAW 。
glGenBuffers(1, &m_ParticlesPosVboId);
glBindBuffer(GL_ARRAY_BUFFER, m_ParticlesPosVboId);
// Initialize with empty (NULL) buffer : it will be updated later, each frame.
glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLES * 3 * sizeof(GLfloat), NULL, GL_DYNAMIC_DRAW);
glGenBuffers(1, &m_ParticlesColorVboId);
glBindBuffer(GL_ARRAY_BUFFER, m_ParticlesColorVboId);
// Initialize with empty (NULL) buffer : it will be updated later, each frame.
glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLES * 4 * sizeof(GLubyte), NULL, GL_DYNAMIC_DRAW);
複製程式碼
新粒子的速度、偏移以及顏色都是隨機生成的,生成新粒子的實現為:
void ParticlesSample::Init()
{
for (int i = 0; i < MAX_PARTICLES; i++)
{
GenerateNewParticle(m_ParticlesContainer[i]);
}
}
void ParticlesSample::GenerateNewParticle(Particle &particle)
{
particle.life = 5.0f;
particle.cameraDistance = -1.0f;
particle.dx = (rand() % 2000 - 1000.0f) / 3000.0f;
particle.dy = (rand() % 2000 - 1000.0f) / 3000.0f;
particle.dz = (rand() % 2000 - 1000.0f) / 3000.0f;
float spread = 1.5f;
glm::vec3 maindir = glm::vec3(0.0f, 2.0f, 0.0f);
glm::vec3 randomdir = glm::vec3(
(rand() % 2000 - 1000.0f) / 1000.0f,
(rand() % 2000 - 1000.0f) / 1000.0f,
(rand() % 2000 - 1000.0f) / 1000.0f
);
glm::vec3 speed = maindir + randomdir * spread;
particle.dxSpeed = speed.x;
particle.dySpeed = speed.y;
particle.dzSpeed = speed.z;
particle.r = static_cast<unsigned char>(rand() % 256);
particle.g = static_cast<unsigned char>(rand() % 256);
particle.b = static_cast<unsigned char>(rand() % 256);
particle.a = static_cast<unsigned char>((rand() % 256) / 3);
}
複製程式碼
查詢生命值結束的粒子:
int ParticlesSample::FindUnusedParticle()
{
for (int i = m_LastUsedParticle; i < MAX_PARTICLES; i++)
{
if (m_ParticlesContainer[i].life <= 0)
{
m_LastUsedParticle = i;
return i;
}
}
for (int i = 0; i < m_LastUsedParticle; i++)
{
if (m_ParticlesContainer[i].life <= 0)
{
m_LastUsedParticle = i;
return i;
}
}
return -1;
}
複製程式碼
更新粒子(更新粒子的位置、運動速度和生命值),然後更新例項化陣列:
int ParticlesSample::UpdateParticles()
{
LOGCATE("ParticlesSample::UpdateParticles");
//每次生成 300 個新粒子,產生爆炸的效果
int newParticles = 300;
for (int i = 0; i < newParticles; i++)
{
int particleIndex = FindUnusedParticle();
if (particleIndex >= 0)
{
GenerateNewParticle(m_ParticlesContainer[particleIndex]);
}
}
int particlesCount = 0;
for (int i = 0; i < MAX_PARTICLES; i++)
{
Particle &p = m_ParticlesContainer[i]; // shortcut
//生命值大於 0 的粒子進行更新
if (p.life > 0.0f)
{
float delta = 0.1f;
glm::vec3 speed = glm::vec3(p.dxSpeed, p.dySpeed, p.dzSpeed), pos = glm::vec3(p.dx,
p.dy,
p.dz);
//更新粒子生命值
p.life -= delta;
if (p.life > 0.0f)
{
//更新粒子速度
speed += glm::vec3(0.0f, 0.081f, 0.0f) * delta * 0.5f;
pos += speed * delta;
p.dxSpeed = speed.x;
p.dySpeed = speed.y;
p.dzSpeed = speed.z;
//更新粒子位置
p.dx = pos.x;
p.dy = pos.y;
p.dz = pos.z;
m_pParticlesPosData[3 * particlesCount + 0] = p.dx;
m_pParticlesPosData[3 * particlesCount + 1] = p.dy;
m_pParticlesPosData[3 * particlesCount + 2] = p.dz;
//不更新粒子的顏色
m_pParticlesColorData[4 * particlesCount + 0] = p.r;
m_pParticlesColorData[4 * particlesCount + 1] = p.g;
m_pParticlesColorData[4 * particlesCount + 2] = p.b;
m_pParticlesColorData[4 * particlesCount + 3] = p.a;
}
particlesCount++;
}
}
//更新例項化陣列
glBindBuffer(GL_ARRAY_BUFFER, m_ParticlesPosVboId);
glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLES * 3 * sizeof(GLfloat), NULL,
GL_DYNAMIC_DRAW); // Buffer orphaning, a common way to improve streaming perf. See above link for details.
GO_CHECK_GL_ERROR();
glBufferSubData(GL_ARRAY_BUFFER, 0, particlesCount * sizeof(GLfloat) * 3, m_pParticlesPosData);
GO_CHECK_GL_ERROR();
glBindBuffer(GL_ARRAY_BUFFER, m_ParticlesColorVboId);
glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLES * 4 * sizeof(GLubyte), NULL,
GL_DYNAMIC_DRAW); // Buffer orphaning, a common way to improve streaming perf. See above link for details.
glBufferSubData(GL_ARRAY_BUFFER, 0, particlesCount * sizeof(GLubyte) * 4,
m_pParticlesColorData);
return particlesCount;
}
複製程式碼
每次繪製時,先獲取生命值大於 0 粒子的數量再進行繪製:
void ParticlesSample::Draw(int screenW, int screenH)
{
LOGCATE("ParticlesSample::Draw()");
if (m_ProgramObj == GL_NONE || m_TextureId == GL_NONE) return;
glEnable(GL_DEPTH_TEST);
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(1.0, 1.0, 1.0, 1.0);
glDisable(GL_BLEND);
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float) screenW / screenH);
//每次獲取生命值大於 0 粒子的數量
int particleCount = UpdateParticles();
// Use the program object
glUseProgram(m_ProgramObj);
glBindVertexArray(m_VaoId);
glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
// Bind the RGBA map
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, particleCount);
}
複製程式碼
實現程式碼路徑: NDK_OpenGLES_3_0