3
\$\begingroup\$

I have made my first program that draws a colored rectangle on-screen. I want to know the best practices of making OpenGL and GLSL programs.

#include <GL/glew.h>
#include <SFML/Window.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
#include <cassert>

#define GLSL(src) "#version 400 core\n" #src
#define GETERROR() getError(__FILE__,__LINE__)

namespace
{
    const unsigned CurrentWidth = 800;
    const unsigned CurrentHeight = 600;
}

void getError(const char *file, int line)
{
    GLenum error(glGetError());

    while (error != GL_NO_ERROR)
    {
        std::string errorString;

        switch (error)
        {
        case GL_INVALID_OPERATION:      errorString = "INVALID_OPERATION";      break;
        case GL_INVALID_ENUM:           errorString = "INVALID_ENUM";           break;
        case GL_INVALID_VALUE:          errorString = "INVALID_VALUE";          break;
        case GL_OUT_OF_MEMORY:          errorString = "OUT_OF_MEMORY";          break;
        case GL_INVALID_FRAMEBUFFER_OPERATION:  errorString = "INVALID_FRAMEBUFFER_OPERATION";  break;
        }

        throw std::runtime_error("GL_" + errorString + " - " + std::string(file) + ":" + std::to_string(line));
        error = glGetError();
    }
}

class Tutorial
{
public:
    Tutorial();
    ~Tutorial();
    void run();


private:
    void processEvent();
    void render();

    void createVBO();
    void destroyVBO();
    void createShaders();
    void destroyShaders();


private:
    sf::Window mWindow;

    GLuint mVertexShaderID;
    GLuint mFragmentShaderID;
    GLuint mProgramID;
    GLuint mVAOID;
    GLuint mVBOID;
    GLuint mColorBufferID;

    const std::vector<GLchar*> mVertexShader =
    {
        {
            GLSL(
            layout(location = 0) in vec4 in_Position;
            layout(location = 1) in vec4 in_Color;

            out vec4 ex_Color;

            void main(void)
            {
                gl_Position = in_Position;
                ex_Color = in_Color;
            }
            )
        }
    };
    const std::vector<GLchar*> mFragmentShader =
    {
        {
            GLSL(
            in vec4 ex_Color;
            out vec4 out_Color;

            void main(void)
            {
                out_Color = ex_Color;
            }
            )
        }
    };

};

Tutorial::Tutorial()
    : mWindow(sf::VideoMode(CurrentWidth, CurrentHeight), "Example 00")
{
    glewExperimental = true; // Needed in core profile 

    if (glewInit() != GLEW_OK)
    {
        throw std::runtime_error("Failed to initialize GLEW\n");
    }

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

    createShaders();
    createVBO();
}

void Tutorial::run()
{
    while (mWindow.isOpen())
    {
        processEvent();
        render();
    }
}

void Tutorial::processEvent()
{
    sf::Event event;
    while (mWindow.pollEvent(event))
    {
        if (event.type == sf::Event::Closed)
            mWindow.close();
        if (event.type == sf::Event::Resized)
            glViewport(0, 0, mWindow.getSize().x, mWindow.getSize().y);
    }
}
void Tutorial::render()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    mWindow.display();
}

Tutorial::~Tutorial()
{
    destroyShaders();
    destroyVBO();
}

void Tutorial::createVBO()
{
    GLfloat Vertices[] =
    {
        -0.8f, -0.8f, 0.0f, 1.0f,
        0.0f, 0.8f, 0.0f, 1.0f,
        0.8f, -0.8f, 0.0f, 1.0f
    };

    GLfloat Colors[] =
    {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };
    // Its first parameter 1 describes the amount, or number of Vertex Arrays to generate.
    // second parameter mVAOID describes identifiers (or "names" in the OpenGL documentation) are stored in the arrays parameter
    glGenVertexArrays(1, &mVAOID);
    glBindVertexArray(mVAOID); // set it active

    glGenBuffers(1, &mVBOID);
    glBindBuffer(GL_ARRAY_BUFFER, mVBOID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);

    glGenBuffers(1, &mColorBufferID);
    glBindBuffer(GL_ARRAY_BUFFER, mColorBufferID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Colors), Colors, GL_STATIC_DRAW);
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(1);

    GETERROR();
}

