Опыт изучения OpenGL — Часть 6 — Улучшаем программу рисования треугольника

В прошлой заметке мы нарисовали треугольник с использованием практически голого OpenGL. Начнем теперь видоизменять программу. Цель — сделать ее более простой для понимания и менее подверженной ошибкам.

Предпосылки для написания своего фреймворка

У приведенной в прошлой заметке программы рисования треугольника есть следующие потенциальные возможности для улучшения:

  • Очень много вызовов всяких функций, выполняющих очень мелкие операции в рамках некой сравнительно крупной задачи. Например, чтобы скомпоновать шейдерную программу, надо вызвать функции glCreateProgram, glAttachShader, glLinkProgram, glDetachShader. Ясно, что в подобных случаях все эти вызовы функций помещают в отдельную функцию.
  • У функций много параметров. Причем эти параметры логически взаимосвязаны, т. е. если параметр A функции Foo имеет значение X, то параметр B функции Bar должен иметь значение Y, иначе программа не заработает. Таким образом мы имеем избыточность: программист должен руками вписывать правильные значения параметров функций, хотя эти значения мог бы логически вывести одно из другого или компилятор, или сама программа во время выполнения. Ниже я попытался стрелками изобразить то, как значения одних параметров функции glVertexAttribPointer непосредственно зависят от других:
    Рисунок 1 — Как одни значения параметров функций зависят от других.

    Как заставить компилятор выводить значения параметров? В этом нам помогут шаблоны (templates) и макросы.
  • Объектная ориентированность API OpenGL. Несмотря на то, что OpenGL — это C API, концепция объект просматривается в нем невооруженным взглядом. Пример: функции glGenBuffers, glBindBuffer и glBufferData все относятся к одному и тому же объекту — буферу. Ссылка на этот объект осуществляется через его дескриптор (хэндл) — GLuint m_VertexBufferObject. Напрашивается идея сделать эту объектную ориентированность явной, т. е. создать класс Buffer, в котором будет поле GLuint m_Handle; конструктор, в котором вызывается функция glGenBuffers; методы Bind и setData.

Эксплуатируем объектную ориентированность OpenGL

Итак, я воспользовался объектной ориентированностью API OpenGL и создал ряд классов, соответствующих объектной модели OpenGL: opengl::Shader, opengl::Program, opengl::Buffer, opengl::VertexArrayObject. Я не буду приводить здесь исходный код всех классов, вы найдете его в репозитории. Приведу только один характерный и, что немаловажно, короткий пример — класс opengl::Shader:

// Shader.cpp

#include <string>
#include <sstream>
#include <memory>

#include "GLEWHeaders.h"
#include "GlException.h"
#include "File.h"

namespace opengl
{
    class Shader final
    {
        public:
            enum class SHADER_TYPE : GLenum
            {
                VERTEX = GL_VERTEX_SHADER,
                FRAGMENT = GL_FRAGMENT_SHADER,
                TESS_CONTROL = GL_TESS_CONTROL_SHADER,
                TESS_EVALUATION = GL_TESS_EVALUATION_SHADER,
                GEOMETRY = GL_GEOMETRY_SHADER,
                COMPUTE = GL_COMPUTE_SHADER
            };

        private:
            GLuint m_Handle;
            SHADER_TYPE m_ShaderType;

        public:
            Shader(SHADER_TYPE shader_type,
                   const std::string& shaderText) :
            m_Handle(glCreateShader(static_cast<GLenum>(shader_type))),
            m_ShaderType(shader_type)
            {
                const GLchar* text = shaderText.c_str();

                // load text into shader object
                glShaderSource(
                    m_Handle, // handle to shader
                    1,        // number of С-strings (zero-ended strings) in the text
                    &text,    // shader text
                    NULL);    // array of C-strings' lengths

                // compile shader
                glCompileShader(m_Handle);

                // here compilation error check must go which I skipped for the sake of simplicity
            }

            // Move constructor.
            Shader(Shader&& shader) :
                m_Handle(shader.m_Handle),
                m_ShaderType(shader.m_ShaderType)
            {
                shader.m_Handle = 0;
            }

        public:
            ~Shader()
            {
                glDeleteShader(m_Handle);
                m_Handle = 0;
            }

        private:
            Shader(const Shader&) = delete;
            Shader& operator=(const Shader&) = delete;

