This is the grand finale of my OpenGL classwork and the project I had the most fun working on. This project was fun because I had to do more problem solving and engineering to really get it to work the way I wanted. In my class on GLSL shaders I tried to create a scene from one of my all time favorite games Metroid Prime. I was happy with the results of that project but since I had all the shaders and models I decided I would build on them and create an interactive scene.
Check out the results:
I had a few challenges to making this project work out but they gave me a really nice taste of what problems an engineer working on a game engine might face. The first challenge was in refactoring my OBJ loader code and getting it to play nice with vertex buffers instead of display lists. We were not required to use vertex buffers for this project but since we had just learned about them and our instructor emphasized that they were the way to go I decided that it would be better experience to me to actually use them. Getting the layout right was a bit tricky at first but once that was settled I found that vertex buffers and so much nicer to use than display lists. To contain everything better I migrated my previous code into a class called Mesh which is responsible for loading the mesh and displaying the mesh. This allowed me to easily use the same mesh in multiple places with only one instance of the mesh in memory.
The next challenge I had was in getting the camera to move with keyboard input. Since the camera in this project was mobile I needed to keep track of its state to be able to move it around. I could have simply created a struct to contain the state, but in the interest of reusability I created a class to handle the camera control. This class allowed me to move the camera by simply calling the appropriate functions which also allowed me to only move the camera components that had changed. For instance I could rotate the camera without having to alter or set the position. It was then simple to set the camera parameters in the pipeline by retrieving the right state values from the camera class.
The last challenge was to create a reusable class for the few particles that I had in the scene. This one was straight forward and like the camera controller was basically just a state container. Doing the particles in a class allowed for better usability because the class itself was responsible to doing the physics calculations to advance the particles instead of relying on an outside function. This also allowed me to do some clever things like reuse particles by having them essentially "restart" when they reached the end of their life time. With enough particles running at random speeds its really difficult to spot a pattern in the particles without having to generate new parameters every time a particle dies.
This post is getting pretty long, but here is the interesting code bits from this project:
This is the gist of the Mesh class, but the actual file parsing is really long:
//Mesh.h
class Mesh
{
private:
glm::vec3 importScale = glm::vec3(1, 1, 1);
glm::vec3 importTransform = glm::vec3(0, 0, 0);
int verts = 0;
int dataSize = 0;
int facesCount = 0;
bool facesLoaded = false;
GLuint vertBuffer;
std::string modelName = "";
public:
Mesh();
~Mesh();
void Load(std::string objFileName);
void Unload();
void Draw();
};
//Mesh.cpp
void Mesh::Draw() {
GLuint buff = this->vertBuffer;
glBindBuffer(GL_ARRAY_BUFFER, buff);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glVertexPointer(3, GL_FLOAT, 8 * sizeof(GLfloat), 0);
glNormalPointer(GL_FLOAT, 8 * sizeof(GLfloat), (GLuint*)(3 * sizeof(GLfloat)));
glTexCoordPointer(2, GL_FLOAT, 8 * sizeof(GLfloat), (GLuint*)(6 * sizeof(GLfloat)));
glDrawArrays(GL_TRIANGLES, 0, 6 * this->verts);
}
void Mesh::Load(std::string objFileName) {
//The actual OBJ file parsing would go here but its way too long for this post
this->verts = triCount * 3;
GLuint buff;
glGenBuffers(1, &buff);
this->vertBuffer = buff;
glBindBuffer(GL_ARRAY_BUFFER, buff);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * dataLen, data, GL_STATIC_DRAW);
free(data);
}
Here is the camera controller code:
//camera.h
class Camera
{
public:
Camera();
~Camera();
void MoveTo(glm::vec3 position); //Move the camera to the new position
void MoveTo(glm::vec4 position); //Move the camera to the new position
void RotateX(float angle); //rotate the camera in radians around x
void RotateY(float angle);
void LookAt(glm::vec3 point);
void Reset(); //reset the camera back to zero looking in +x direction, the OpenGL default
void SetOffset(glm::vec4 newOffset);
void SetOffset(glm::vec3 newOffset);
glm::vec4 GetPos(); //get the position
glm::vec4 GetLookAt(); //get the adjusted look at
glm::vec4 GetUp(); //get the up direction
float getAngX();
float getAngY();
private:
glm::mat4 position = glm::mat4();
glm::mat4 lookAt = glm::mat4();
glm::mat4 up = glm::mat4();
glm::mat4 rotation = glm::mat4();
glm::mat4 rotation_x = glm::mat4();
glm::mat4 rotation_y = glm::mat4();
glm::mat4 offset = glm::mat4(); //offset is used to move the camera relative to its position
float angx = 0;
float angy = 0;
};
//camera.cpp
Camera::Camera()
{
this->lookAt = glm::translate(glm::mat4(), glm::vec3(10, 0, 0));
}
Camera::~Camera()
{
}
glm::vec4 Camera::GetPos() {
return this->position * this->offset * glm::vec4(0, 0, 0, 1);
}
glm::vec4 Camera::GetLookAt() {
//effect: 2nd 1st
//return this->position * this->offset * this->lookAt * glm::vec4(10, 0, 0, 1);//rotate first (inner) then translate (outer)
return this->position * this->offset * this->rotation_x * this->rotation_y * glm::vec4(10, 0, 0, 1);//rotate first (inner) then translate (outer)
}
glm::vec4 Camera::GetUp() {
return this->rotation_y * glm::vec4(0, 1, 0, 1); //we only want the rotation of the y component for the up
}
void Camera::MoveTo(glm::vec3 newPosition) { //Move the camera to the new position
this->position = glm::translate(glm::mat4(), newPosition);
}
void Camera::MoveTo(glm::vec4 newPosition) { //Move the camera to the new position
this->position = glm::translate(glm::mat4(), glm::vec3(newPosition.x, newPosition.y, newPosition.z));
}
void Camera::LookAt(glm::vec3 point) {
this->lookAt = glm::translate(glm::mat4(), point);
}
void Camera::RotateX(float angle) { //rotate the camera in radians around the axis
this->angx += angle;
this->rotation_x = glm::rotate(this->rotation_x, angle, glm::vec3(0, 1, 0));
}
void Camera::RotateY(float angle) { //rotate the camera in radians around the axis
this->angy += angle;
this->rotation_y = glm::rotate(this->rotation_y, angle, glm::vec3(0, 0, 1));
}
float Camera::getAngX() {
return this->angx;
}
float Camera::getAngY() {
return this->angy;
}
void Camera::Reset() {
this->lookAt = glm::translate(glm::mat4(), glm::vec3(10, 0, 0));
this->position = glm::mat4();
this->offset = glm::mat4();
}
void Camera::SetOffset(glm::vec4 newOffset) {
this->offset = glm::translate(glm::mat4(), glm::vec3(newOffset.x, newOffset.y, newOffset.z));
}
void Camera::SetOffset(glm::vec3 newOffset) {
this->offset = glm::translate(glm::mat4(), newOffset);
}
Here is the code for the Particle class:
//particle.h
class Particle
{
public:
Particle();
Particle(Mesh *mesh, GLSLProgram *shaderProgram);
Particle(Mesh *mesh, GLSLProgram *shaderProgram, glm::vec3 position, glm::vec3 velocity);
~Particle();
void Draw();
void Update(float dt);
void Reset();
void SetTimes(float birthTime, float deathTime);
bool Dead();
bool resetOnDeath = true;
private:
Mesh *m;
glm::vec3 position0 = glm::vec3(0, 0, 0);
glm::vec3 position = glm::vec3(0, 0, 0);
glm::mat4 currentTransformation = glm::mat4();
glm::vec3 velocity = glm::vec3(0, 0, 0);
float birthTime = 0;
float deathTime = 999;
float currentTime = 0;
GLSLProgram *shader;
bool isDead = false;
bool isValid = false;
};
//particle.cpp
//GLSLProgram is a class used to load and use shader pipelines.
//I did not write it so I'm not including it
Particle::Particle(Mesh *mesh, GLSLProgram *shaderProgram)
{
this->m = mesh;
this->shader = shaderProgram;
this->isValid = true;
}
Particle::Particle(Mesh *mesh, GLSLProgram *shaderProgram, glm::vec3 startingPosition, glm::vec3 startingVelocity) {
this->m = mesh;
this->shader = shaderProgram;
this->position0 = startingPosition;
this->velocity = startingVelocity;
this->isValid = true;
}
Particle::~Particle()
{
//this->m->~Mesh(); since we're passing by reference, this would erase the mesh for any other class that would use it
//big no no
//Same goes for the shader, so I guess we just won't do anything here
}
void Particle::Draw() {
//transform first,
//but then draw
if (!this->isValid) return;
if (this->currentTime > this->birthTime && !this->isDead) { //Need to be sure the particle isnt dead here
this->currentTransformation = glm::translate(glm::mat4(), this->position0) * glm::translate(glm::mat4(), this->position);
//needed to scale the particles down. easier to do it here than get back into blender.
//since the particles are moving anyways there's also no cost hit to do it here instead of in the base model
//this->currentTransformation = this->currentTransformation * glm::scale(glm::mat4(), glm::vec3(0.5, 0.5, 0.5));
//this is how to enable the pipeline and set the uniform variables
this->shader->Use();
this->shader->SetUniformVariable("Transform", this->currentTransformation);
this->m->Draw();
this->shader->Use(0);
}
}
void Particle::Update(float dt) {
if (!this->isValid) return;
this->currentTime += dt;
//if we have died, then reset
if (this->currentTime > (this->deathTime + this->birthTime)) {
if (this->resetOnDeath) {
this->Reset();
return;
}
else {
this->isDead = true;
return;
}
}
//if we are born, update
if (this->currentTime > this->birthTime) {
float time = this->currentTime - this->birthTime;
glm::vec3 newPosition = this->position0 + (time * this->velocity);
this->position = newPosition;
}
}
void Particle::Reset() {
this->currentTime = 0;
this->position = this->position0;
this->currentTransformation = glm::mat4();
this->isDead = false;
}
void Particle::SetTimes(float newBirthTime, float newDeathTime) {
if (!this->isValid) return;
this->birthTime = newBirthTime;
this->deathTime = newDeathTime;
}
bool Particle::Dead() {
if (!this->isValid) return false;
else return this->isDead;
}