Search code examples
c++openglattributesglslglfw

Only getting position data when rendering multiple attributes in OpenGL program?


I am having some issues with rendering color. I'd like to encapsulate objects into their own class and have the class manage buffer data. Eventually, I'll move to having a ObjectManager that can keep track of cleanup of different objects. I currently am stuck at rendering data with 2 attributes. I have one vertex buffer allocated with data that is tightly packed into attribute blocks as a batch (VVVCCC) and one element array buffer. Structured like this:

VAO
    VBO (VVVCCC)
    EBO

Whenever I run the program, I only get position data that seems to be read. I get a black triangle with no color data.

Vertex Shader

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
   gl_Position = vec4(aPos, 1.0);
   ourColor = aColor;
}

Fragment Shader

#version 330 core

in vec3 ourColor;
out vec4 FragColor;

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

HelloGL.cpp

#include <iostream>
#include "utils.h"

static void glfwError(int id, const char* description)
{
    std::cout << description << std::endl;
}

const int WIDTH = 800;
const int HEIGHT = 600;

void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, WIDTH, HEIGHT);
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

int main(int argc, char** argv) {

    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit()) {
        std::cout << "GLFW failed to initialize" << std::endl;
        return -1;
    }

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(WIDTH, HEIGHT, "Hello World", NULL, NULL);
    if (!window)
    {
        std::cout << "Error creating window" << std::endl;
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // Initialize GLEW using GL context created by GLFW
    GLenum err = glewInit();
    if (GLEW_OK != err) {
        std::cout << err << "GLEW failed to initialize" << std::endl;
        return -1;
    }

    // Provide GLFW an interface to send error messages
    glfwSetErrorCallback(&glfwError);

    // Create and setup shader pipeline
    shader::Pipeline pipeline(true);
    pipeline.addSource(shader::loadShader("Shaders/Vert/plot_color.vert").c_str(), shader::VERTEX);
    pipeline.addSource(shader::loadShader("Shaders/Frag/colors.frag").c_str(), shader::FRAGMENT);
    pipeline.linkShaders();

    // Create an object and add the data
    object::ObjectManager objects;

    objects.createObject("triangle");
    std::vector<float>vertices = {
        0.5f, -0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
        0.0f, 0.5f, 0.0f
    };
    std::vector<float> color = {
        1.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f
    };
    std::vector<int>indices = {
        0, 1, 2
    };

    objects["triangle"]->addBufferData(vertices, 3);
    objects["triangle"]->addBufferData(color, 3);
    objects["triangle"]->addElement(indices);
    objects["triangle"]->initBuffers();

    objects.setActiveObject("triangle");

    // Check for GL Errors before rendering
    while ((err = glGetError()) != GL_NO_ERROR) {
        std::cout << std::hex << err << std::endl;
    }

    // uncomment this call to draw in wireframe polygons.
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        //processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        pipeline.use();
        glDrawElements(GL_TRIANGLES, objects.activeElementCount(), GL_UNSIGNED_INT, 0);
        // glBindVertexArray(0); // no need to unbind it every time 

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

utils.h

#pragma once
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <map>

namespace shader {
    enum Type {
        VERTEX,
        FRAGMENT,
        LAST
    };

    std::string loadShader(const char* fileName) {
        std::string content;
        std::ifstream fileStream;
        fileStream.exceptions(std::ifstream::failbit | std::ifstream::badbit);

        try {
            fileStream.open(fileName);
            std::stringstream shaderStream;

            shaderStream << fileStream.rdbuf();
            fileStream.close();

            content = shaderStream.str();
        }
        catch (std::ifstream::failure e) {
            std::cout << "ERROR::SHADER::CANNOT READ FILE" << std::endl;
        }

        return content;
    }

    // Manages GPU pipeline by combining shaders into a program pipeline. Add shaders as necessary by using GLSL.
    class Pipeline {
    public:
        Pipeline() : Pipeline(false) {}
        Pipeline(bool v) : verbose(v) {
            shaderProgram = glCreateProgram();
            shaderFlags = 0;
            vertexShaderSource = fragmentShaderSource = "\0";
            vertexShader = fragmentShader = 0;
            compileSuccess = 0;
            infoLog[0] = '\0';
        }
        ~Pipeline() {
            glDeleteProgram(shaderProgram);
        }

        // Adds and compiles shader source.
        void addSource(const char* s, shader::Type t) {
            shaderFlags ^= (1 << t);
            switch (t) {
            case VERTEX:
                vertexShaderSource = s;
                break;
            case FRAGMENT:
                fragmentShaderSource = s;
                break;
            default:
                break;
            }
            if (verbose) {
                std::cout << "Adding shader program:\n" << s << std::endl;
            }
            compileShader(t);
        }

        // Links shader source to the GPU pipeline program. Cleans up shader source compilation after linking.
        void linkShaders() {
            for (int i = 1; i < LAST; i++) {
                if ((shaderFlags & (1 << i)) == (1 << VERTEX)) {
                    glAttachShader(shaderProgram, vertexShader);
                }
                if ((shaderFlags & (1 << i)) == (1 << FRAGMENT)) {
                    glAttachShader(shaderProgram, fragmentShader);
                }
            }
            glLinkProgram(shaderProgram);
            glGetProgramiv(shaderProgram, GL_LINK_STATUS, &compileSuccess);
            if (!compileSuccess) {
                glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
                std::cout << "ERROR::SHADER::PROGRAM_LINK_FAILED\n" << infoLog << std::endl;
            }
            deleteShaders();
        }

        void use() {
            glUseProgram(shaderProgram);
        }

    private:
        const char* vertexShaderSource;
        const char* fragmentShaderSource;
        unsigned int shaderProgram, vertexShader, fragmentShader;
        int shaderFlags;
        int compileSuccess;
        char infoLog[512];
        bool verbose;

        // Deletes shaders after use
        void deleteShaders() {
            for (int i = 1; i < LAST; i++) {
                if ((shaderFlags & (1 << i)) == (1 << VERTEX)) {
                    glDeleteShader(vertexShader);
                }
                if ((shaderFlags & (1 << i)) == (1 << FRAGMENT)) {
                    glDeleteShader(fragmentShader);
                }
            }
            shaderFlags = 0;
        }

        // Compiles shaders into GLSL binaries
        void compileShader(shader::Type t) {
            switch (t) {
            case VERTEX:
                vertexShader = glCreateShader(GL_VERTEX_SHADER);
                glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
                glCompileShader(vertexShader);
                glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &compileSuccess);
                break;
            case FRAGMENT:
                fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
                glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
                glCompileShader(fragmentShader);
                glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compileSuccess);
                break;
            default:
                break;
            }
            if (!compileSuccess)
            {
                glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
                std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
            }
        }
    };
}
namespace object {