        public:
            const SHADER_TYPE getShaderType() const { return m_ShaderType; }
            const GLuint getHandle() const { return m_Handle; }
    };

    // Makes a shader object out of the specified text file.
    inline Shader make_ShaderFromFile(
        Shader::SHADER_TYPE shader_type,
        const std::string& fileName)
    {
        std::string shaderText = util::file::ReadAllText(fileName);
        return Shader(shader_type, shaderText);
    }
}

На примере этого класса можно поговорить об общих принципах построения подобных классов. Ниже мне придется часто ссылаться на книжки Бьярна Страуструпа и Скотта Мейерса:

  • Bjarne Stroustrup — The C++ Programming Language. Fourth Edition.
  • Скотт Мейерс — Эффективное использование C++.
  • Scott Meyers — Effective Modern C++.

Числовые константы — перечисления

Функции OpenGL часто принимают в качестве параметров значения типа GLenum. Например функция glCreateShader принимает в качестве параметра значение типа GLenum — тип шейдера. По смыслу эти параметры — перечисления, т. е. они должны принимать лишь ограниченный набор значений. Например в функцию glCreateShader можно передать в качестве параметра одно из шести допустимых значений: GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER и GL_COMPUTE_SHADER. Однако на самом деле GLenum — это просто целое число без знака (unsigned int), а GL_VERTEX_SHADER, GL_FRAGMENT_SHADER и пр. — это числовые константы-макроопределения, определенные (при помощи директивы #define) в заголовочном файле glew.h (если вы пользуетесь библиотекой GLEW). Таким образом, если вместо одного из допустимых значений мы передадим в функцию glCreateShader какое-нибудь недопустимое значение (скажем, GL_UNIFORM или GL_FLOAT), то для компилятора это будет нормальная ситуация, а об ошибке мы узнаем только во время выполнения программы. Чтобы заставить компилятор контролировать значения параметров, надо сделать тип шейдера реальным перечислением enum:

enum class SHADER_TYPE : GLenum
{
    VERTEX = GL_VERTEX_SHADER,
    FRAGMENT = GL_FRAGMENT_SHADER,
    TESS_CONTROL = GL_TESS_CONTROL_SHADER,
    TESS_EVALUATION = GL_TESS_EVALUATION_SHADER,
    GEOMETRY = GL_GEOMETRY_SHADER,
    COMPUTE = GL_COMPUTE_SHADER
};

Конструктор класса opengl::Shader принимает в качестве параметра не GLenum, а SHADER_TYPE, и поэтому у нас не получится по ошибке передать ему значение, которое не является одним из шести перечисленных выше допустимых значений.

Конструктор и деструктор

Обычно для создания объекта в OpenGL вызывается функция с именем типа glCreateObject, а для уничтожения объекта — функция glDeleteObject. Поэтому первую мы помещаем в конструктор, а вторую — в деструктор класса.

Конструктор перемещения (move constructor)

Move semantics — это очень полезная штука, появившаяся в стандарте языка C++ 11. Она позволяет заменить дорогостоящую операцию копирования дешевой операцией перемещения. Объяснять, что такое это самое перемещение — долго. Почитайте [Stroustrup — Chapter 17. Construction, Cleanup, Copy and Move] и [Meyers — Chapter 5. Rvalue References, Move Semantics, and Perfect Forwarding.]. В случае OpenGL копирование объектов на мой взгляд вообще следует запретить, поскольку внутри реализации OpenGL каждый объект существует в единственном экземпляре. А если мы запрещаем копирование, то перемещение нас сильно выручит. Например в файле Shader.h есть функция make_ShaderFromFile, которая возвращает объект типа opengl::Shader. При возврате управления именно перемещение и происходит, и не будь в классе opengl::Shader конструктора перемещения, создать функцию типа make_ShaderFromFile было бы невозможно.

Удаленные функции (deleted functions)

Некоторые функции-члены класса, такие как конструктор копирования и оператор присваивания, компилятор может сгенерировать самостоятельно. Для некоторых классов программист может захотеть запретить копирование. До появления стандарта C++ 11 для этого нужно было объявить в классе конструктор копирования и оператор присваивания и сделать их приватными функциями без реализации [Мейерс — Глава 2. Правило 6. Явно запрещайте компилятору генерировать функции, которые вам не нужны]. В стандарте C++ 11 появилось понятие deleted function, которое позволяет запретить использование функций, таких как конструкторы копирования, операторы присваивания и преобразования. В своем фреймворке я запретил копирование для всех объектов OpenGL. Например для класса opengl::Shader:

class Shader
{
...
    private:
        Shader(const Shader&) = delete;
        Shader& operator=(const Shader&) = delete;
...
}

Свойства

Программируя на C#, я привык к концепции свойства. Свойство — это ни что иное как пара методов: get и set. get возвращает значение свойства, а set — задает значение свойства. В C# обращение (считывание или присваивание) к свойству выглядит ровно так же, как обращение к полю класса, хотя на самом деле при считывании вызывается get, а при присваивании — set. В C++ такого синтаксиса свойств нет, но сама концепция свойств на мой взгляд — красивая. Я никогда не делаю поля класса открытыми [Мейерс — Глава 4. Правило 22. Объявляйте данные-члены закрытыми]. Если поле должно быть доступно для чтения, то я добавляю в класс метод getИмяПоля. Если оно должно быть доступно также и для записи, то я добавляю метод setИмяПоля. Например, в том же классе opengl::Shader есть свойство только для чтения «ShaderType», поэтому там есть метод getShaderType. Когда речь идет о свойствах только для чтения, на сцену выходит ключевое слово const, которое, как известно, следует использовать где только можно [Мейерс — Глава 1. Правило 3. Везде, где только можно, используйте const].

Привязывание к контексту OpenGL и паттерн RAII

В OpenGL много объектов, которые нужно привязывать к контексту OpenGL, чтобы как-то их использовать. Мне кажется, что полезно также их отвязывать от контекста, как только работа с ними завершена. Я полагаю, что это сделает работу программы более предсказуемой. Идея тут такая же, как при захвате ресурсов: захватил ресурс, поработал с ним, освободил ресурс. Привязал объект, поработал с ним, отвязал объект. И вот в таких случаях используют паттерн программирования под названием RAII (Resource Acquisition is Initialization). Суть его в том, что для управления неким ресурсом (под управлением понимается захват и освобождение ресурса) создают класс. В конструкторе класса ресурс захватывается, а в деструкторе — освобождается. Для захвата ресурса достаточно объявить локальную переменную (экземпляр класса, о котором мы только что говорили). Когда локальная переменная создается, вызывается конструктор класса, который захватывает ресурс. Когда поток управления выходит за пределы области видимости локальной переменной, эта переменная уничтожается, т. е. вызывается деструктор класса, который освобождает ресурс. Причем переменная уничтожается гарантированно, даже в случае если в потоке управления произошло исключение. Таким образом, захватив ресурс, вы гарантированно его освободите. Больше подробностей — в [Мейерс — Глава 3. Правило 13. Используйте объекты для управления ресурсами].

Перенося эту практику на привязки в OpenGL, для каждого типа объекта, который надо привязывать к контексту, я создаю класс, в конструкторе которого выполняется привязывание, а в деструкторе — отвязывание объекта. Для примера можно рассмотреть класс opengl::VertexArrayObject. Для него я написал вложенный класс opengl::VertexArrayObject::Binding.

class VertexArrayObject
{
    ...

    public:
        // This class is nested into class VertexArrayObject
        class Binding final
        {
            private:
                GLuint m_PreviousVAO;

            private:
                Binding(const Binding&) = delete;
                Binding& operator=(const Binding&) = delete;

            public:
                Binding(VertexArrayObject& vao)
                    : m_PreviousVAO(VertexArrayObject::getCurrentlyBoundVAOHandle())
                {
                    vao.Bind();
                }

                ~Binding()
                {
                    glBindVertexArray(m_PreviousVAO);
                }
        };

    ...
}

На этом примере видно, что я понимаю под «отвязыванием» объекта от контекста — привязку к контексту того объекта, который был привязан к нему ранее (что логично). Как подобный класс использовать? Надо просто объявить локальную переменную типа Binding, когда вы хотите привязать объект к контексту:

void DoSomeRendering(VertexArrayObject& vao)
{
    VertexArrayObject::Binding vao_binding(vao); // here we bind the VAO by declaring local variable "vao_binding"
    glDrawArrays(GL_TRIANGLES, 0, 3); // do some rendering using the VAO
    // here the local variable "vao_binding" is going to be destroyed which means that the VAO is going to be unbound
}

Программа, рисующая разноцветный треугольник при помощи классов из пространства имен opengl

После того, как разработаны классы, повторяющие объектную модель OpenGL, можно переписать программу рисования треугольника, сделав ее немного проще:

// Program.h

#include "ProgramBase.h"
#include "OpenGLProgram.h"
#include "Buffer.h"
#include "VertexArrayObject.h"

class Program final : public engine::ProgramBase
{
    private:
        opengl::Program m_Program;
        std::unique_ptr<opengl::Buffer> m_VertexBufferObject;
        opengl::VertexArrayObject m_VAO;

    public:
        Program() :
            m_Program(
                std::vector<opengl::Shader*>
                {
                    &opengl::make_ShaderFromFile(opengl::Shader::SHADER_TYPE::VERTEX, "../../shaders/passthrough.vert"),
                    &opengl::make_ShaderFromFile(opengl::Shader::SHADER_TYPE::FRAGMENT, "../../shaders/passthrough.frag")
                }),

            m_VAO()
        {
            // --- Create buffer and populate it with vertex data ---
            GLfloat vertexData[] {
                -0.5f, 0.5f, 0.5f,      // 1st vertex coordinates
                1.0f, 0.0f, 0.0f, 1.0f, // 1st vertex color = red   (R=1, G=0, B=0, A=1)

                0.0f, -0.5f, 0.5f,      // 2nd vertex coordinates
                0.0f, 1.0f, 0.0f, 1.0f, // 2nd vertex color = green (R=0, G=1, B=0, A=1)

                0.5f, 0.5f, 0.5f,       // 3rd vertex coordinates
                0.0f, 0.0f, 1.0f, 1.0f  // 3rd vertex color = blue  (R=0, G=0, B=1, A=1)
            };

            m_VertexBufferObject = std::make_unique<opengl::Buffer>(
                sizeof(vertexData),
                opengl::Buffer::TARGET::ARRAY,
                opengl::Buffer::DRAW_USAGE::STATIC_DRAW,
                vertexData);

            // --- Create Vertex Array Object (VAO) and store vertex data format in it ---
            opengl::VertexArrayObject::Binding bind_vao(m_VAO);

            opengl::Buffer::Binding binding(
                *m_VertexBufferObject,
                opengl::Buffer::TARGET::ARRAY);

            GLuint positionAttribLocation = m_Program.getAttributeLocation("v3_position");
            glVertexAttribPointer(
                /*index*/      positionAttribLocation,
                /*size*/       3,
                /*type*/       GL_FLOAT,
                /*normalized*/ GL_FALSE,
                /*stride*/     sizeof(GLfloat) * 7,
                /*pointer*/    nullptr
            );
            glEnableVertexAttribArray(positionAttribLocation);

            GLuint colorAttribLocation = m_Program.getAttributeLocation("v4_color");
            glVertexAttribPointer(
                /*index*/      colorAttribLocation,
                /*size*/       4,
                /*type*/       GL_FLOAT,
                /*normalized*/ GL_FALSE,
                /*stride*/     sizeof(GLfloat) * 7,
                /*pointer*/    (void*)(sizeof(GLfloat)*3)
            );
            glEnableVertexAttribArray(colorAttribLocation);
        }

    protected:
        void UpdateTime(double currentTime, double timeDelta) override { }
        void setupCamera(const engine::Camera& cam) override { }

        void OnRender(opengl::OpenGLWindow* sender) override
        {
            opengl::Program::Using use(m_Program);
            opengl::VertexArrayObject::Binding bind_vao(m_VAO);
            glDrawArrays(GL_TRIANGLES, 0, 3);
        }
};

Здесь я был вынужден сделать m_VertexBufferObject указателем, поскольку я не мог создать объект типа opengl::Buffer в списке инициализации так, чтобы это было красиво. Заметьте, что указатель этот не простой, а std::unique_ptr, что позволяет не беспокоиться об удалении объекта (для обычного указателя надо было бы не забыть вызвать оператор delete в деструкторе класса Program).

Решаем проблемы с избыточностью параметров. Создаем систему классов Vector, Vertex, Color

В самом начале этой заметки я говорил, что у некоторых функций OpenGL много параметров и при этом нередко значения одних параметров определяются значениями других (см. рисунок 1 выше). Речь конечно же идет о функциях, которые задают формат вершинных данных, например glVertexAttribPointer. Можно заставить компилятор взять на себя работу по определению того, какие значения должны иметь некоторые параметры. Для этого у нас есть арсенал средств. Во-первых, это операторы sizeof, decltype, offsetof. Во-вторых — шаблоны (templates). Чтобы воспользоваться всем этим, нам придется для начала типизировать вершинные данные — т. е. создать описать эти данные в виде типов (классов, структур). Почему это нужно? Вы это сейчас увидите на примере, но могу сказать сразу: типизация дает компилятору информацию о формате данных, а именно формат данных мы и пытаемся сообщить OpenGL при помощи функции glVertexAttribPointer. О’кей, какова структура отдельной вершины? Я уже об этом говорил, в примере с рисованием треугольника вершина — это позиция (координаты) и цвет. Описать вершину в виде структуры можно было бы так:

// Represents a vertex.
// "PC" stands for Position & Color.
struct VertexPC
{
    vec3 Position;
    color_rgba32f Color;
}

Что такое vec3 и color_rgba32f? Вот что:

// 3-dimensional vector.
struct vec3
{
    using value_type = GLfloat;
    static constexpr GLsizei components_number = 3;

    GLfloat X;
    GLfloat Y;
    GLfloat Z;
}

// Color in RGBA format.
// "32f" stands for 32-bit floating point type of each channel.
struct color_rgba32f
{
    using value_type = GLfloat;
    static constexpr GLsizei components_number = 4;

    GLfloat R; // red channel (0...1)
    GLfloat G; // green channel (0...1)
    GLfloat B; // blue channel (0...1)
    GLfloat A; // alpha channel (0...1)
}

Вообще-то на самом деле в моем проекте эти типы имеют немного другой, чуть более сложный, вид — его мы обсудим в следующих заметках. А для текущей заметки сойдет и тот код, который представлен выше. Теперь, с этими типами в руках, мы можем немного переписать конструктор класса Program (привожу его ниже с сокращениями):

Program() :
    m_Program(...),
    m_VAO()
{
    // array of vertices
    engine::VertexPC vertexData[] {
        engine::VertexPC
        {
            vec3{ -0.5f, 0.5f, 0.5f },
            color_rgba32f{ 1.0f, 0.0f, 0.0f, 1.0f }
        },
        engine::VertexPC
        {
            vec3{ 0.0f, -0.5f, 0.5f },
            color_rgba32f{ 0.0f, 1.0f, 0.0f, 1.0f }
        },
        engine::VertexPC
        {
            vec3{ 0.5f, 0.5f, 0.5f },
            color_rgba32f{ 0.0f, 0.0f, 1.0f, 1.0f }
        }
    };

    ...
    ...
    ...

    GLuint positionAttribLocation = m_Program.getAttributeLocation("v3_position");
    glVertexAttribPointer(
        /*index*/      positionAttribLocation,
        /*size*/       decltype(VertexPC::Position)::components_number,
        /*type*/       GL_FLOAT,
        /*normalized*/ GL_FALSE,
        /*stride*/     sizeof(VertexPC),
        /*pointer*/    (void*)offsetof(VertexPC, VertexPC::Position)
    );
    glEnableVertexAttribArray(positionAttribLocation);

    GLuint colorAttribLocation = m_Program.getAttributeLocation("v4_color");
    glVertexAttribPointer(
        /*index*/      colorAttribLocation,
        /*size*/       decltype(VertexPC::Color)::components_number,
        /*type*/       GL_FLOAT,
        /*normalized*/ GL_FALSE,
        /*stride*/     sizeof(VertexPC),
        /*pointer*/    (void*)offsetof(VertexPC, VertexPC::Color)
    );
    glEnableVertexAttribArray(colorAttribLocation);
}

В приведенном коде мы избавились от ручного вписывания значений параметров size, stride и pointer функции glVertexAttribPointer. Как нам это удалось? Давайте по порядку…

size — число компонент (координат вектора) в атрибуте вершины. Для атрибута «позиция» компонент три, для атрибута «цвет» — четыре. Как это число получить? Есть два варианта:
1) его можно рассчитать, разделив размер вектора на размер одной его координаты при помощи выражения (пример для атрибута «позиция»):

