OpenGL Project Setup and Boilerplate Code


I had quite a journey setting up the environment for OpenGL development. Lots of research was done while I was solving all the problems and here I documented my version of approach.

Resources

I’ve been mainly using Visual Studios to work on my previous Raytracing project since it was raw C++ implementation. As OpenGL is a graphic API that has been integrated into MacOS, I started with setting the project in Xcode.

The tutorial I’ve been following is https://github.com/tomdalling/opengl-series which covers the basics in details with code base supporting multiple platforms(MacOS, Windows, Linux). Here I base my practice on its first chapter - 01_project_skeleton.

The website http://learnopengl.com is however by far the most well-rounded one for me, and I will constantly referring to it.

Xcode

Project Setting

Includes

#include "platform.hpp"

// third-party libraries
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>

// standard c++ libraries
#include <cassert>
#include <iostream>
#include <stdexcept>
#include <cmath>

#include "Program.h"

To get the includes working properly in Xcode project, we have to set the path in Project Setting - Search Paths:


Third Party

The code is using third-party tools including

  • GLEW (Graphics Library Extension Wrangler),
  • GLFW (Graphics Library FrameWork), and
  • GLM (OpenGL Mathematics).

They have different roles:

  1. GLEW is used to access the modern OpenGL API functions. In modern OpenGL, the API functions are determined at run time, not compile time. GLEW will handle the run time loading of the OpenGL API.
  2. GLFW will allow us to** create a window, and receive mouse and keyboard input in a cross-platform way**. OpenGL does not handle those so we have to use library to do so.
  3. GLM is a mathematics library that handles vectors and matrices etc. Older versions of OpenGL provided functions like glRotate, glTranslate, glScale. But in modern OpenGL, these functions do not exist, and we must do all of the math ourselves. GLM will help us on that.

Note that,

  1. In the tutorial code of http://learnopengl.com , instead of GLEW it is now using GLAD as an alternative.
  2. GLFW has an alternative GLUT. Current available version is called FreeGLUT

Build Library Binary

Among them, GLFW(or FreeGLUT if you choose to use that) has to be built for the corresponding OS. This can be done with CMake and details can be found here. A libglfw3.a file will be generated if on MacOS (or glfw3.lib if on Windows).

To include all of them into project, best practice is to group GLEW/GLFW/GLM header files into a folder Includes, and put libglfw3.a into a folder Libs.

Other Files

In 01_project_skeleton, platform.hpp contains declaration of std::string ResourcePath(std::string fileName);, an extra function used to get the path of shader files on MacOS. The corresponding platformosx.mm contains its defination in objective-c.

The code puts functionalities of shader loading/compiling into the Shader.h/Shader.cpp and shader program linking into Program.h/Program.cpp. Hence we include Program.h here.

Boilerplate Code Breakdown

main.cpp

Here I removed most of error-checking code to get a minimal look of the whole setup code, just for future reference. However they are very important in practice to help locating problems.

// constants
const glm::vec2 SCREEN_SIZE(400,400);

// globals
GLFWwindow* gWindow = NULL;
GLuint gVAO = 0;
GLuint gVBO = 0;
tdogl::Program* gProgram = NULL;

// loads the vertex shader and fragment shader, and
// links them to make the global gProgram
static void LoadShaders(){
    std::vector<tdogl::Shader> shaders;
    shaders.push_back(tdogl::Shader::shaderFromFile(
    ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER));
    shaders.push_back(tdogl::Shader::shaderFromFile(
    ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER));
	gProgram = new tdogl::Program(shaders);
}

static void LoadTriangle(){
    glGenVertexArrays(1, &gVAO);
    glBindVertexArray(gVAO);
    glGenBuffers(1, &gVBO);
    glBindBuffer(GL_ARRAY_BUFFER, gVBO);

    GLfloat vertexData[] = {
        // X       Y     Z
         0.000f,  0.5f, 0.0f,
        -0.577f, -0.5f, 0.0f,
         0.577f, -0.5f, 0.0f,
    };

    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
    glEnableVertexAttribArray(gProgram->attrib("vert"));
    glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);

    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindVertexArray(0);
}

Render function to draw stuff.

static void Render(){
    glClearColor(0,0,0,1);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(gProgram->object());
    glBindVertexArray(gVAO);
	glDrawArrays(GL_TRIANGLES,0,3);

    glBindVertexArray(0);
	glUseProgram(0);

    glfwSwapBuffers(gWindow);
}

Main function to

  • initialize GLFW and GLEW
  • create window
  • load shader program
  • load geometry
  • loop the Render() funtion.
void AppMain(){
	// initialise GLFW
	glfwInit();

	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,2);

	gWindow = glfwCreateWindow((int)SCREEN_SIZE.x,(int)SCREEN_SIZE.y,"test",NULL,NULL);

	// GLFW settings
	// Before you can use the OpenGL API, you must have a current OpenGL context.
	glfwMakeContextCurrent(gWindow);

	// initialise GLEW
	glewExperimental = GL_TRUE; // stops glew crashing on OSX :-/
	glewInit();

	LoadShaders();
	LoadTriangle();

	while(!glfwWindowShouldClose(gWindow)){
	    glfwPollEvents();
	    Render();
	}
	glfwTerminate();
}

int main(int argc, char *argv[]){
    AppMain();
}

Shader Class

Header:

#include <GL/glew.h>
#include <string>

namespace tdogl {
	class Shader{
	public:
		static Shader shaderFromFile(const std::string& filePath, GLenum shaderType);
		Shader(const std::string& shaderCode, GLenum shaderType);
		GLuint object() const;
	private:
		GLuint _object;
		unsigned* _refCount;
		void _release();
	};
}

