There are many cool applications out there that use particle system to display and animate 3D volumetric objects, which are very inspiring. Other than creating interesting visual effect, I’ve also seen a conceptual application that reconstructs 3D scene with live streaming vector data, which makes more sense because this technique is meant to reduce the large bandwidth required for live video streaming. On the other hand, 3D model assets are perfect resources for populating particle systems, as they are binary files that contain mainly vertex data.

Example

Mesh preparation

For this sketch I used a space shuttle model which also appeared in my other posts. The model is a low polygon approximation which consists of about 5k vertices, most of which represents the very intricate parts of the model. As I imported the original model and rendered it in particles, I noticed there was a quite uneven distribution of vertices.

5k Vertices

I subdivided the model for 3 folds and got a model with 500k vertices, which is dense enough for my laptop to render, but there is still more headroom.

500k Vertices

Creating particle system

I loaded the model with Cinder’s ObjLoader, and after quite a few steps converting back and forth I obtained a VboMesh, which I put into a batch associated with my particle shader.

// load obj file into TriMesh, convert to VboMesh
TriMeshRef triMesh = TriMesh::create(ObjLoader(loadAsset("space-shuttle-500k.obj"), false, false));
gl::VboMeshRef vboMesh = gl::VboMesh::create(*triMesh);

// get attributes layout and VBO from VboMesh
auto layoutVboPair = vboMesh->getVertexArrayLayoutVbos()[0];
mVbo = layoutVboPair.second;
auto layout = layoutVboPair.first;
auto numVertices = vboMesh->getNumVertices();

// construct new VboMesh for GL_POINTS primitive
mVboMesh = gl::VboMesh::create(numVertices, GL_POINTS, { { layout, mVbo } } );
mParticleShader = gl::GlslProg::create(loadAsset("particle.vs"), loadAsset("particle.fs"));
mBatch = gl::Batch::create(mVboMesh, mParticleShader);

To render this mesh in a particle manner, I just need to implement a shader that outputs gl_Position and oColor, then call mBatch->draw().

Enabling point sprite

However, there are a couple of problems with OpenGL’s default GL_POINTS primitive rendering. First is that the points are all 1 pixel dots, you won’t get good enough look even though you have 500k particles per screen size. Also, it is very confusing that the gl::pointSize() function doesn’t work, because it is actually deprecated, and this fact is not documented in Cinder.

After a lot of searching I found it’s possible through calling gl::enable(GL_VERTEX_PROGRAM_POINT_SIZE) and outputting gl_PointSize in vertex shader. Then I found that the particles became larger squares. And sadly GL_POINT_SMOOTH is not supported in Cinder. So I eventually went with point sprites.

To render the mesh in point sprites, you can still use a VboMesh with primitive type GL_POINTS, just enable GL_POINT_SPRITE_ARB. Then you can bind a texture to the shader. In the fragment shader, simply use a varying called gl_PointCoord to sample the texture. I disabled depth test and used additive blending in this sketch. I also associated the color with fragment depths.

Point sprite with alpha blending Point sprite with alpha blending

Animation

Since it’s always good to harness the power of GPU to do particle system animations, I wrote a simple animation in the vertex shader using fake 3D perlin noise. Another way of doing complex computing is to use transform feedback buffers.

Animation Animation Animation

The final shader code is here:

Vertex shader:

#version 150

uniform mat4 ciModelViewProjection;
uniform float ciElapsedSeconds;

in vec4 ciPosition;
in vec4 ciColor;

out float vDeformPerc;

float hash(float n) {
return fract(sin(n) * 43758.5453);
}

float noise(vec3 x) {
vec3 p = floor(x);
vec3 f = fract(x);

f = f * f * (3.0 - 2.0 * f);
float n = p.x + p.y * 57.0 + 113.0 * p.z;

return mix(mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
mix(hash(n + 57.0), hash(n + 58.0), f.x), f.y),
mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
mix(hash(n + 170.0), hash(n + 171.0), f.x), f.y), f.z);
}

void main() {
float t = ciElapsedSeconds * 0.3;
vDeformPerc = 1 - (cos(t) + 1) / 2;
float amount = vDeformPerc * 5;


float dx = noise(vec3(t, ciPosition.y, ciPosition.z));
float dy = noise(vec3(ciPosition.x, t, ciPosition.z));
float dz = noise(vec3(ciPosition.x, ciPosition.y, t));
vec4 displace = vec4(dx, dy, dz, 0);
gl_Position = ciModelViewProjection * (ciPosition + normalize(ciPosition) + displace * amount);
gl_PointSize = 8;
}

Fragment shader:

#version 150

uniform sampler2D uSpriteTexture;

in vec4 ciColor;
in float vDeformPerc;

out vec4 oColor;

void main() {
float depth = gl_FragCoord.z / gl_FragCoord.w;
float mappedDepth = smoothstep(50, -50, depth);
vec4 tint = vec4(0.4 * mappedDepth, 0.1, 0.3 + 0.6 * (1 - vDeformPerc), 0.5 * mappedDepth + 0.5 * (1 - vDeformPerc));
oColor = texture(uSpriteTexture, gl_PointCoord) * tint;
}

References

Cinder 0.9.0 ParticleSphere examples, Paul Haux’s “Stars” opensource application and those invaluable posts (1, 2).