sizeof(decltype(VertexPC::Position)) / sizeof(decltype(VertexPC::Position)::value_type)

2) определить в структурах vec3 и color_rgba32f константу components_number и присвоить ей количество компонент (это количество можно рассчитать при помощи вышеприведенного выражения либо просто вбить руками — в примере я так и поступил). Константа должна иметь модификатор constexpr (см. [Straustrup — 2.2.3 Constants]), чтобы она вычислялась на этапе компиляции.

stride — расстояние в байтах между адресами соседних элементов массива вершинных атрибутов. В плотно упакованном (tightly packed) массиве между соседними элементами нет зазоров, поэтому stride равен размеру одного элемента массива — в нашем случае sizeof(VertexPC).

pointer — смещение в байтах конкретного атрибута относительно содержащего его элемента массива. Например, атрибут «позиция» расположен вначале элемента массива (элемент массива имеет тип VertexPC), поэтому смещение его равно 0, а атрибут «цвет» расположен сразу после атрибута «позиция», поэтому смещение его равно размеру атрибута «позиция». Для расчета смещения поля структуры относительно начала этой структуры используется оператор offsetof. Например, смещение атрибута позиция рассчитывается так: offsetof(VertexPC, VertexPC::Position). Аналогично — с атрибутом «цвет».