void Tutorial::destroyVBO()
{
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDeleteBuffers(1, &mColorBufferID);
    glDeleteBuffers(1, &mVBOID);

    glBindVertexArray(0);
    glDeleteVertexArrays(1, &mVAOID);

    GETERROR();
}

void Tutorial::createShaders()
{
    mVertexShaderID = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(mVertexShaderID, 1, &mVertexShader[0], NULL);
    glCompileShader(mVertexShaderID);

    mFragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(mFragmentShaderID, 1, &mFragmentShader[0], NULL);
    glCompileShader(mFragmentShaderID);

    mProgramID = glCreateProgram();
    glAttachShader(mProgramID, mVertexShaderID);
    glAttachShader(mProgramID, mFragmentShaderID);
    glLinkProgram(mProgramID);
    glUseProgram(mProgramID);

    GETERROR();
}

void Tutorial::destroyShaders()
{
    glUseProgram(0);

    glDetachShader(mProgramID, mVertexShaderID);
    glDetachShader(mProgramID, mFragmentShaderID);

    glDeleteShader(mFragmentShaderID);
    glDeleteShader(mVertexShaderID);

    glDeleteProgram(mProgramID);

    GETERROR();
}

int main()
{
    try
    {
        Tutorial tut;
        tut.run();
    }
    catch (std::runtime_error& e)
    {
        std::cout << "\nException: " << e.what() << std::endl;
        return 1;
    }
}
\$\endgroup\$
2
\$\begingroup\$

A few things, roughly order of appearance in the code:

Miscellaneous:

  • I don't think CurrentWidth/CurrentHeight describe the intent of those constants very well. They are the initial dimensions of the application window, so something in the lines of InitWindowWidth, InitWindowHeight would make more sense.

  • In the getError() function, you have used a while loop to query the GL errors, which is correct, since glGetError pops the top of the error stack, so there might be more than one. However, you throw an exception right at the first iteration of the loop, so it is just the same as no loop at all. If you just care about the latest error, replace the while loop with an if. If you'd like to clear the error stack instead, change your function to append the error list to a string and move the throw to the end of the function. By the way, if getError() is a local function, it can also be placed inside the unnamed namespace.

  • Make sure to use const on local variables whenever practical to ensure single-assignment. In createVBO(), the local data arrays can and should be constants.

Shader source code management:

I see that you are using the slightly hackish stringification macro to make the shader source code look like normal C code. This is cool because it enables syntax highlighting in your editor and removes the need to manually quote each line to make it a string, but like all hackish solutions, it breaks very easily. In particular, it will break if you have a comma (,) in the shader source, because the preprocessor will think it is actually a comma separating the parameters of a function-like macro.

So this solution is far from ideal and not very scalable either. Having it as a literal string avoids the macros issues, but it is even worse to maintain. You are also forced to recompile the program whenever the shader source is updated, which shouldn't be the case, since it is not part of the program, but rather, a resource or asset in the form of a text string.

For any usage more complex than a couple lines long shader program, you should definitely move the code to an external text file and load it at startup into a string. This will decouple the C++ program from the shader code, allowing you to freely change the shaders without recompiling the C++ program. You can also take this further and allow for hot reloading of shaders while the main program is running.

Reading a text file into a string in C++ is as simple as this:

std::string readTextFile(const std::string & filename)
{
    std::ifstream ifs(filename);
    if (ifs)
    {
        return std::string(std::istreambuf_iterator<char>(ifs), 
                           std::istreambuf_iterator<char>());
    }
    return std::string(); // Or throw and error, return a default string, etc...
}

Also note that you can use a std::string for the source code. No need to use a vector of GLchar. OpenGL/GLSL only accepts ASCII strings, so GLchar is a typedef to char. I think this typedef is only provided for consistently with the rest of the API (like GLvoid for instance, which must be void), since it is very unlikely that it will ever change to some other char type, due to the big legacy codebase of GL.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.