A depth-data-driven brand experience
Front-End
There were a few prerequisites going into the build for this application that I was able to establish at the very onset. I knew that the depth input was going to require some heavy lifting to return meaningful data. And in order for the system to run at a decent framerate, the gpu was going to likely need to play a role. So I set off to build a test front-end that could perform well above 60fps. Cinder seemed to be a perfect candidate to assist me through this.
The visual architecture is broken up into two levels which can simply be described as anchor points and control points. The anchor points act as a skewed particle grid. And the control points interpolate between their respective anchors.

In order to produce a continuous curve, I employed a cubic interpolation method found here: http://www.paulinternet.nl/
const Vec2f &p0,
const Vec2f &p1,
const Vec2f &p2,
const Vec2f &p3,
float t
)
{
return p1 + 0.5f*t*(p2-p0 + t*(2*p0-5*p1+4*p2-p3 + t*(3*(p1-p2)+p3-p0)));
}
and so to get the interpolated location of a control point, say C2, it might look like this:
anchor_set.at(0),
anchor_set.at(1),
anchor_set.at(2),
anchor_set.at(3), 0.33f);
you can add as many segments as will suit your needs here. In this sense, they act much more like the middle segments more than control points… only a difference in nomenclature. For N points between two anchors, their offset is an interval of 1.0f/(N+1).

The idea here was to limit the number of points required to update their velocities after receiving user input.
In my initial test, I decided to test-drive Path2d to test out my math and to see how it would perform with a simple mouse input.

The red specs are bezier control points along the curve which are being interpolated by their four nearest anchors.

In this test, the particle grid was a std::vector of 50(per shape side)*2 *160(number of shapes)=16,000 objects that would resolve their position and velocity independently based on their mass and the mouse position and velocity. The framerate was too far below my predetermined ideal. It was probably around 10. This was expected though. So, it was a breakthrough as far as I was concerned.
The next step was to shift my thinking to build a polygon mesh set with a controlled number of curve segments. So I played around a little with some VBO samples provided with the cinder framework to understand how to work with and initialize vertex positions, indices and eventually texture coordinates.
Initially I was constructing a single VBO mesh and instancing it in a draw loop and iterating through the particle vector. But eventually I ended up with the code below. This is the initialization code for the VBO portion of the application. In fact, this is the only code for VBO anything other than the draw calls.
{
// setup the parameters of the Vbo
int totalVertices = VERTICES_Y*VERTS_X_PER_MESH;
int totalQuads = (VERTS_X_PER_MESH-1) * (VERTICES_Y-1) * 4;
gl::VboMesh::Layout layout;
layout.setStaticIndices();
layout.setStaticPositions();
layout.setStaticTexCoords2d();
layout.setStaticNormals();
gl::VboMesh mVboMesh = gl::VboMesh( totalVertices, totalQuads, layout, GL_QUADS);
// buffer our static data - the texcoords and the indices
vector<uint32_t> indices;
vector<Vec2f> texCoords;
for (int j=0; j!=VERTICES_Y; j++)
{
//indices.push_back(j*VERTS_X_PER_MESH+k);// for points versus quads (i.e. below)
for (int k=0; k!=VERTS_X_PER_MESH; k++)
{
// create a quad for each vertex, except for along the bottom and right edges
if(j + 1 != VERTICES_Y && k + 1 != VERTS_X_PER_MESH)
{
indices.push_back( (j+0) * VERTS_X_PER_MESH + (k+0) );
indices.push_back( (j+1) * VERTS_X_PER_MESH + (k+0) );
indices.push_back( (j+1) * VERTS_X_PER_MESH + (k+1) );
indices.push_back( (j+0) * VERTS_X_PER_MESH + (k+1) );
}
float x_ordered = (float)(k+i*VERTS_X_PER_MESH) / (float)(VERTICES_X-1);
float y_ordered = (float)j / (float)(VERTICES_Y-1);
texCoords.push_back(Vec2f(x_ordered, y_ordered));
}
}
mVboMesh.bufferIndices( indices );
mVboMesh.bufferTexCoords2d(0, texCoords);
mesh_set.push_back(mVboMesh);
}
This loop will construct a cinder VBOmesh for each of the diagonal shapes in the application. Notice that I am looping through a const “VERTICES_Y” and not “ANCHORS_Y”. That is because these meshes are going to include every single point with no differentiation of whether they’re an anchor or not. Also notice that the vertex positions are not being set. These things will happen later in a shader in a very awesome way. The trickiest part about this chunk was setting up the texture coordinates to make sense for all VBOs to belong to a singular 2d texture.
Setting up the textures is the fun part because this is when you start to see all of it make sense. To grok the concept of a hardware accelerated particle system, I constantly have referred back to http://www.2ld.de/gdc2004/ and always try to keep an eye on what is going on in this thread. So a big thank you to Num3ric for his activity on this post.