Остался один параметр функции glVertexAttribPointer, значение которого мы задаем руками — это type. Это тип отдельной компоненты в вершинном атрибуте. Задается он константой, имя которой начинается с GL_ — в нашем случае — GL_FLOAT. Мы знаем, что тип отдельной компоненты вектора vec3 — это GLfloat. Как же нам перейти от GLfloat к GL_FLOAT? Для этого мы используем шаблоны вроде тех, что определены в фале <type_traits> стандартной библиотеки языка C++ (см. [Straustrup — 28.2.4 Traits]). Я создал две шаблонных структуры — opengl::type_traits и opengl::gl_type_traits и поместил их в файл OpenGLTypes.h. strong>opengl::type_traits позволяет по имени типа определить соответствующую ему OpenGL’евскую константу (например GLfloat -> GL_FLOAT), opengl::gl_type_traits — наоборот, по имени константы позволяет определить тип (GL_FLOAT -> GLfloat). Для каждого типа данных надо писать свою специализацию шаблона. Ниже для примера я привожу только по две специализации каждого шаблона.

// opengl::type_traits

template<typename T>
struct type_traits
{};

// template specialization for T=GLuint
template<>
struct type_traits<GLuint>
{
    static constexpr TYPE gl_type = TYPE::UNSIGNED_INT;
};

