I've been trying to make my own basic .obj (Wavefront) renderer using the OpenGL 3.x core profile. I'm using the OpenGL SuperBible 5th ed. and the Swiftless tutorials as reference material.
The geometry appears to load correctly, so now I'm trying to get an ADS Phong lighting model to work, but something is screwy, and I think it has something to do with either my OpenGL calls or else maybe the way I'm loading my normals, but I can't seem to figure out how to fix it. (It could be some other problem too I guess, since I'm not an expert in OpenGL).
When rendering a simple cube, it almost looks right, but there's a weird point of light on one side:
When rendering a sphere, the lighting shows up with "veins":
Something is obviously very wrong with my code. Here are the sections of code which I think might be relevant. I'll be happy to post more/all of the code if necessary... let me know if I should post more.
file: objFileRenderer.cpp
#include "objFileRenderer.hpp"
namespace def
{
objFileRenderer::objFileRenderer(const char* fileToRender, float objScale)
{
windowWidth = 800;
windowHeight = 600;
settings.majorVersion = 3;
settings.minorVersion = 1;
app = NULL;
shader = NULL;
vaoID = NULL;
vboID = NULL;
iboID = NULL;
vaoID = new unsigned int[1];
vboID = new unsigned int[3];
iboID = new unsigned int[2];
rotSpeed = 50.0f;
rot = 0.0f;
initSFML();
initOpenGL();
std::string objFilename(fileToRender);
std::vector< float > vertices;
std::vector< unsigned short > indices;
std::vector< float > normals;
std::vector< unsigned short > normalIndices;
loadObj(objFilename, vertices, indices, normals, normalIndices);
std::vector< float > colorA;
std::vector< float > colorD;
std::vector< float > colorS;
loadMtl(objFilename, colorA, colorD, colorS);
float* vertexArray = NULL;
int numVertices = 0;
unsigned short* indexArray = NULL;
int numFaces = 0;
vertexArray = vertexVectorToVertexArray(vertices, numVertices);
indexArray = indexVectorToIndexArray(indices, numFaces);
float* colorArrayA = NULL;
float* colorArrayD = NULL;
float* colorArrayS = NULL;
int numColoredObjects = 0;
colorVectorsToColorArrays(colorA, colorD, colorS, colorArrayA, colorArrayD, colorArrayS, numColoredObjects);
float* normalArray = NULL;
unsigned short* normalIndicesArray = NULL;
int numNormals = 0;
int numNormalIndices = 0;
normalVectorsToNormalArrays(normals, normalIndices, normalArray, normalIndicesArray, numNormals, numNormalIndices);
setupScene();
putArraysIntoVAO(vertexArray, numVertices, indexArray, numFaces, normalArray, numNormals, normalIndicesArray, numNormalIndices);
mainLoop(numVertices, numFaces, colorArrayA, colorArrayD, colorArrayD, normalArray, objScale);
delete [] vertexArray;
delete [] indexArray;
delete [] colorArrayA;
delete [] colorArrayD;
delete [] colorArrayS;
delete [] normalArray;
delete [] normalIndicesArray;
}
objFileRenderer::~objFileRenderer()
{
shutdownSFML();
}
void objFileRenderer::loadObj(std::string& objFilename, std::vector< float >& vertices, std::vector< unsigned short >& indices, std::vector< float >& normals, std::vector< unsigned short >& normalIndices)
{
std::ifstream objFile(objFilename.c_str());
if (!objFile.is_open())
{
std::cerr << "Error: unable to open .obj file: " << objFilename << std::endl;
exit(1);
}
std::string line;
while (objFile.good())
{
getline(objFile, line);
// vertices
if (line[0] == 'v' && line[1] == ' ') // if line in .obj file contains vertices
{
std::vector< std::string > tmpStrVerts;
std::string subline;
subline = line.substr(2);
boost::split(tmpStrVerts, subline, boost::is_any_of("\t "));
std::vector< std::string >::iterator it;
for (it = tmpStrVerts.begin(); it != tmpStrVerts.end(); it++)
{
float vertex;
std::stringstream ss;
ss << *it;
ss >> vertex;
vertices.push_back(vertex);
}
}
// normals
else if (line[0] == 'v' && line[1] == 'n')
{
std::vector< std::string > tmpStrNorms;
std::string subline;
subline = line.substr(3);
boost::split(tmpStrNorms, subline, boost::is_any_of("\t "));
std::vector< std::string >::iterator it;
for (it = tmpStrNorms.begin(); it != tmpStrNorms.end(); it++)
{
float normal;
std::stringstream ss;
ss << *it;
ss >> normal;
normals.push_back(normal);
//std::cout << normal << std::endl;
}
}
// indices and normalIndices
else if (line[0] == 'f' && line[1] == ' ') // else if line in .obj file contains indices
{
std::vector< std::string > tmpStrIndices;
std::string subline;
subline = line.substr(2);
// indices
boost::split(tmpStrIndices, subline, boost::is_any_of("\t "));
std::vector< std::string >::iterator it;
for (it = tmpStrIndices.begin(); it != tmpStrIndices.end(); it++)
{
unsigned short index;
std::stringstream ss;
ss << *it;
ss >> index;
indices.push_back(index);
}
// normalIndices
boost::split(tmpStrIndices, subline, boost::is_any_of("/"));
int count = 0;
std::vector< std::string >::iterator it2;
for (it2 = tmpStrIndices.begin(); it2 != tmpStrIndices.end(); it2++)
{
if (count == 2)
{
unsigned short index;
std::stringstream ss;
ss << *it2;
ss >> index;
normalIndices.push_back(index);
count = 0;
}
count++;
}
}
}
objFile.close();
return;
}
void objFileRenderer::loadMtl(std::string& objFilename, std::vector< float >& colorA, std::vector< float >& colorD, std::vector< float >& colorS)
{
int extpos = objFilename.find('.');
std::string mtlFilename = objFilename.substr(0, extpos+1) + "mtl";
std::ifstream mtlFile(mtlFilename.c_str());
if (!mtlFile.is_open())
{
std::cerr << "Error: unable to open .mtl file: " << mtlFilename << std::endl;
exit(1);
}
std::string line;
while (mtlFile.good())
{
getline(mtlFile, line);
if (line[0] == 'K' && line[1] == 'a')
{
std::vector< std::string > tmpStrColorA;
std::string subline;
subline = line.substr(3);
boost::split(tmpStrColorA, subline, boost::is_any_of("\t "));
std::vector< std::string >::iterator it;
for (it = tmpStrColorA.begin(); it != tmpStrColorA.end(); it++)
{
float rgbValue;
std::stringstream ss;
ss << *it;
ss >> rgbValue;
colorA.push_back(rgbValue);
}
}
if (line[0] == 'K' && line[1] == 'd')
{
std::vector< std::string > tmpStrColorD;
std::string subline;
subline = line.substr(3);
boost::split(tmpStrColorD, subline, boost::is_any_of("\t "));
std::vector< std::string >::iterator it;
for (it = tmpStrColorD.begin(); it != tmpStrColorD.end(); it++)
{
float rgbValue;
std::stringstream ss;
ss << *it;
ss >> rgbValue;
colorD.push_back(rgbValue);
}
}
if (line[0] == 'K' && line[1] == 's')
{
std::vector< std::string > tmpStrColorS;
std::string subline;
subline = line.substr(3);
boost::split(tmpStrColorS, subline, boost::is_any_of("\t "));
std::vector< std::string >::iterator it;
for (it = tmpStrColorS.begin(); it != tmpStrColorS.end(); it++)
{
float rgbValue;
std::stringstream ss;
ss << *it;
ss >> rgbValue;
colorS.push_back(rgbValue);
}
}
}
mtlFile.close();
return;
}
float* objFileRenderer::vertexVectorToVertexArray(std::vector< float >& vertices, int& numVertices)
{
numVertices = vertices.size() / 3;
float* vertexArray = NULL;
vertexArray = new float[vertices.size()];
for (unsigned int i = 0; i < vertices.size(); i++)
{
vertexArray[i] = vertices[i];
}
return vertexArray;
}
unsigned short* objFileRenderer::indexVectorToIndexArray(std::vector< unsigned short >& indices, int& numFaces)
{
numFaces = indices.size() / 3;
unsigned short* indexArray = NULL;
indexArray = new unsigned short[indices.size()];
for (unsigned int i = 0; i < indices.size(); i++)
{
indexArray[i] = indices[i]-1;
}
return indexArray;
}
void objFileRenderer::colorVectorsToColorArrays(std::vector< float >& colorA, std::vector< float >& colorD, std::vector< float >& colorS, float*& colorArrayA, float*& colorArrayD, float*& colorArrayS, int& numColoredObjects)
{
numColoredObjects = colorA.size() / 3;
colorArrayA = new float[numColoredObjects*3];
colorArrayD = new float[numColoredObjects*3];
colorArrayS = new float[numColoredObjects*3];
for (int i = 0; i < numColoredObjects; i+=3)
{
colorArrayA[i] = colorA[i]; colorArrayA[i+1] = colorA[i+1]; colorArrayA[i+2] = colorA[i+2];
colorArrayD[i] = colorD[i]; colorArrayD[i+1] = colorD[i+1]; colorArrayD[i+2] = colorD[i+2];
colorArrayS[i] = colorS[i]; colorArrayS[i+1] = colorS[i+1]; colorArrayS[i+2] = colorS[i+2];
}
return;
}
void objFileRenderer::normalVectorsToNormalArrays(std::vector< float >& normals, std::vector< unsigned short >& normalIndices, float*& normalArray, unsigned short*& normalIndicesArray, int& numNormals, int& numNormalIndices)
{
numNormals = normals.size() / 3;
numNormalIndices = normalIndices.size();
normalArray = new float[numNormalIndices];
normalIndicesArray = new unsigned short[numNormalIndices];
for (int i = 0; i < numNormalIndices; i+=3)
{
normalIndicesArray[i] = normalIndices[i]-1;
normalIndicesArray[i+1] = normalIndices[i+1]-1;
normalIndicesArray[i+2] = normalIndices[i+2]-1;
}
// load normals in index order
for (int i = 0; i < numNormalIndices; i+=3)
{
int index = normalIndicesArray[i];
normalArray[i] = normals[index];
normalArray[i+1] = normals[index+1];
normalArray[i+2] = normals[index+2];
}
return;
}
void objFileRenderer::putArraysIntoVAO(float* vertexArray, int& numVertices, unsigned short* indexArray, int& numFaces, float* normalArray, int& numNormals, unsigned short* normalIndicesArray, int& numNormalIndices)
{
glGenVertexArrays(1, &vaoID[0]); // create our vertex array object
glBindVertexArray(vaoID[0]); // bind our vertex array object so we can use it
glGenBuffers(2, &iboID[0]); // generate our index buffer object
glGenBuffers(2, &vboID[0]); // generate our vertex buffer object
// normalArray holds normals in index order, so I shouldn't use this
// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboID[1]); // bind our normal index buffer object
// glBufferData(GL_ELEMENT_ARRAY_BUFFER, (numNormalIndices) * sizeof(GLushort), normalIndicesArray, GL_STATIC_DRAW); // set the size and data of our IBO
glBindBuffer(GL_ARRAY_BUFFER, vboID[1]); // bind our normal vertex buffer object
glBufferData(GL_ARRAY_BUFFER, (numNormalIndices) * sizeof(GLfloat), normalArray, GL_STATIC_DRAW); // set the size and data of our VBO and set it to STATIC_DRAW
glVertexAttribPointer((GLuint)1, 3, GL_FLOAT, GL_FALSE, 0, 0); // set up our vertex attributes pointer
glEnableVertexAttribArray(1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboID[0]); // bind our index buffer object
glBufferData(GL_ELEMENT_ARRAY_BUFFER, (numFaces*3) * sizeof(GLushort), indexArray, GL_STATIC_DRAW); // set the size and data of our IBO
glBindBuffer(GL_ARRAY_BUFFER, vboID[0]); // bind our vertex buffer object
glBufferData(GL_ARRAY_BUFFER, (numVertices*3) * sizeof(GLfloat), vertexArray, GL_STATIC_DRAW); // set the size and data of our VBO and set it to STATIC_DRAW
glVertexAttribPointer((GLuint)0, 3, GL_FLOAT, GL_FALSE, 0, 0); // set up our vertex attributes pointer
glEnableVertexAttribArray(0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
return;
}
void objFileRenderer::setupScene()
{
app->setFramerateLimit(60); // max 60 FPS
glClearColor(0.4f, 0.6f, 0.9f, 0.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glEnable(GL_CULL_FACE);
shader = new Shader("shader.vert", "shader.frag");
projectionMatrix = glm::perspective(60.0f, (float)windowWidth / (float)windowHeight, 0.1f, 100.0f);
return;
}
void objFileRenderer::renderScene(int& numVertices, int& numFaces, float*& colorArrayA, float*& colorArrayD, float*& colorArrayS, float*& normalArray, float objScale)
{
sf::Time elapsedTime = clock.getElapsedTime();
clock.restart();
if (rot > 360.0f)
rot = 0.0f;
rot += rotSpeed * elapsedTime.asSeconds();
float lightPosition[3] = { -100.0, -100.0, 100.0 };
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
viewMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, -3.0f, -10.0f)); // move back by 5 units
modelMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(objScale)); // change last arg to 0.5f to shrink model by half
modelMatrix *= glm::rotate< float >(glm::mat4(1.0f), rot, glm::vec3(0, 1, 0));
shader->bind();
int projectionMatrixLocation = glGetUniformLocation(shader->id(), "projectionMatrix");
int viewMatrixLocation = glGetUniformLocation(shader->id(), "viewMatrix");
int modelMatrixLocation = glGetUniformLocation(shader->id(), "modelMatrix");
int ambientLocation = glGetUniformLocation(shader->id(), "ambientColor");
int diffuseLocation = glGetUniformLocation(shader->id(), "diffuseColor");
int specularLocation = glGetUniformLocation(shader->id(), "specularColor");
int lightPositionLocation = glGetUniformLocation(shader->id(), "lightPosition");
int normalMatrixLocation = glGetUniformLocation(shader->id(), "normalMatrix");
glUniformMatrix4fv(projectionMatrixLocation, 1, GL_FALSE, &projectionMatrix[0][0]);
glUniformMatrix4fv(viewMatrixLocation, 1, GL_FALSE, &viewMatrix[0][0]);
glUniformMatrix4fv(modelMatrixLocation, 1, GL_FALSE, &modelMatrix[0][0]);
glUniform3fv(ambientLocation, 1, colorArrayA);
glUniform3fv(diffuseLocation, 1, colorArrayD);
glUniform3fv(specularLocation, 1, colorArrayS);
glUniform3fv(lightPositionLocation, 1, lightPosition);
glUniformMatrix3fv(normalMatrixLocation, 1, GL_FALSE, normalArray);
glBindVertexArray(vaoID[0]);
glDrawRangeElements(GL_TRIANGLES, 0, numFaces*3, numFaces*3, GL_UNSIGNED_SHORT, NULL);
glBindVertexArray(0);
shader->unbind();
app->display();
return;
}
void objFileRenderer::handleEvents()
{
sf::Event event;
while (app->pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
app->close();
}
if ((event.type == sf::Event::KeyPressed) && (event.key.code == sf::Keyboard::Escape))
{
app->close();
}
if (event.type == sf::Event::Resized)
{
glViewport(0, 0, event.size.width, event.size.height);
}
}
return;
}
void objFileRenderer::mainLoop(int& numVertices, int& numFaces, float*& colorArrayA, float*& colorArrayD, float*& colorArrayS, float*& normalArray, float objScale)
{
while (app->isOpen())
{
renderScene(numVertices, numFaces, colorArrayA, colorArrayD, colorArrayS, normalArray, objScale);
handleEvents();
}
}
}
file: shader.cpp
#include "shader.h"
#include <string.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
static char* textFileRead(const char *fileName) {
char* text = NULL;
if (fileName != NULL) {
FILE *file = fopen(fileName, "rt");
if (file != NULL) {
fseek(file, 0, SEEK_END);
int count = ftell(file);
rewind(file);
if (count > 0) {
text = (char*)malloc(sizeof(char) * (count + 1));
count = fread(text, sizeof(char), count, file);
text[count] = '\0';
}
fclose(file);
}
}
return text;
}
Shader::Shader() {
}
Shader::Shader(const char *vsFile, const char *fsFile) {
init(vsFile, fsFile);
}
void Shader::init(const char *vsFile, const char *fsFile) {
shader_vp = glCreateShader(GL_VERTEX_SHADER);
shader_fp = glCreateShader(GL_FRAGMENT_SHADER);
const char* vsText = NULL;
const char* fsText = NULL;
vsText = textFileRead(vsFile);
fsText = textFileRead(fsFile);
if (vsText == NULL)
{
cerr << "Error: vertex shader file not found" << endl;
}
if (fsText == NULL) {
cerr << "Error: fragment shader file not found." << endl;
}
if (vsText == NULL || fsText == NULL)
return;
glShaderSource(shader_vp, 1, &vsText, 0);
glShaderSource(shader_fp, 1, &fsText, 0);
glCompileShader(shader_vp);
glCompileShader(shader_fp);
shader_id = glCreateProgram();
glAttachShader(shader_id, shader_fp);
glAttachShader(shader_id, shader_vp);
glLinkProgram(shader_id);
glBindAttribLocation(shader_id, 0, "in_Position");
//glBindAttribLocation(shader_id, 1, "in_Color");
glBindAttribLocation(shader_id, 1, "in_Normal");
}
Shader::~Shader() {
glDetachShader(shader_id, shader_fp);
glDetachShader(shader_id, shader_vp);
glDeleteShader(shader_fp);
glDeleteShader(shader_vp);
glDeleteProgram(shader_id);
}
unsigned int Shader::id() {
return shader_id;
}
void Shader::bind() {
glUseProgram(shader_id);
}
void Shader::unbind() {
glUseProgram(0);
}
file: shader.vert
#version 150 core
in vec3 in_Position;
in vec3 in_Normal;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec3 lightPosition;
uniform mat3 normalMatrix;
smooth out vec3 vVaryingNormal;
smooth out vec3 vVaryingLightDir;
void main()
{
// derive MVP and MV matrices
mat4 modelViewProjectionMatrix = projectionMatrix * viewMatrix * modelMatrix;
mat4 modelViewMatrix = viewMatrix * modelMatrix;
// get surface normal in eye coordinates
vVaryingNormal = normalMatrix * in_Normal;
// get vertex position in eye coordinates
vec4 vPosition4 = modelViewMatrix * vec4(in_Position, 1.0);
vec3 vPosition3 = vPosition4.xyz / vPosition4.w;
// get vector to light source
vVaryingLightDir = normalize(lightPosition - vPosition3);
// Set the position of the current vertex
gl_Position = modelViewProjectionMatrix * vec4(in_Position, 1.0);
}
file: shader.frag
#version 150 core
out vec4 out_Color;
uniform vec3 ambientColor;
uniform vec3 diffuseColor;
uniform vec3 specularColor;
smooth in vec3 vVaryingNormal;
smooth in vec3 vVaryingLightDir;
void main()
{
// dot product gives us diffuse intensity
float diff = max(0.0, dot(normalize(vVaryingNormal), normalize(vVaryingLightDir)));
// multiply intensity by diffuse color, force alpha to 1.0
out_Color = vec4(diff * diffuseColor, 1.0);
// add in ambient light
out_Color += vec4(ambientColor, 1.0);
// specular light
vec3 vReflection = normalize(reflect(-normalize(vVaryingLightDir), normalize(vVaryingNormal)));
float spec = max(0.0, dot(normalize(vVaryingNormal), vReflection));
if (diff != 0)
{
float fSpec = pow(spec, 128.0);
// Set the output color of our current pixel
out_Color.rgb += vec3(fSpec, fSpec, fSpec);
}
}
I know it's a lot to look through, but I would be very happy to get some help with getting to the bottom of this problem, so thanks in advance to anyone who has some time to help me figure this out!
I don't think you're treating the indices correctly. OBJ's are tricky to parse in opengl because they provide data using a useless multiple-index format. Essentially you have to break down all of the vertices and normals and rebuild the indices such that for every vertex it's position/normal/color/whatever all share the same index. It's not a trivial task to reorder them, but if you look online for "obj vertex buffer" I'm sure you'll find hundreds of references and posts about it.
Try reading this and see if it makes more sense: http://aresio.blogspot.com/2009/07/wavefront-obj-files-vertex-buffer.html