Source:

  • create the shader object: glCreateShader
  • load shader files with std::ifstream
  • setup shader code: glShaderSource
  • compile the shader: glCompileShader
#include "Shader.h"
#include <stdexcept>
#include <fstream>
#include <string>
#include <cassert>
#include <sstream>

using namespace tdogl;

Shader::Shader(const std::string& shaderCode, GLenum shaderType) :
_object(0),
_refCount(NULL)
{
    //create the shader object
    _object = glCreateShader(shaderType); // had bug here before!
    if(_object == 0)
        throw std::runtime_error("glCreateShader failed");

    //set the source code
    const char* code = shaderCode.c_str();
    glShaderSource(_object, 1, (const GLchar**)&code, NULL);

    //compile
    glCompileShader(_object);

    //throw exception if compile error occurred
    // ...

    _refCount = new unsigned;
    *_refCount = 1;
}

GLuint Shader::object() const {
    return _object;
}

Shader Shader::shaderFromFile(const std::string& filePath, GLenum shaderType) {
    //open file
    std::ifstream f;
    f.open(filePath.c_str(), std::ios::in | std::ios::binary);

    if(!f.is_open()){
        throw std::runtime_error(std::string("Failed to open file: ") + filePath);
    }

    //read whole file into stringstream buffer
    std::stringstream buffer;
    buffer << f.rdbuf();

    //return new shader
    Shader shader(buffer.str(), shaderType);
    return shader;
}

void Shader::_release() {
    assert(_refCount && *_refCount > 0);
    *_refCount -= 1;
    if(*_refCount == 0){
        glDeleteShader(_object); _object = 0;
        delete _refCount; _refCount = NULL;
    }
}

Program Class

Header:

#include "Shader.h"
#include <vector>

namespace tdogl {
	class Program{
	public:
		Program(const std::vector<Shader>& shaders);
		GLuint object() const;
		GLint attrib(const GLchar* attribName) const;
	private:
		GLuint _object;
		Program(const Program&);
	};
}

Source:

  • create the program object: glCreateProgram
  • attach all the shaders: glAttachShader
  • link the shaders together: glLinkProgram
  • detach all the shaders: glDetachShader
#include "Program.h"
#include <stdexcept>

using namespace tdogl;

Program::Program(const std::vector<Shader>& shaders) :
_object(0)
{
    if(shaders.size() <= 0)
        throw std::runtime_error("No shaders were provided to create the program");

    //create the program object
    _object = glCreateProgram();
    if(_object == 0)
        throw std::runtime_error("glCreateProgram failed");

    //attach all the shaders
    for(unsigned i = 0; i < shaders.size(); ++i)
        glAttachShader(_object, shaders[i].object());

    //link the shaders together
    glLinkProgram(_object);

    //detach all the shaders
    for(unsigned i = 0; i < shaders.size(); ++i)
        glDetachShader(_object, shaders[i].object());

    //throw exception if linking failed
    // ...
}

GLuint Program::object() const {
    return _object;
}

GLint Program::attrib(const GLchar* attribName) const {
    if(!attribName)
        throw std::runtime_error("attribName was NULL");

    GLint attrib = glGetAttribLocation(_object, attribName);
    if(attrib == -1)
        throw std::runtime_error(std::string("Program attribute not found: ") + attribName);

    return attrib;
}

Shader Files

These are very minimal functional shaders.

Vertex Shader

#version 150
in vec3 vert;
void main() {
    // does not alter the verticies at all
    gl_Position = vec4(vert, 1);
}

Fragment Shader

#version 150
out vec4 finalColor;
void main() {
    //set every drawn pixel to red
    finalColor = vec4(1.0, 0, 0, 1.0);
}

Debug

Apple Mach-O Linker (ld) Error Group

This is caused by failure on linking libraries or source files. Need to check Build Phase of build target.

If the errors are about glfw functions, it’s because we didn’t link with libglfw3.a properly. If they are about other OpenGL functions that is because we have to explicitly link with certain frameworks there:


Also make sure all the source files (all .cpp including the .mm) are added into Compile Sources of the build target.

If there are errors about glew functions, that is because we also need to explicitly add glew.c file to compile,


Another option is to just include it in main.cpp

#include <GL/glew.h>
#include <glew.c> // add this
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>

Thread 1: signal SIGABRT

At this point I can pass the compiling but still failed on running the build.

The program broke at function Shader::shaderFromFile(), and I found the path where the program reading my shader files is actually to the wrong folder(thanks to breakpoint). So I set the new DerivedData folder and put shader files right by the executable inside DerivedDate/.../Build/Products.


This way the function successfully located the shader files and the program proceeded.

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)

I also got this error at some point. I made sure the shader files are successfully located and shader program is successfully created. I then found the error actually happened when I was calling glCreateShader() in Shader.cpp.


It turns out I missed the line glfwMakeContextCurrent(gWindow); in main.cpp, so I was calling GL functions before I have a current GL context!

Result

Finally it compiled and ran successfully!


Visual Studios

Make sure using 32-bit binaries.

Update paths to the includes and libraries

  1. Right click on the project -> Properties -> VC++ Directories
  2. Include Directories: C:...\includes;$(IncludePath)
  3. Library Directories: C:...\libs;$(LibraryPath)

    Update libraries dependencies

    Linker -> Input -> Additional Dependencies add glfw3.lib and opengl32.lib

END

Reference

  1. Learnopengl.com - PBR - Theory
  2. Modern OpenGL 01 - Getting Started in Xcode, Visual C++, and Linux