ROAD RUNNER

OpenGL     C++     1 month    2020

ABOUT

ROADRUNNER is a racing game set in a surreal world. The player drives a motorcycle along a topsy-turvy route between painterly hills and floating pavilions, dodging bombs and scoring points along the way.

OPENGL implemented for lighting, primitives, textures, and animations. The 3D racing track and basic primitives are programmatically generated. Fragment, geometry, and vertex shaders are programmed for advanced rendering techniques that include but are not limited to geometry shaders for animations, instanced rendering, multi-texturing, and real-time rendering for colour filters using frame buffer objects. 

Colour Filters, rendered real-time using OpenGL shaders and Frame Buffer Objects

An overview of the track can be seen on the left. The game incorporates the following meshes, primitives, and game objects:

  • Player: motorcycle mesh controllable through keyboard input
  • Track: 3D spline, programmatically generated using triangle primitives
  • Heightmap: 3D vertex mesh generated using a 2D image
  • Meshes: floating pavilions, vegetation, loops
  • Primitives: trapezohedron, urchin shape, sphere

Additionally, the scene has two lighting modes: day and night. Two static and two dynamic spotlights are included in the game. During night, the spotlights attached to the player character turn on and light up the scene before it. 

Different camera viewing is provided and is possible to switch during gameplay: 3rd person, side view, top view, and freeview (only for debugging). A minimap option is also provided on the top-left of the screen upon clicking ‘M’. 

The player motorcycle twists, leans, and turns on 3D track using quaternion rotations. The camera follows the player according to the TNB frame of the track. 

Note that the images above are taken during ‘night mode’ for the game scene. 

Multitexturing is applied on the heightmap via an OpenGL fragment shader:

code snippet : multi-texturing

else if (renderTerrain) {
vec4 vTexColour0 = texture(sampler0, vTexCoord);
vec4 vTexColour1 = texture(sampler1, vTexCoord);
vec4 vTexColour2 = texture(sampler2, vTexCoord);
vec4 vTexColour3 = texture(sampler3, vTexCoord);

float f = clamp(3*(worldPosition.y – fMinHeight) /
(fMaxHeight – fMinHeight), 0, 3);

vec4 vTexColour;
if (f < 1)
vTexColour = mix(vTexColour0, vTexColour1, f);
else if (f < 2)
vTexColour = mix(vTexColour1, vTexColour2,
f – 1.0);
else
vTexColour = mix(vTexColour2, vTexColour3,
f – 2.0);
vOutputColour = vTexColour*vec4(vColour, 1.0f);
}

An exploding effect is applied in the geometry shader for pickups and bombs to animate upon proximity to player:

code snippet : explode

# version 400

in vec3 vColourPass[];out vec3 vColour;
in vec2 vTexCoordPass[]; out vec2 vTexCoord;

layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;

uniform bool bExplodeObject;
uniform float explodeFactor;

void main() {
float localExplode = bExplodeObject?
explodeFactor : 0.0;

vec3 e1 = gl_in[1].gl_Position.xyz
– gl_in[0].gl_Position.xyz;
vec3 e2 = gl_in[2].gl_Position.xyz
– gl_in[0].gl_Position.xyz;
vec3 n = normalize(cross(e1, e2));

for (int i = 0; i < 3; i++) {
vec4 explodedPos = gl_in[i].gl_Position
+ vec4(localExplode * n, 0);
gl_Position = matrices.projMatrix
* matrices.modelViewMatrix * explodedPos;
vColour = vColourPass[i];
vTexCoord = vTexCoordPass[i];
EmitVertex();
}
EndPrimitive();
}

Additionally both a bounce animation and toon shader is applied for the spheres:

code snippet : bounce

void main() {
vec3 p = inPosition;
p.y += sin(p.z + t);
p.z += sin(p.x + t);
gl_Position = vec4(p, 1.0);

}

code snippet : toon

uniform int levels;
in vec3 vColour;
uniform float changeColour;

void main() {
quantisedColour = floor(vColour * levels) / levels;
vOutputColour = vec4(vec3(
quantisedColour.r + changeColour,
quantisedColour.g,
quantisedColour.b – changeColour/2),
0.6f) ;
}

Instanced rending is applied for vegetation to create a forest. 

code snippet : instanced rendering

void COpenAssetImportMesh::
RenderInstances(int count)
{
glBindVertexArray(m_vao);
for (unsigned int i = 0;
i < m_Entries.size(); i++)
{
//do some preparations
glDrawElementsInstanced(
GL_TRIANGLES,
m_Entries[i]. NumIndices,
GL_UNSIGNED_INT,
0, count);
}
}

pTreeShader->UseProgram();
modelViewMatrixStack.Push();
m_pOakMesh->RenderInstances(30);
modelViewMatrixStack.Pop();