前言

Application.cpp中,设置一组极点到OpenGL的 ArrayBuffer:

float vertices[3 * 3] = {
        -0.5f, -0.5, 0.0,
        0.5f, -0.5f, 0.0f,
        0.0f, 0.5f, 0.0f
};
m_VertexBuffer.reset(VertexBuffer::Create(vertices, sizeof(vertices)));
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), nullptr);

现在只有一种极点特色,即由3个float组成的position(坐标),假设再加上color(色彩)、normal(法线),glEnableVertexAttribArray和glVertexAttribPointer函数就会重复调用。

软件规划中,有重复代码的当地,便是坏味道,应该条件反射的想到抽象,提取相同代码。

本章节经过BufferLayout类,减少glEnableVertexAttribArray和glVertexAttribPointer的重复调用。

从Application倒推BufferLayout规划

为了更好的了解BufferLayout的规划,我们从Application调用的当地出发,从需求倒推BufferLayout应该提供什么样的接口

先来看看glVertexAttribPointer函数的各个参数的含义:

游戏引擎从零开始(16)-BufferLayout抽象

我们需求将这些参数封装起来,不要写死,有多个极点特色时,在for循环里设置。

uint32_t index = 0;
const auto& layout = m_VertexBuffer->GetLayout();
for (const auto& element : layout) {
    glEnableVertexAttribArray(index);
    glVertexAttribPointer(index,
                          element.GetComponentCount(),
                          ShaderDataTypeToOpenGLBaseType(element.Type),
                          element.Normallized ? GL_TRUE : GL_FALSE,
                          layout.GetStride(),
                          (const void*)element.Offset);
    index++;
}

规划BufferLayout数据结构

VertexBuffer中增加BufferLayout字段,BufferLayout里边放置一个BufferElement的列表。

游戏引擎从零开始(16)-BufferLayout抽象

关键看BufferElement的完结,要针对每种类型的数据定义数据size: Sandbox/Hazel/src/Hazel/Renderer/Buffer.h

enum class ShaderDataType {
    None = 0, Float, Float2, Float3, Float4, Mat3, Mat4, Int, Int2, Int3, Int4, Bool
};
/**ShaderDataTypeSize:用于核算偏移*/
static uint32_t ShaderDataTypeSize(ShaderDataType type) {
    switch (type) {
        case ShaderDataType::Float:         return 4;
        case ShaderDataType::Float2:        return 4 * 2;
        case ShaderDataType::Float3:        return 4 * 3;
        case ShaderDataType::Float4:        return 4 * 4;
        case ShaderDataType::Mat3:          return 4 * 3 * 3;
        case ShaderDataType::Mat4:          return 4 * 4 * 4;
        case ShaderDataType::Int:           return 4;
        case ShaderDataType::Int2:          return 4 * 2;
        case ShaderDataType::Int3:          return 4 * 3;
        case ShaderDataType::Int4:          return 4 * 4;
        case ShaderDataType::Bool:          return 1;
    }
    HZ_CORE_ASSERT(false, "Unknow ShaderDataType!")
    return 0;
}

BufferElement纯数据结构,用struct来定义:

struct BufferElement {
    std::string Name;
    ShaderDataType Type;
    uint32_t  Size;
    uint32_t Offset;
    bool Normallized;
    BufferElement(){}
    BufferElement( ShaderDataType type, const std::string &name,bool normallized = false)
            : Name(name), Type(type), Size(ShaderDataTypeSize(type)), Offset(0), Normallized(normallized) {}
    uint32_t GetComponentCount() const {
        switch (Type) {
            case ShaderDataType::Float:         return 1;
            case ShaderDataType::Float2:        return 2;
            case ShaderDataType::Float3:        return 3;
            case ShaderDataType::Float4:        return 4;
            case ShaderDataType::Mat3:          return 3 * 3;
            case ShaderDataType::Mat4:          return 4 * 4;
            case ShaderDataType::Int:           return 1;
            case ShaderDataType::Int2:          return 2;
            case ShaderDataType::Int3:          return 3;
            case ShaderDataType::Int4:          return 4;
            case ShaderDataType::Bool:          return 1;
        }
        HZ_CORE_ASSERT(false, "Unknow ShaderDataType!");
        return 0;
    }
};

BufferLayout:

class BufferLayout {
    public:
        BufferLayout() {}
        BufferLayout(const std::initializer_list<BufferElement>& elements):m_Elements(elements) {
            CalculateOffsetsAndStride();
        }
        inline uint32_t GetStride() const {return m_Stride;}
        inline const std::vector<BufferElement>& GetElements() const {return m_Elements;}
        std::vector<BufferElement>::iterator begin() {return m_Elements.begin();}
        std::vector<BufferElement>::iterator end() {return m_Elements.end();}
        std::vector<BufferElement>::const_iterator begin() const {return m_Elements.begin();}
        std::vector<BufferElement>::const_iterator end() const {return m_Elements.end();}
    private:
        void CalculateOffsetsAndStride() {
            uint32_t offset = 0;
            m_Stride = 0;
            for (auto& element : m_Elements) {
                element.Offset = offset;
                offset += element.Size;
                m_Stride += element.Size;
            }
        }
    private:
        std::vector<BufferElement> m_Elements;
        uint32_t m_Stride = 0;
    };

