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

791 阅读5分钟

前言

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函数的各个参数的意义:

我们需要将这些参数封装起来,不要写死,有多个顶点属性时,在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的列表。

重点看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++编程能力、良好的工程设计能力。