// template specialization for T=GLfloat
template<>
struct type_traits<GLfloat>
{
    static constexpr TYPE gl_type = TYPE::FLOAT;
};

...
// opengl::gl_type_traits

template<TYPE gl_type>
struct gl_type_traits
{};

// template specialization for TYPE=TYPE::UNSIGNED_INT
template<>
struct gl_type_traits<TYPE::UNSIGNED_INT>
{
    using value_type = GLuint;
};

// template specialization for TYPE=TYPE::FLOAT
template<>
struct gl_type_traits<TYPE::FLOAT>
{
    using value_type = GLfloat;
};

...

Теперь можно снова переписать код, тут уж все параметры будет вычислять компилятор:

Program() :
    m_Program(...),
    m_VAO()
{
    ...
    ...
    ...

    GLuint positionAttribLocation = m_Program.getAttributeLocation("v3_position");
    glVertexAttribPointer(
        /*index*/      positionAttribLocation,
        /*size*/       sizeof(decltype(VertexPC::Position)) / sizeof(decltype(VertexPC::Position)::value_type),
        /*type*/       static_cast<GLenum>(opengl::type_traits<decltype(VertexPC::Position)::value_type>::gl_type),
        /*normalized*/ GL_FALSE,
        /*stride*/     sizeof(VertexPC),
        /*pointer*/    (void*)offsetof(VertexPC, VertexPC::Position)
    );
    glEnableVertexAttribArray(positionAttribLocation);

    GLuint colorAttribLocation = m_Program.getAttributeLocation("v4_color");
    glVertexAttribPointer(
        /*index*/      colorAttribLocation,
        /*size*/       sizeof(decltype(VertexPC::Color)) / sizeof(decltype(VertexPC::Color)::value_type),
        /*type*/       static_cast<GLenum>(opengl::type_traits<decltype(VertexPC::Color)::value_type>::gl_type),
        /*normalized*/ GL_FALSE,
        /*stride*/     sizeof(VertexPC),
        /*pointer*/    (void*)offsetof(VertexPC, VertexPC::Color)
    );
    glEnableVertexAttribArray(colorAttribLocation);

Очень длинно и страшно? Давайте весь этот кошмар спрячем в макрос:

#define glVertexAttribPointerWithFun(ATTRIB_LOCATION, VERTEX, FIELD, NORMALIZED) \
    glVertexAttribPointer( \
        /*index*/      ATTRIB_LOCATION, \
        /*size*/       decltype(VERTEX::FIELD)::components_number, \
        /*type*/       static_cast<GLenum>(opengl::type_traits<decltype(VERTEX::FIELD)::value_type>::gl_type), \
        /*normalized*/ NORMALIZED, \
        /*stride*/     sizeof(VERTEX), \
        /*pointer*/    (void*)offsetof(VERTEX, FIELD) \
        )

Теперь длинный и страшный код станет коротким. Приведу крайнюю на сегодня версию всей программы:

using namespace engine;

class Program final : public ProgramBase
{
private:
    opengl::Program m_Program;
    std::unique_ptr<opengl::Buffer> m_VertexBufferObject;
    opengl::VertexArrayObject m_VAO;

public:
    Program() :
        m_Program(
            std::vector<opengl::Shader*>
            {
                &opengl::make_ShaderFromFile(opengl::Shader::SHADER_TYPE::VERTEX, "../../shaders/passthrough.vert"),
                &opengl::make_ShaderFromFile(opengl::Shader::SHADER_TYPE::FRAGMENT, "../../shaders/passthrough.frag")
            }),
        m_VAO()
    {
        // --- Create buffer and populate it with vertex data ---
        VertexPC vertexData[]
        {
            VertexPC
            {
                vec3{ -0.5f, 0.5f, 0.5f },
                color_rgba32f{ 1.0f, 0.0f, 0.0f, 1.0f }
            },
            VertexPC
            {
                vec3{ 0.0f, -0.5f, 0.5f },
                color_rgba32f{ 0.0f, 1.0f, 0.0f, 1.0f }
            },
            VertexPC
            {
                vec3{ 0.5f, 0.5f, 0.5f },
                color_rgba32f{ 0.0f, 0.0f, 1.0f, 1.0f }
            }
        };

        m_VertexBufferObject = std::make_unique<opengl::Buffer>(
            sizeof(vertexData),
            opengl::Buffer::TARGET::ARRAY,
            opengl::Buffer::DRAW_USAGE::STATIC_DRAW,
            vertexData);

        // --- Create Vertex Array Object (VAO) and store vertex data format in it ---
        opengl::VertexArrayObject::Binding bind_vao(m_VAO);
        opengl::Buffer::Binding binding(*m_VertexBufferObject, opengl::Buffer::TARGET::ARRAY);

        GLuint positionAttribLocation = m_Program.getAttributeLocation("v3_position");
        glVertexAttribPointerWithFun(positionAttribLocation, VertexPC, Position, GL_FALSE);
        glEnableVertexAttribArray(positionAttribLocation);

        GLuint colorAttribLocation = m_Program.getAttributeLocation("v4_color");
        glVertexAttribPointerWithFun(colorAttribLocation, VertexPC, Color, GL_FALSE);
        glEnableVertexAttribArray(colorAttribLocation);
    }

protected:
    void UpdateTime(double currentTime, double timeDelta) override { }
    void setupCamera(const Camera& cam) override { }

    void OnRender(opengl::OpenGLWindow* sender) override
    {
        opengl::Program::Using use(m_Program);
        opengl::VertexArrayObject::Binding bind_vao(m_VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
    }
};

На этом пока всё. В следующих заметках займемся уже не плоскими, а трехмерными объектами и окунемся в мир матриц и векторов.
Более далекий стратегический план таков: насоздавать N штук примеров рисования различных объектов (уже есть разноцветный треугольник, на очереди — одноцветный и разноцветный кубы, куб с наложенной на него текстурой — skybox, сфера с текстурой и т. д.) и посмотреть, что между ними есть принципиально общего. После чего — создать систему классов для этих самых объектов, которые можно нарисовать на экране (забегая вперед: в моем уже написанном фреймворке все эти классы лежат в папке Renderable3dObject).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *