8
\$\begingroup\$

This program draws a random music note on a staff, refreshing the note every second. It compiles with g++ and also emscripten, so it is runnable in both the browser and on desktop.

This is my first attempt at graphics programming, so I'm not interested in feedback on how I'm using OpenGL. In particular, I'm vaguely considered that I'm using shaders poorly. Do I really write a new shader program for each sort of thing I have to draw?

My code seems tightly coupled to OpenGL. I would really love tips to make this less coupled.

This code uses Bitmap.h, Bitmap.cpp, platform.hpp, and platform_linux.cpp by Thomas Dalling. These files assume that stb_image.h is located on the system include path. shader.cpp is modified from the shader code in this tutorial, but it's just standard shader-loading code. This project uses a unity build.

Output: Program output

Makefile

all: a.out index.html

EMS_OPTS = -s FULL_ES3=1 \
       -s WASM=1 \
       -s NO_EXIT_RUNTIME=1 \
       -s USE_GLFW=3 \
       -s USE_LIBPNG=1 \
       -DEMSCRIPTEN

CXX_FLAGS = -Wall -Wextra -std=c++17
LD_LFAGS = -lGL -lGLU -lglfw -lGLEW -lpng

index.html: main.cpp Drawable.cpp Drawable.h
    emcc main.cpp -o index.html ${CXX_FLAGS} ${EMS_OPTS} --preload-file resources/whole-note.png

a.out: main.cpp Drawable.cpp Drawable.h
    g++ -g main.cpp ${CXX_FLAGS} ${LD_LFAGS} 

Drawable.h

#ifndef DRAWABLE_H_
#define DRAWABLE_H_

#include <chrono>
#include <memory>
#include <vector>

class Drawable
{
public:
    virtual ~Drawable() {}
    virtual void draw() const = 0;
    virtual void update() {};
    Drawable(const Drawable& other) = delete;
    Drawable& operator=(const Drawable& other) = delete;
    Drawable() {}
};

class Note : public Drawable
{
public:
    Note(GLuint program);
    ~Note();
    virtual void draw() const;

    void setY(double value);
private:
    GLuint vao, vbo, ibo, texture, program;
};

class Staff : public Drawable
{
public:
    Staff(GLuint program, Note& note);
    ~Staff();
    virtual void draw() const;
    virtual void update();
private:
    GLuint vao, vbo, program;
    static const GLfloat points[30];
    Note& note;
    std::chrono::time_point<std::chrono::system_clock> start;
    std::vector<float> valid_positions;
};

#endif

Drawable.cpp

#include <algorithm>
#include <random>
#include "Drawable.h"

namespace
{
GLfloat vertices[] = {
     // X,    Y,   Z      U,   V
        0.2,  0.0, 0.0,   1.0, 1.0,
        -0.2, 0.0, 0.0,   0.0, 1.0,
        0.2,  0.0, 0.0,   1.0, 0.0,
        -0.2, 0.0, 0.0,   0.0, 0.0,
    };
}

Note::Note(GLuint program) :
    program(program)
{
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // X, Y, Z (dest) coordinates
    glVertexAttribPointer(0,
                          3,
                          GL_FLOAT,
                          GL_FALSE,
                          5 * sizeof(GL_FLOAT),
                          NULL);
    glEnableVertexAttribArray(0);

    // U, V (src) coordinates
    glVertexAttribPointer(1,
                          2,
                          GL_FLOAT,
                          GL_FALSE,
                          5 * sizeof(GL_FLOAT),
                          (const GLvoid *)(3 * sizeof(GL_FLOAT)));
    glEnableVertexAttribArray(1);

    // TODO: Pass this in?
    auto bmp = tdogl::Bitmap::bitmapFromFile(ResourcePath("whole-note.png"));
    bmp.flipVertically();

    // Index buffer object
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    static const GLuint indexData[] = {
        0, 1, 2,
        2, 1, 3
    };
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indexData), indexData,
                 GL_STATIC_DRAW);
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D,
            0,
            GL_RGBA,
            (GLsizei)bmp.width(),
            (GLsizei)bmp.height(),
            0,
            GL_RGBA,
            GL_UNSIGNED_BYTE, bmp.pixelBuffer());
    glBindTexture(GL_TEXTURE_2D, 0);
}

void Note::setY(double value)
{
    static const float HALF_DIM = .2;
    vertices[1] = value + HALF_DIM;
    vertices[6] = value + HALF_DIM;
    vertices[11] = value - HALF_DIM;
    vertices[16] = value - HALF_DIM;

    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

Note::~Note()
{
    glDeleteProgram(program);
    glDeleteBuffers(1, &vbo);
    glDeleteBuffers(1, &ibo);
    glDeleteVertexArrays(1, &vao);
    glDeleteTextures(1, &texture);
}

void Note::draw() const
{
    glUseProgram(program);
    glBindVertexArray(vao);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);

    GLint uniform = glGetUniformLocation(program, "texture_");
    glUniform1i(uniform, 0);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

    glBindVertexArray(0);
}

Staff::Staff(GLuint program, Note& note) :
    program(program),
    note(note),
    start(std::chrono::system_clock::now()),
    valid_positions{.5, .55, .6, .65, .7, .75, .8, .85}
{
    note.setY(valid_positions[0]);
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);

    glVertexAttribPointer(
            0,
            3,
            GL_FLOAT,
            GL_FALSE,
            0,
            (void*)0
            );
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
}

Staff::~Staff()
{
    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
    glDeleteProgram(program);
}

void Staff::draw() const
{
    glUseProgram(program);
    glBindVertexArray(vao);
    glDrawArrays(GL_LINES, 0, 10);

    note.draw();
}

const GLfloat Staff::points[30] = {
    -1.0f, 0.9f, 0.0f,
    1.0f, 0.9f, 0.0f,

    -1.0f, 0.8f, 0.0f,
    1.0f, 0.8f, 0.0f,

    -1.0f, 0.7f, 0.0f,
    1.0f, 0.7f, 0.0f,

    -1.0f, 0.6f, 0.0f,
    1.0f, 0.6f, 0.0f,

    -1.0f, 0.5f, 0.0f,
    1.0f, 0.5f, 0.0f,
};

void Staff::update()
{
    const auto current_time = std::chrono::system_clock::now();
    const auto duration = std::chrono::duration_cast<std::chrono::seconds>(current_time - start).count();
    if (duration >= 1)
    {
        start = std::chrono::system_clock::now();

        std::vector<float> new_position;
        std::sample(valid_positions.begin(), valid_positions.end(),
                    std::back_inserter(new_position), 1,
                    std::mt19937{std::random_device{}()});
        note.setY(new_position[0]);
    }
}

main.cpp

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <memory>

#ifdef EMSCRIPTEN
    #include <GLES3/gl3.h>
    #include "emscripten.h"
#else
    #include <GL/glew.h>
    #include <unistd.h>
#endif

#include <GLFW/glfw3.h>
#include "platform_linux.cpp"
#include "Bitmap.h"
#include "Bitmap.cpp"
#include "shader.hpp"
#include "shader.cpp"
#include "Drawable.h"
#include "Drawable.cpp"

#define FRAMES_PER_SECOND 30

#ifdef EMSCRIPTEN
static const char* noteVertSource = "\
precision mediump float;\n\
attribute vec4 position;\n\
attribute vec2 verTexCoord;\n\
varying vec2 fragTexCoord;\n\
void main()\n\
{\n\
    fragTexCoord = verTexCoord;\n\
    gl_Position = position;\n\
}";

static const char* noteFragSource = "\
precision mediump float;\n\
uniform sampler2D texture_;\n\
varying vec2 fragTexCoord;\n\
void main()\n\
{\n\
    gl_FragColor = texture2D(texture_, fragTexCoord);\n\
}";

#else
static const char* noteVertSource = "\
attribute vec4 position;\n\
attribute vec2 verTexCoord;\n\
varying vec2 fragTexCoord;\n\
void main()\n\
{\n\
    fragTexCoord = verTexCoord;\n\
    gl_Position = position;\n\
}";

static const char* noteFragSource = "\
uniform sampler2D texture_;\n\
varying vec2 fragTexCoord;\n\
void main()\n\
{\n\
    gl_FragColor = texture2D(texture_, fragTexCoord);\n\
}";

#endif


static const char* vertSource = "\
#ifdef EMSCRIPTEN\n\
precision mediump float;\n\
#endif\n\
attribute vec4 position;\n\
void main()\n\
{\n\
    gl_Position = position;\n\
}";

static const char* fragSource = "\
#ifdef EMSCRIPTEN\n\
precision mediump float;\n\
#endif\n\
void main()\n\
{\n\
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n\
}";

void GLAPIENTRY
MessageCallback( GLenum source,
                 GLenum type,
                 GLuint id,
                 GLenum severity,
                 GLsizei length,
                 const GLchar* message,
                 const void* userParam )
{
    (void) source;
    (void) id;
    (void) length;
    (void) userParam;
    const char *repr = type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "";
    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
            repr, type, severity, message );
}

