Psychedelic Backgrounds
Journalists and players called the visuals of Radio Flare REDUX “stunning” (Macworld) and “fantastic” (cybereal). This can be partly attributed to the mind-bending, jaw-dropping, eye-popping psychedelic background effects. In this blog post I’ll explain the basics of the tech behind those backgrounds. The magic comes from linking the background effects to the music. But the code that actually draws the graphics is dead simple. Let’s get started.
Setting up shop
All backgrounds are nodes in the scene graph that stores the whole game scene. The backgrounds are flat (2D) and drawn in the background. They are semi-transparent and composed of straight & simple OpenGL. In the game, I use three different kinds of background elements: Texture-based (like the noise in the “Motor” level and the interlacing lines), PointSprite-based (like the rain and pollen in “Revolution Void”), and Vertex-based (all others, most of them based on basic shapes like lines and rectangles). Texture-based effects are the most simple: I just tile a texture over the whole screen and move it a little bit (in sync with the beat). The PointSprite-based ones are based on the Particle code I use. Comparable Open Source-code can be found in the particle class of Cocos2D. In the following paragraphs I will explain how to render Vertex-based psychedelic effects.
All vertices that will be drawn need to be stored in an array. The following code draws a waveform-like visual effect. In REDUX, it can be found in the “Motor” and the “DJ Glow” levels. Here’s a screenshot. The code here draws the striped waveform in the background.
The vertices are stored in an array called _vertices. The number of vertices stored is stored in _numVertices, the absolute number of vertices that fit in the allocated memory is stored in _numElements.
A point is added with the following function:
- (void) addPoint:(SONAVector2D)point {
if (_numVertices < _numElements-1) {
_vertices[_numVertices].x = point.x;
_vertices[_numVertices].y = point.y;
_vertices[_numVertices+1].x = point.x;
_vertices[_numVertices+1].y = -point.y;
if (_numVertices >= 2) {
_indices[_numIndices+0] = _numVertices+0;
_indices[_numIndices+1] = _numVertices+1 - 2;
_indices[_numIndices+2] = _numVertices+0 - 2;
_indices[_numIndices+3] = _numVertices+1 - 2;
_indices[_numIndices+4] = _numVertices+1;
_indices[_numIndices+5] = _numVertices+0;
} else {
_indices[_numIndices+0] = _numVertices+0;
_indices[_numIndices+1] = _numVertices+1;
_indices[_numIndices+2] = _numVertices+0;
_indices[_numIndices+3] = _numVertices+0;
_indices[_numIndices+4] = _numVertices+1;
_indices[_numIndices+5] = _numVertices+0;
}
_numIndices += 6;
_numVertices += 2;
}
}
As you can see there’s a second array called _indices that stores vertex indices to be used in the glDrawElements() call later on. Indices are used because some vertices are used repeatedly in order to draw one continuous stripe. The vertices are initialized with the following function call (gSize is a 2-dimensional grid size that determines how much memory is allocated for this object):
- (void) resetPoints:(SONAGrid)gSize {
// superclass calculates _numElements (=gSize.x * gSize.y)
[super resetPoints:gSize];
// xStride is the stride between the different quads
_xStride = ((float)[_rfrState.elements.sceneManager width])/((float)_numElements/2.f - 2.f);
// allocate memory for the vertices and the indices
_vertices = (SONAVector2D *)malloc(_numElements * 2 * sizeof(SONAVector2D));
_indices = (GLushort *)malloc(_numElements * 6 * sizeof(GLushort));
// add the points. one call to addPoint adds a whole square (ugly!)
for (int i=0; i<_numElements/2.f; i++) {
[self addPoint:makeVector2D(((float)i) * _xStride, 25.0f+SONA_RANDOM_FLOAT()*50.0f)];
}
}
The squares (the wave form is composed of cubes) are moved to the left at each beat of the music. This is achieved by copying over the y vertices. The leftmost square is moved all the way to the right.
- (void) moveSquares {
_vertices[_numVertices-2].y = _vertices[0].y;
_vertices[_numVertices-1].y = _vertices[1].y;
for (int i=0; i<_numVertices-2; i++) {
_vertices[i ].y = _vertices[i+2 ].y;
}
}
The following piece of code is the callback from the sound engine. The superclass administers some variables like _updateCountdown, a countdown that is set to 1 on each beat and linearly decreases to 0 over the duration of one beat.
- (void) onBeat:(unsigned int)beat ofBar:(unsigned int)bar {
[super onBeat:beat ofBar:bar];
[self moveSquares];
}
The last — and most important — function is the drawing function. My default OpenGL state has enabled vertex and texture coordinate arrays. I need to disable texturing in order to draw plain shapes. I also store the blending settings and color mode before proceeding to the actual drawing and reset it afterwards. Before drawing, I translate vertically to the centre of the screen and translate horizontally by a fraction of _updateCountdown and _xStride to smoothly scroll the whole wave form. Then I draw the stripe in 3 passes. Each pass is scaled differently on the y-axis and has a different alpha blending. The actual vertices are only transmitted once with the glVertexPointer() call. Subsequent passes draw the same vertices and indices again.
- (void) draw {
// disable texture coordinate arrays and texturing
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisable(GL_TEXTURE_2D);
// store blending settings
int blendSrc, blendDst;
glGetIntegerv(GL_BLEND_DST, &blendDst);
glGetIntegerv(GL_BLEND_SRC, &blendSrc);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
// store color mode settings
int colorMode;
glGetTexEnviv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, &colorMode);
glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
// translate to the vertical centre of the screen before drawing.
glTranslatef(_xStride*_updateCountdown/2.0f - _xStride, [_rfrState.elements.sceneManager height]/2.f, 0.f);
// there are three drawing passes with different scaling on the y-axis and different transparency
glScalef(1.f, _height * (1.0f+_updateCountdown/2.0f), 0.0f);
glColor4f(_blendColour.r, _blendColour.g, _blendColour.b, _blendColour.a/4.5f);
glVertexPointer(2, GL_FLOAT, 0, _vertices);
glDrawElements(GL_TRIANGLES, _numIndices, GL_UNSIGNED_SHORT, _indices);
glScalef(1.f, _height * (1.0f+_updateCountdown/2.0f)*_factor, 0.0f);
glColor4f(_blendColour.r, _blendColour.g, _blendColour.b, _blendColour.a/8.5f);
glDrawElements(GL_TRIANGLES, _numIndices, GL_UNSIGNED_SHORT, _indices);
glScalef(1.0f, _height * (1.0f+_updateCountdown/3.0f)*_factor, 0.0f);
glColor4f(_blendColour.r, _blendColour.g, _blendColour.b, _blendColour.a/16.5f);
glDrawElements(GL_TRIANGLES, _numIndices, GL_UNSIGNED_SHORT, _indices);
// restore color mode
glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, colorMode);
// restore blend state
glBlendFunc( blendSrc, blendDst );
// re-enabled texture coordinates
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnable(GL_TEXTURE_2D);
}
The other backgrounds are coded slightly different but basically similar. All of them are based on arrays of vertices. I will share some more interesting pieces of code in the future, but that's it for now.