    class Object {
    public:
        Object() {
            // Generate buffers
            glGenVertexArrays(1, &vertexArray);
            glGenBuffers(1, &vertexBuffer);
            glGenBuffers(1, &elementBuffer);

            attributes = 0;
            maxAttributes = 16; // Default minimum guarunteed number of attributes per buffer
        }

        Object(int maxAttributes) : Object() {
            this->maxAttributes = maxAttributes;
        }

        ~Object() {
            glDeleteVertexArrays(1, &vertexArray);
            glDeleteBuffers(1, &vertexBuffer);
            glDeleteBuffers(1, &elementBuffer);
        }

        void addBufferData(std::vector<float> input, int vertexLength) {
            if (attributes >= maxAttributes) {
                std::cout << "ERROR::ADD_BUFFER_DATA::MAXIMUM ATTRIBUTE COUNT REACHED" << std::endl;
                return;
            }

            if (input.size() % vertexLength != 0) {
                std::cout << "ERROR::ADD_BUFFER_DATA::STRIDE LENGTH MISMATCH" << std::endl;
                return;
            }

            vertexLengths.push_back(vertexLength);
            offsets.push_back(vertexData.size());

            for (float each : input) {
                vertexData.push_back(each);
            }
            attributes++;
        }

        void addElement(std::vector<int> e) {
            for (int each : e) {
                indices.push_back(each);
            }
        }

        void initBuffers() {
            // Bind VAO
            glBindVertexArray(vertexArray);

            // Bind and point VBO to data
            glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
            glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), &vertexData[0], GL_STATIC_DRAW);

            // Bind and point EBO to data
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(int), &indices[0], GL_STATIC_DRAW);

            // Assign attributes
            int offset = 0;
            for (int i = 0; i < attributes; i++) {
                offset += offsets[i];
                glVertexAttribPointer(i, vertexLengths[i], GL_FLOAT, GL_FALSE, 0, (void*)(offset * sizeof(float)));
                glEnableVertexAttribArray(i);
            }
        }

        GLuint getVertexArray() { return vertexArray; }
        int getElementCount() { return indices.size(); }

    private:
        GLuint vertexArray, vertexBuffer, elementBuffer;
        int attributes, maxAttributes;
        std::vector<float> vertexData;
        std::vector<int> indices;
        std::vector<int> offsets;
        std::vector<int> vertexLengths;
    };

    class ObjectManager {
    public:
        ObjectManager() {
            activeObject = nullptr;
            glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxAttr);
        }
        ~ObjectManager() {
            // Unbind any VAO that may be bound
            glBindVertexArray(0);

            // Clean up Objects stored in OM
            for (auto& o : objects) {
                // Deconstruct Object
                o.second->~Object();
                // Deallocate memory for Object
                delete o.second;
            }
        }

        void createObject(std::string name) {
            // New object pointer
            if (objects.find(name) != objects.end()) return;
            objects[name] = new Object(maxAttr);
        }

        void destroyObject(std::string name) {
            if (objects.find(name) == objects.end()) return;
            // Deallocate memory for Object
            delete objects[name];
            // Remove from map
            objects.erase(name);
        }

        void setActiveObject(std::string name) {
            if (objects.size() == 0) return;
            if (objects.find(name) == objects.end()) return;
            if (objects[name] == activeObject) return;

            // Set active object and bind buffers
            activeObject = objects[name];
            glBindVertexArray(activeObject->getVertexArray());
        }

        Object* operator[](std::string name) {
            return objects[name];
        }

        int activeElementCount() { return activeObject->getElementCount(); }

    private:
        int maxAttr;
        Object* activeObject;
        std::map<std::string, Object*> objects;
    };
}

Currently, I've tried different shaders that don't take attributes and these render fine. If I create a shader that specifies a constant color, that seems to render fine. Only when I introduce attributes and attempt to call glAttribPointer(...) and glEnableVertexAttribArray(...) for more than one attribute, it doesn't seem to render the attributes after position data. I've tried creating more buffers for each attribute and this seems to work although I'd like to keep all data tightly packed in one buffer for each object. I've tried forcing indices for glAttribPointer but even magic numbers for indices corresponding to layout (location=n)... don't seem to work either. I've tried explicitly rebinding the buffer with glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer) before calling glAttribPointer but that doesn't seem to affect any changes. I've attempted to unbind the EBO and VBO before rendering but that didn't make a difference either. Finally, I've enabled a console log if any OpenGL errors are raised before rendering; but, I don't get any errors to the console.


Solution

  • You're doing everything right but you're forgetting that enums start from 0. So here:

    for (int i = 1; i < LAST; i++) {
        if ((shaderFlags & (1 << i)) == (1 << VERTEX)) {
            glAttachShader(shaderProgram, vertexShader);
        }
        if ((shaderFlags & (1 << i)) == (1 << FRAGMENT)) {
            glAttachShader(shaderProgram, fragmentShader);
        }
    }
    

    You should start iterating from 0 instead of 1! (Same in your cleanup code!) You're not actually linking your vertex shader, so your attributes aren't being sent through to your fragment shader.

    And for future reference allow me to introduce you to Renderdoc! Whenever something doesn't work just run it through here first and make sure everything is how you expect it to be...