This was the most revealing point of the paper for me. With 32 bit floating point precision on GPUs now, pixels of a texture can represent what I initially had built as 16,000+ objects. With a little creativity and some elbow grease to set up, this is a virtually limitless opportunity for code-artists to elevate visions.
Setup steps are pretty sensible:
1. Initialize a Surface for each particle texture and populate it.
2. Initialize Textures and populate their data with their Surfaces.
3. Initialize a 2 buffer FBO with X color buffers based on how many Textures there are and bind them.
Surfaces I need: Initial position, Initial Velocity/Mass.
Textures I need: Initial position, Position (data from initial position is used here as well), Velocity/Mass.
FBO color buffers I need: Initial position (GL_COLOR_ATTACHMENT0_EXT), Position (GL_COLOR_ATTACHMENT1_EXT), Velocity/Mass(GL_COLOR_ATTACHMENT2_EXT).
All are formatted as 32f RGBA and match the number of anchors (not the same as VBO since it includes the vertices between them as well). During surface creation, I zero out the channels I don’t need.
{
//SETUP TEXTURES - Position
mInitPos = Surface32f(VERTICES_X, ANCHORS_Y, true);
//SETUP TEXTURES - Velocity
mInitVel = Surface32f(VERTICES_X, ANCHORS_Y, true);
//SETUP PARTICLES AND MESH
setupMesh();
//SETUP TEXTURES
gl::Texture::Format tFormat;
tFormat.setInternalFormat(GL_RGBA32F_ARB);
//INSTANTIATE TEXTURES - Position
mPositions = gl::Texture(mInitPos, tFormat);
mPositions.setWrap(GL_REPEAT, GL_REPEAT);
mPositions.setMinFilter(GL_NEAREST);
mPositions.setMagFilter(GL_NEAREST);
//INSTANTIATE TEXTURES - Velocity
mVelocities = gl::Texture(mInitVel, tFormat);
mVelocities.setWrap(GL_REPEAT, GL_REPEAT);
mVelocities.setMinFilter(GL_NEAREST);
mVelocities.setMagFilter(GL_NEAREST);
//INSTANTIATE Fbos
gl::Fbo::Format format;
format.enableDepthBuffer(false);
format.enableColorBuffer(true, FBO_COLOR_BUFFERS);
format.setMinFilter(GL_NEAREST);
format.setMagFilter(GL_NEAREST);
format.setColorInternalFormat(GL_RGBA32F_ARB);
mFbo[0] = gl::Fbo(VERTICES_X, ANCHORS_Y, format);
mFbo[1] = gl::Fbo(VERTICES_X, ANCHORS_Y, format);
}
void resetFbos()
{
drawFboIndex = 0;
mFbo[0].bindFramebuffer();
mFbo[1].bindFramebuffer();
//Attachment 0 - Positions Origin
glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
gl::setMatricesWindow(mFbo[0].getSize(), false);
gl::setViewport(mFbo[0].getBounds());
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
mPositions.enableAndBind();
gl::draw(mPositions, mFbo[0].getBounds());
mPositions.unbind();
//Attachment 1 - Positions
glDrawBuffer(GL_COLOR_ATTACHMENT1_EXT);
gl::setMatricesWindow(mFbo[0].getSize(), false);
gl::setViewport(mFbo[0].getBounds());
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
mPositions.enableAndBind();
gl::draw(mPositions, mFbo[0].getBounds());
mPositions.unbind();
// Attachment 2 - Velocities
glDrawBuffer(GL_COLOR_ATTACHMENT2_EXT);
gl::setMatricesWindow(mFbo[0].getSize(), false);
gl::setViewport(mFbo[0].getBounds());
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
mVelocities.enableAndBind();
gl::draw( mVelocities, mFbo[0].getBounds() );
mVelocities.unbind();
mFbo[1].unbindFramebuffer();
mFbo[0].unbindFramebuffer();
mPositions.disable();
mVelocities.disable();
}
The 2 biggest hangups I got stuck on through this process were making sure that the formatting of Surfaces, Textures, FBO color buffers were compatible and properly set AND setting up the initial position Surface. A color channel is represented as a normalized 32f. So given a particle origin at (-500, 200) in a 1920×1080 window, the correct origin would need to be provided two new properties, particle bounds: dim, and an offset from window’s coordinates: zero_offset. the result: ((-500.0f+zero_offset.x)/dim.x, (200.0f+zero_offset.y)/dim.y).
However, there is one reason why initializing this way would be disadvantageous. Precision is unnecessarily reduced. Besides, who cares about what you can’t see?
Instead, allow the values to be capped by the nature of the texture format, GL_REPEAT. i.e. (-500.0f/window.x, 200.0f/window.y).
{
Vec2f zero_offset = Vec2f(-h, (app::getWindowHeight()-h)*0.5f);
//system iterates in a VERTS_X_PER_MESH by height fashion to complete a multimesh walkthrough
for (int i=0; i<ANCHORS_Y; i++)
{
for (int j=0; j<VERTICES_X/VERTS_X_PER_MESH; j++)
{
float x = zero_offset.x;
float y = zero_offset.y + ((float)h/(ANCHORS_Y))*i;
// anchor 1 left
x += ((float)i/(ANCHORS_Y))*h;
x += j*SHAPE_GAP;
float x_norm = (float)x/app::getWindowWidth();
float y_norm = (float)y/app::getWindowHeight();
float mRadius = Rand::randFloat( 0.01f )+0.99f;
float mass = mRadius * mRadius;
mInitPos.setPixel(Vec2i(j*VERTS_X_PER_MESH,i), ColorAf(x_norm, y_norm, 0.0f, mass));
// anchor 1 right
x += w;
x_norm = (float)x/app::getWindowWidth();
y_norm = (float)y/app::getWindowHeight();
mRadius = Rand::randFloat( 0.01f )+0.99f;
mass = mRadius * mRadius;
mInitPos.setPixel(Vec2i(j*VERTS_X_PER_MESH+1,i), ColorAf(x_norm, y_norm, 0.0f, mass));
}
}
Surface32f::Iter iterV = mInitVel.getIter();
while (iterV.line()) {
while (iterV.pixel()) {
mInitVel.setPixel(iterV.getPos(), ColorAf(0.0f, 0.0f, 0.0f, 1.0f));
}
}
}
Setting the origin positions is really the only component that is overly-specific to this application within the cpp code-base.
The last bits of that need written — other than the shaders — are the update and draw procedures:
During update, one frame buffer’s textures are bound while the other is written to. The writing is a frag shader’s job.
{
gl::setMatricesWindow(mFbo[0].getSize(), false); // false to prevent vertical flipping
gl::setViewport(mFbo[0].getBounds());
mFbo[drawFboIndex].bindFramebuffer();
GLenum buf[FBO_COLOR_BUFFERS] = { GL_COLOR_ATTACHMENT0_EXT,
GL_COLOR_ATTACHMENT1_EXT,
GL_COLOR_ATTACHMENT2_EXT,
};
glDrawBuffers(FBO_COLOR_BUFFERS, buf);
mFbo[(drawFboIndex+1)%FBO_TOTAL].bindTexture(0,0);
mFbo[(drawFboIndex+1)%FBO_TOTAL].bindTexture(1,1);
mFbo[(drawFboIndex+1)%FBO_TOTAL].bindTexture(2,2);
vTexture_xy.bind(3);
fTexture.bind(4);
super_kernel.bind(5);
mPosShader.bind();
mPosShader.uniform("origins", 0);
mPosShader.uniform("posArray", 1);
mPosShader.uniform("velArray", 2);
mPosShader.uniform("user_velxy", 3);
mPosShader.uniform("user_f", 4);
mPosShader.uniform("user_kernel", 5);
mPosShader.uniform("width", (float)VERTICES_X);
mPosShader.uniform("height", (float)ANCHORS_Y);
mPosShader.uniform("kernel_size", kernel_size);
mPosShader.uniform("kernel_sum", kernel_sum);
mPosShader.uniform("vel_correction", vel_correction);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f); glVertex2f(0.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex2f(0.0f, ANCHORS_Y);
glTexCoord2f(1.0f, 1.0f); glVertex2f(VERTICES_X, ANCHORS_Y);
glTexCoord2f(1.0f, 0.0f); glVertex2f(VERTICES_X, 0.0f);
glEnd();
mPosShader.unbind();
super_kernel.unbind(5);
fTexture.unbind(4);
vTexture_xy.unbind(3);
mFbo[(drawFboIndex+1)%FBO_TOTAL].unbindTexture();
mFbo[drawFboIndex].unbindFramebuffer();
drawFboIndex = (drawFboIndex+1)%FBO_TOTAL;
}
You’ll notice some other crap in here, like the user textures. These relate to the next chapter, input.
During draw, a separate vertex shader takes the latest frame buffer and applies the position color buffer to the VBOmeshes.
{
gl::clear(ColorA::black(), true);
glShadeModel(GL_FLAT);
glLineWidth(1.0f);
gl::color(ColorA(1.0f,1.0f,1.0f,1.0f));
mFbo[drawFboIndex].bindTexture(1,1);
mDispShader.bind();
mDispShader.uniform("displacementMap0", 1);
mDispShader.uniform("coordInc", (float)1.0f/(float)(VERTICES_Y-1));
mDispShader.uniform("window", Vec2f(app::getWindowSize()));
mDispShader.uniform("control_points", NUM_CONTROL_POINTS);
draw();
mDispShader.unbind();
mFbo[drawFboIndex].unbindTexture();
}
void draw()
{
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
for (vector<gl::VboMesh>::iterator mesh_iter = mesh_set.begin(); mesh_iter!=mesh_set.end(); mesh_iter++)
{
gl::draw( *mesh_iter );
}
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
for (vector<gl::VboMesh>::iterator mesh_iter = mesh_set.begin(); mesh_iter!=mesh_set.end(); mesh_iter++)
{
gl::draw( *mesh_iter );
}
}
coordInc is probably more commonly known as a step in the Y direction of the texture coordinates.
If you want to jump straight to how Part III – Shaders correlate, you can. But you might want to know how Part II – Input applies first.