class GLFWContext
{
public:
    GLFWContext(size_t width, size_t height, const char *name)
    {
        // Initialise GLFW
        if( !glfwInit() )
        {
            throw std::runtime_error("Failed to initialize GLFW");
        }

        glfwWindowHint(GLFW_SAMPLES, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // To make MacOS happy; should not be needed
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

        // Open a window and create its OpenGL context
        window = glfwCreateWindow(width, height, name, nullptr, nullptr);
        if (!window)
        {
            glfwTerminate();
            throw std::runtime_error("Failed to open GLFW window. "\
                    "If you have an Intel GPU, they are not 3.3 compatible. "\
                    "Try the 2.1 version of the tutorials.\n");
        }
        glfwMakeContextCurrent(window);
    }

    ~GLFWContext()
    {
        glfwDestroyWindow(window);
        glfwTerminate();
    }

    bool isActive()
    {
        return glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&
                glfwWindowShouldClose(window) == 0;
    }

    void update()
    {
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

private:
    GLFWwindow* window;
};

class Game : public Drawable
{
public:
    Game(GLFWContext& glfw) : glfw(glfw) {}

    virtual void draw() const
    {
        glClear(GL_COLOR_BUFFER_BIT);
        for (auto& object : objects)
            object->draw();
    }

    inline void add(std::unique_ptr<Drawable> object)
    {
        objects.push_back(std::move(object));
    }

    virtual void update()
    {
        for (auto& object : objects) object->update();
        glfw.update();
    }
private:
    GLFWContext& glfw;
    std::vector<std::unique_ptr<Drawable>> objects;
};


void main_loop(void* arg)
{
#ifndef EMSCRIPTEN
    const auto start = std::clock();
#endif
    Game *game = (Game *)arg;
    assert(game);

    game->draw();
    game->update();

    unsigned int err = 0;
    while ( (err = glGetError()) )
    {
        std::cerr << err << "\n";
    }

#ifndef EMSCRIPTEN
    const auto seconds_elapsed = (std::clock() - start) /
        static_cast<double>(CLOCKS_PER_SEC);
    if (seconds_elapsed < 1.0 / FRAMES_PER_SECOND)
    {
        usleep(1000000 * (1.0 / FRAMES_PER_SECOND - seconds_elapsed));
    }
#endif
}

int main( void )
{
    GLFWContext context(1024, 768, "Music game");

    // Initialize GLEW
#ifndef EMSCRIPTEN
    glewExperimental = true; // Needed for core profile
    if (glewInit() != GLEW_OK) {
        fprintf(stderr, "Failed to initialize GLEW\n");
        return -1;
    }

    glEnable(GL_DEBUG_OUTPUT);
    glDebugMessageCallback(MessageCallback, 0);
#endif

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glClearColor(0.9f, 0.9f, 0.9f, 1.0f);

    // Create and compile our GLSL program from the shaders
    const GLuint linesProgram = LoadShaders(vertSource, fragSource);
    const GLuint noteProgram = LoadShaders(noteVertSource, noteFragSource);

    Game game(context);
    Note note(noteProgram);
    note.setY(.75);
    game.add(std::unique_ptr<Drawable>(
                new Staff(linesProgram, note)));

#ifdef EMSCRIPTEN
    emscripten_set_main_loop_arg(main_loop, &game, FRAMES_PER_SECOND, 1);
#else
    do{
        main_loop(&game);
    } while (context.isActive());
#endif

    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ This looks very interesting, can you add some images so it attracts more people to answer. :) \$\endgroup\$ – bhathiya-perera Mar 28 at 10:05
  • \$\begingroup\$ @422_unprocessable_entity Thank you for the advice. Edited. \$\endgroup\$ – User319 Mar 28 at 21:44
5
+50
\$\begingroup\$

Advice

Rather than use escaped characters.

static const char* noteVertSource = "\
precision mediump float;\n\
attribute vec4 position;\n\
attribute vec2 verTexCoord;\n\
varying vec2 fragTexCoord;\n\
void main()\n\
{\n\
    fragTexCoord = verTexCoord;\n\
    gl_Position = position;\n\
}";

You can now use RAW strings:

static const char* noteVertSource = R"FUNC(
precision mediump float;
attribute vec4 position;
attribute vec2 verTexCoord;
varying vec2 fragTexCoord;
void main()
{
    fragTexCoord = verTexCoord;
    gl_Position = position;
};
)FUNC";

Note: FUNC is an arbitrary string (that can be empty). It just matches the characters between R"<del>( and )<del>"

Code Review:

Don't use C style casts:

                      (const GLvoid *)(3 * sizeof(GL_FLOAT)));

C++ has its own casts to make things obvious.

reinterpret_cast<const GLvoid *>(3 * sizeof(GL_FLOAT))

By using the C++ casts you make it easy to search for dangerous casts (like this).


\$\endgroup\$
  • \$\begingroup\$ @user1118321 You are correct (and have fixed). But the compiler would not have noticed. As the close quotes required a )<del>" the other quote would have been naturally embedded in the string (which may have been an issue from some other system but not the C++ compiler). \$\endgroup\$ – Martin York Mar 28 at 4:26
  • \$\begingroup\$ Learning about raw strings is by far the best thing to happen to me this week. \$\endgroup\$ – User319 Mar 28 at 21:44

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.