留意以下几点:

BufferLayout中定义了一组获取std::vector::iterator的方法,C++中完结begin()、end()函数的class支撑快速for循环操作。

BufferLayout中定义了支撑初始化列表参数的结构函数,以支撑{“key”, value}方式的入参,比较便当。

CalculateOffsetsAndStride函数,用于核算一组数据中,不同特色的子数据的偏移,比方有position,color的极点数据,position的偏移为0,color的偏移便是position的数据长度,此处不了解,可能到这篇代码悉数写完,联系上下文来了解。

完善VertexBuffer类:

Sandbox/Hazel/src/Hazel/Renderer/Buffer.h

声明: Sandbox/Hazel/src/Hazel/Renderer/Buffer.h

class VertexBuffer {
public:
    ...
    virtual const BufferLayout& GetLayout() const = 0;
    virtual void SetLayout(const BufferLayout& layout) = 0;
    ...
};

完结: Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLBuffer.h

class OpenGLVertexBuffer : public VertexBuffer {
public:
    ...
    const BufferLayout &GetLayout() const override {
        return m_Layout;
    };
    void SetLayout(const BufferLayout &layout) override {
        m_Layout = layout;
    };
private:
    uint32_t m_RendererID;
    BufferLayout m_Layout;
};

Application中的逻辑替换

定义数据类型转化,我们要做跨渠道的shader,上面定义的数据类型和OpenGL无关,application中我们清晰基于OpenGL来完结,需求转化成OpenGL的类别。

Sandbox/Hazel/src/Hazel/Application.cpp

static GLenum ShaderDataTypeToOpenGLBaseType(ShaderDataType type) {
    switch (type) {
        case ShaderDataType::Float:         return GL_FLOAT;
        case ShaderDataType::Float2:        return GL_FLOAT;
        case ShaderDataType::Float3:        return GL_FLOAT;
        case ShaderDataType::Float4:        return GL_FLOAT;
        case ShaderDataType::Mat3:          return GL_FLOAT;
        case ShaderDataType::Mat4:          return GL_FLOAT;
        case ShaderDataType::Int:           return GL_INT;
        case ShaderDataType::Int2:          return GL_INT;
        case ShaderDataType::Int3:          return GL_INT;
        case ShaderDataType::Int4:          return GL_INT;
        case ShaderDataType::Bool:          return GL_BOOL;
    }
    HZ_CORE_ASSERT(false, "Unknow ShaderDataType!");
    return 0;

vertex数据增加color,丰厚测验事例

Application::Application() {
    ...
    float vertices[3 * 7] = {
            -0.5f, -0.5, 0.0, 0.8f, 0.2f, 0.8f, 1.0f,
            0.5f, -0.5f, 0.0f, 0.2f, 0.3f, 0.8f, 1.0f,
            0.0f, 0.5f, 0.0f, 0.8f, 0.8f, 0.2f, 1.0f
    };
    m_VertexBuffer.reset(VertexBuffer::Create(vertices, sizeof(vertices)));
    ...
}

设置极点特色,前面做了很多封装的作业,便是为了这儿更通用一点,运用的当地更高雅一点。

  ...
{
    BufferLayout layout = {
            {ShaderDataType::Float3, "a_Position"},
            {ShaderDataType::Float4, "a_Color"},
    };
    m_VertexBuffer->SetLayout(layout);
}
uint32_t index = 0;
const auto& layout = m_VertexBuffer->GetLayout();
for (const auto& element : layout) {
    glEnableVertexAttribArray(index);
    glVertexAttribPointer(index,
                          element.GetComponentCount(),
                          ShaderDataTypeToOpenGLBaseType(element.Type),
                          element.Normallized ? GL_TRUE : GL_FALSE,
                          layout.GetStride(),
                          (const void*)element.Offset);
    index++;
  ...
}

shader glsl中增加color

std::string vertexSrc = R"(
    #version 330 core
    layout(location = 0) in vec3 a_Position;
    layout(location = 1) in vec4 a_Color;
    out vec3 v_Position;
    out vec4 v_Color; //新增
    void main()
    {
        v_Position = a_Position;
        v_Color = a_Color;
        gl_Position = vec4(a_Position, 1.0);
    }
)";
std::string fragmentSrc = R"(
    #version 330 core
    layout(location = 0) out vec4 color;
    in vec3 v_Position;
    in vec4 v_Color; //新增
    void main()
    {
       // color = vec4(v_Position * 0.5 + 0.5, 1.0);
        color = v_Color;
    }
)";

到此,BufferLayout的封装和接入就完结了。代码没问题的话,运转能看到一个五颜六色的三角形

完整代码 & 总结

本次代码修正参阅:Buffer API abstract-vertex

可以看到,我们写了这么多代码,大部分都和图形烘托无关。都是在工程层面做封装、解耦。

游戏引擎本质上是一个产品工程,很多的作业都是对渠道API的封装抽象、对数据的封装传递、对线程和内存的处理。更多的需求开发者有出色的c++编程才能、出色的工程规划才能。