Instead of triangles, we want to render a "monkey head". To avoid building the vertices for such a complex model yourself in the source code, we need a model loader.
Here we will use a simple .obj file loader i have written. Its far from perfect, but it works for most model files.
The .obj file format is a text file you can open with every text editor, like notepad.
It regularly has an additional file to store "Materials", the so-called "Material Library".
We wont use it this time, but in comming tutorials.
Complete Source Code:
Use the same source files from the previous Tutorial 00: System Interface.
OBJ.h
#pragma once
#include "Math.h"
#include <map>
#include <vector>
#include <string>
class OBJ
{
public:
OBJ(const std::string& obj, const std::string& mtl_basepath = "");
~OBJ();
std::string GetErrorString() const;
struct Material {
std::string Name;
vec3 Ka, Kd, Ks;
float Ns{ 0 }, d{ 0 };
struct {
std::string Ka, Kd, Ks, Ns, d, bump;
} map;
unsigned int illum{ 0 };
};
struct FacePoint {
unsigned int Index_v{ 0 }; // 0 = invalid value
unsigned int Index_vt{ 0 };
unsigned int Index_vn{ 0 };
};
struct Face {
std::vector<FacePoint> Points;
};
struct Group {
std::string Name;
std::map<
std::string, // material name
std::map<unsigned int, std::vector<Face>> // face arrays mapped to their number of points (sorted triangles, quads, etc ...)
> Faces;
};
struct Object {
std::string Name;
std::map<std::string, Group> Groups;
};
std::map<std::string, Object> Objects;
std::vector<vec3> Positions;
std::vector<vec2> TexCoords;
std::vector<vec3> Normals;
std::map<std::string, Material> MaterialLibrary;
private:
std::string m_mtl_filename{ "" };
std::string m_object_current{ "" };
std::string m_group_current{ "" };
std::string m_material_current{ "" };
std::string m_errors{ "" };
void ParseLine(std::stringstream& stream);
};
OBJ.cpp
#include "OBJ.h"
#include <iostream>
#include <fstream>
#include <sstream>
OBJ::OBJ(const std::string& obj, const std::string& mtl_basepath)
{
// open .obj file
std::fstream f(obj, std::ios::in);
if (!f.good())
{
m_errors.append("cannot open obj data file: " + obj + "\n");
return;
}
// read .obj file
while (f.good())
{
std::string line;
std::getline(f, line);
ParseLine(std::stringstream(line));
}
f.close();
// check if all indices are valid:
for (auto& objectdata : Objects)
{
for (auto& groupdata : objectdata.second.Groups)
{
for (auto& facedata : groupdata.second.Faces)
{
for (auto& facemap : facedata.second)
{
for (auto& face : facemap.second)
{
for (auto& point : face.Points)
{
if (point.Index_v == 0 || point.Index_v > Positions.size())
m_errors.append("error face index_v out of range\n");
if (point.Index_vt > TexCoords.size())
m_errors.append("error face index_vt out of range\n");
if (point.Index_vn > Normals.size())
m_errors.append("error face index_vn out of range\n");
}
}
}
}
}
}
// open .mtl file
std::string filename = mtl_basepath + m_mtl_filename;
f.open(filename, std::ios::in);
if (!f.good())
{
m_errors.append("cannot open material library file: " + filename + "\n");
return;
}
// read .mtl file
m_material_current = "";
while (f.good())
{
std::string line;
std::getline(f, line);
ParseLine(std::stringstream(line));
}
f.close();
// check for missing materials:
for (auto& objectdata : Objects)
{
for (auto& groupdata : objectdata.second.Groups)
{
for (auto& facedata : groupdata.second.Faces)
{
auto it = MaterialLibrary.find(facedata.first);
if (it == MaterialLibrary.end())
m_errors.append("missing material data: " + facedata.first + "\n");
}
}
}
}
OBJ::~OBJ()
{}
void OBJ::ParseLine(std::stringstream& stream)
{
std::string token;
stream >> token;
if (token.empty()) return;
if (token[0] == '#') return;
if (token == "o")
{
stream >> m_object_current;
Objects[m_object_current].Name = m_object_current;
}
else if (token == "g")
{
stream >> m_group_current;
Objects[m_object_current].Groups[m_group_current].Name = m_object_current;
}
else if (token == "v")
{
vec3 pos;
stream >> pos.x >> pos.y >> pos.z;
Positions.push_back(pos);
}
else if (token == "vt")
{
vec2 tex;
stream >> tex.x >> tex.y;
TexCoords.push_back(tex);
}
else if (token == "vn")
{
vec3 nor;
stream >> nor.x >> nor.y >> nor.z;
Normals.push_back(nor);
}
else if (token == "f")
{
Face face;
while (stream >> token)
{
FacePoint point;
std::size_t l1 = token.find('\/');
if (l1 == std::string::npos)
{
// only position
point.Index_v = std::stoi(token);
//point.Index_vt = 0;
//point.Index_vn = 0;
face.Points.push_back(point);
}
else
{
std::size_t l2 = token.find_first_of('\/', l1 + 1);
if (l2 == std::string::npos)
{
// position + texcoords
point.Index_v = std::stoi(token.substr(0, l1));
point.Index_vt = std::stoi(token.substr(l1 + 1, token.size() - l1 - 1));
//point.Index_vn = 0;
face.Points.push_back(point);
}
else if (l2 - l1 == 1)
{
// position + NO texcoords + normal
point.Index_v = std::stoi(token.substr(0, l1));
//point.Index_vt = 0;
point.Index_vn = std::stoi(token.substr(l2 + 1, token.size() - l2 - 1));
face.Points.push_back(point);
}
else
{
// position + texcoords + normal
point.Index_v = std::stoi(token.substr(0, l1));
point.Index_vt = std::stoi(token.substr(l1 + 1, l2 - l1 - 1));
point.Index_vn = std::stoi(token.substr(l2 + 1, token.size() - l2 - 1));
face.Points.push_back(point);
}
}
}
Objects[m_object_current].Groups[m_group_current].Faces[m_material_current][face.Points.size()].push_back(face);
}
else if (token == "usemtl")
{
stream >> m_material_current;
}
else if (token == "mtllib")
{
stream >> m_mtl_filename;
}
else if (token == "newmtl")
{
stream >> m_material_current;
MaterialLibrary[m_material_current].Name = m_material_current;
}
else if (token == "Ka")
{
vec3 Ka;
stream >> Ka.x >> Ka.y >> Ka.z;
MaterialLibrary[m_material_current].Ka = Ka;
}
else if (token == "Kd")
{
vec3 Kd;
stream >> Kd.x >> Kd.y >> Kd.z;
MaterialLibrary[m_material_current].Kd = Kd;
}
else if (token == "Ks")
{
vec3 Ks;
stream >> Ks.x >> Ks.y >> Ks.z;
MaterialLibrary[m_material_current].Ks = Ks;
}
else if (token == "Ns")
{
stream >> MaterialLibrary[m_material_current].Ns;
}
else if (token == "d")
{
stream >> MaterialLibrary[m_material_current].d;
}
else if (token == "Tr")
{
float Tr = 0;
stream >> Tr;
MaterialLibrary[m_material_current].d = 1 - Tr;
}
else if (token == "map_Ka")
{
stream >> MaterialLibrary[m_material_current].map.Ka;
}
else if (token == "map_Kd")
{
stream >> MaterialLibrary[m_material_current].map.Kd;
}
else if (token == "map_Ks")
{
stream >> MaterialLibrary[m_material_current].map.Ks;
}
else if (token == "map_Ns")
{
stream >> MaterialLibrary[m_material_current].map.Ns;
}
else if (token == "map_d")
{
stream >> MaterialLibrary[m_material_current].map.d;
}
else if (token == "map_bump" || token == "bump")
{
stream >> MaterialLibrary[m_material_current].map.bump;
}
else if (token == "illum")
{
stream >> MaterialLibrary[m_material_current].illum;
}
}
std::string OBJ::GetErrorString() const
{
return m_errors;
}
Renderer_OpenGL.h
#pragma once
#include "Renderer.h"
#include <GL/glew.h>
class Renderer_OpenGL : public Renderer
{
public:
Renderer_OpenGL(unsigned int width, unsigned int height);
virtual ~Renderer_OpenGL();
virtual void Render(const Scene& scene);
protected:
unsigned int m_width{ 0 }, m_height{ 0 };
unsigned int m_program{ 0 };
unsigned int m_vertexarray{ 0 };
unsigned int m_vertexbuffer{ 0 };
unsigned int m_vertexcount{ 0 };
};
Renderer_OpenGL.cpp
#include "Renderer_OpenGL.h"
#include "Shader.h"
#include "Model.h"
#include "OBJ.h"
#include <iostream>
void CheckForGLError()
{
GLenum error;
while ((error = glGetError()) != GL_NO_ERROR)
{
std::cout << "ERROR: (Renderer_OpenGL) \t";
if (error == GL_INVALID_ENUM)
std::cout << "GL_INVALID_ENUM";
if (error == GL_INVALID_VALUE)
std::cout << "GL_INVALID_VALUE";
if (error == GL_INVALID_OPERATION)
std::cout << "GL_INVALID_OPERATION";
if (error == GL_INVALID_FRAMEBUFFER_OPERATION)
std::cout << "GL_INVALID_FRAMEBUFFER_OPERATION";
if (error == GL_OUT_OF_MEMORY)
std::cout << "GL_OUT_OF_MEMORY";
if (error == GL_STACK_UNDERFLOW)
std::cout << "GL_STACK_UNDERFLOW";
if (error == GL_STACK_OVERFLOW)
std::cout << "GL_STACK_OVERFLOW";
std::cout << (char)7 << std::endl; /*play sound*/
std::cin.get();
}
}
Renderer_OpenGL::Renderer_OpenGL(unsigned int width, unsigned int height)
{
// settings
// ----------------------------------------------------------------------------------------------------------
glEnable(GL_DEPTH_TEST); // 3D
glEnable(GL_CULL_FACE); // dont render backward faces
// ----------------------------------------------------------------------------------------------------------
// setup program
// ----------------------------------------------------------------------------------------------------------
m_program = glCreateProgram();
unsigned int vertexshader = glCreateShader(GL_VERTEX_SHADER);
unsigned int fragmentshader = glCreateShader(GL_FRAGMENT_SHADER);
CompileShader(vertexshader, LoadTextFile("shader.vs"));
CompileShader(fragmentshader, LoadTextFile("shader.fs"));
LinkProgram(m_program, { vertexshader , fragmentshader });
glDeleteShader(vertexshader);
glDeleteShader(fragmentshader);
// ----------------------------------------------------------------------------------------------------------
// setup vertexarray
// ----------------------------------------------------------------------------------------------------------
glGenVertexArrays(1, &m_vertexarray);
glGenBuffers(1, &m_vertexbuffer);
glBindVertexArray(m_vertexarray);
glBindBuffer(GL_ARRAY_BUFFER, m_vertexbuffer);
glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(Vertex), (void*)(sizeof(float) * 0));
glVertexAttribPointer(1, 4, GL_FLOAT, false, sizeof(Vertex), (void*)(sizeof(float) * 3));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindVertexArray(0);
// ----------------------------------------------------------------------------------------------------------
// setup vertex buffer
// ----------------------------------------------------------------------------------------------------------
OBJ model("monkey.obj", "");
if (!model.GetErrorString().empty())
std::cout << model.GetErrorString() << std::endl;
std::vector<Vertex> vertices;
for (auto& object : model.Objects)
{
for (auto& groupmember : object.second.Groups)
{
for (auto& facemap : groupmember.second.Faces)
{
for (auto& facetype : facemap.second)
{
// only process triangles:
if (facetype.first == 3)
{
for (auto& face : facetype.second)
{
vec3 position, normal;
position = model.Positions[face.Points[0].Index_v - 1];
vertices.push_back({ position,{ 1, 0, 0, 1 } });
position = model.Positions[face.Points[1].Index_v - 1];
vertices.push_back({ position,{ 0, 1, 0, 1 } });
position = model.Positions[face.Points[2].Index_v - 1];
vertices.push_back({ position,{ 0, 0, 1, 1 } });
}
}
}
}
}
}
if (vertices.size() == 0)
{
vertices.push_back({ { 0, 0, 0 },{ 1, 0, 0, 1 } });
vertices.push_back({ { 1, 0, 0 },{ 0, 1, 0, 1 } });
vertices.push_back({ { 0, 1, 0 },{ 0, 0, 1, 1 } });
}
m_vertexcount = vertices.size();
glBindBuffer(GL_ARRAY_BUFFER, m_vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * vertices.size(), vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// ----------------------------------------------------------------------------------------------------------
}
Renderer_OpenGL::~Renderer_OpenGL()
{
glDeleteProgram(m_program);
glDeleteVertexArrays(1, &m_vertexarray);
glDeleteBuffers(1, &m_vertexbuffer);
}
void Renderer_OpenGL::Render(const Scene & scene)
{
glClearColor(0, 0.5, 0, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(m_program);
glBindVertexArray(m_vertexarray);
// set camera
glUniformMatrix4fv(glGetUniformLocation(m_program, "View"), 1, false, value_ptr(scene.Camera.View()));
glUniformMatrix4fv(glGetUniformLocation(m_program, "Projection"), 1, false, value_ptr(perspective(scene.Camera.FoV, 1.33f, scene.Camera.ZNear, scene.Camera.ZFar)));
// render objects in scene
for (auto& object : scene.Objects)
{
glUniformMatrix4fv(glGetUniformLocation(m_program, "Model"), 1, false, value_ptr(object.ModelMatrix()));
glDrawArrays(GL_TRIANGLES, 0, m_vertexcount);
}
glBindVertexArray(0);
glUseProgram(0);
// check for errors
CheckForGLError();
}