游戏引擎从零开始(36)-Batch Rendering(2)

上一篇实现了"简陋"的批渲染,Demo中的矩形都使用同一种纹理,这篇继续完善,每个矩形可以有自己的纹理,下图是我们最终要实现的效果。

一、批渲染纹理

1.1回顾下批处理和纹理的使用

开始之前,如果你不记得批处理的原理,不妨回顾下上篇《游戏引擎从零开始(35)-Batch Rendering(1)》。

如果你忘了OpenGL中纹理的使用流程,一个简单的demo带你回顾下:

c++ 复制代码
// 假设你加载了一张纹理,对应的句柄为myTextureID(还在CPU内存中).

// 1. 激活GPU中的一个纹理,默认为0,也可以设置为别的索引
glActiveTexture(GL_TEXTURE0);

// 2. 将CPU内存中的的纹理绑定到GPU内存中
glBindTexture(GL_TEXTURE_2D, myTextureID);

// 3. 获取shader程序中的纹理采样器地址
GLuint myTextureLocation = glGetUniformLocation(shaderProgram, "myTexture");

// 4. 将shader程序中纹理采样器映射到0,0对应GL_TEXTURE0
glUniform1i(myTextureLocation, 0);  // 0 corresponds to GL_TEXTURE0.

1.2 多纹理绑定

想实现批处理,最根本的条件是所有的数据必须使用同一个shader,这个shader的实现要通用。

如下图,假设我们要绘制15个矩形,每个矩形都使用不同的纹理,则需要15张不同的图片,分别绑定到GL_TEXTURE0~GL_TEXTURE15上。

在shader中将这15张图片都包含进来,则所有的矩形都可以用同一个shader了,然后给每个矩形增加一个索引,以标记它用的是第几个纹理。

二、代码实现

我们从shader代码入手

2.1 Texture.glsl支持多纹理

Sandbox/assets/shaders/Texture.glsl

c++ 复制代码
// Basic Texture Shader

#type vertex
#version 330 core

layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord;
// 增加纹理索引,表示当前点使用的是第几个纹理
layout(location = 3) in float a_TexIndex;
// 支持tiling缩放因子
layout(location = 4) in float a_TilingFactor;

uniform mat4 u_ViewProjection;

out vec4 v_Color;
out vec2 v_TexCoord;
// 输出纹理索引到片元着色器
out float v_TexIndex;
// 输出tiling缩放因子到片元着色器
out float v_TilingFactor;

void main()
{
   v_Color = a_Color;
	  v_TexCoord = a_TexCoord;
	  v_TexIndex = a_TexIndex;
	  v_TilingFactor = a_TilingFactor;
//	 gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
	  gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
}

#type fragment
#version 330 core

layout(location = 0) out vec4 color;

in vec4 v_Color;
in vec2 v_TexCoord;
in float v_TexIndex;
in float v_TilingFactor;

// 纹理数组u_Textures只是存储[0~15]的索引
// 实际绑定到哪个纹理上是在外面的程序中实现
uniform sampler2D u_Textures[16];

void main()
{
	 color = texture(u_Textures[int(v_TexIndex)], v_TexCoord * v_TilingFactor) * v_Color;
}

2.2 完善OpenGLShader

Shader增加设置Int数组,用于设置shader中的纹理数组,注意glsl中Sample2D是int类型

Sandbox/Hazel/src/Hazel/Renderer/Shader.h

c++ 复制代码
virtual void SetInt(const std::string& name, const int value) = 0;
virtual void SetIntArray(const std::string& name, int* values, uint32_t count) = 0;
virtual void SetFloat(const std::string &name, const float value) = 0;

子类OpenGLShader中也需要更新

Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLShader.h

c++ 复制代码
void UploadUniformIntArray(const std::string& name, int* values, uint32_t);

void SetIntArray(const std::string &name, int *values, uint32_t count) override;

// 注意shader中数组的设置,glUniform1iv函数中传入count Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLShader.cpp

c++ 复制代码
...
void OpenGLShader::UploadUniformIntArray(const std::string &name, int *values, uint32_t count) {
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform1iv(location, count, values);
}

...
void OpenGLShader::SetIntArray(const std::string &name, int *values, uint32_t count) {
    UploadUniformIntArray(name, values, count);
}

2.3 Renerer2D支持多纹理绑定

先对Texture做点小改动,后面用的上,重载"=="操作符

Sandbox/Hazel/src/Hazel/Renderer/Texture.h

c++ 复制代码
...
virtual void SetData(void* data, uint32_t size) = 0;
virtual void Bind(uint32_t slot = 0) const = 0;

virtual bool operator==(const Texture& othre) const = 0;

子类同步改动

Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLTexture.h

c++ 复制代码
bool operator==(const Texture &other) const override{
    return m_RendererID == ((OpenGLTexture2D&)other).m_RendererID;
}

2.4 完善Renderer2D

下面进入到最核心的代码了,打起精神哟~ Sandbox/Hazel/src/Hazel/Renderer/Renderer2D.cpp 矩形顶点增加纹理索引、tiling缩放因子

c++ 复制代码
struct QuadVertex
{
    glm::vec3 Position;
    glm::vec4 Color;
    glm::vec2 TexCoord;
    float TexIndex;
    float TilingFactor;
};

Renderer2DData增加纹理槽

c++ 复制代码
struct Renderer2DData{
    const uint32_t MaxQuads = 10000;
    const uint32_t MaxVertices = MaxQuads * 4;
    const uint32_t MaxIndices = MaxQuads * 6;
    
    // 纹理最大值取决于GPU,至少有8个,我的mac笔记本上是16个
    static const uint32_t MaxTexturesSlots = 16; // TODO:RenderCaps

    Ref<VertexArray> QuadVertexArray;
    Ref<VertexBuffer> QuadVertexBuffer;
    Ref<Shader> TextureShader;
    Ref<Texture2D> WhiteTexture;

    uint32_t QuadIndexCount = 0;
    QuadVertex* QuadVertexBufferBase = nullptr;
    QuadVertex* QuadVertexBufferPtr = nullptr;

    // 增加数组,以存放纹理的引用,所有的纹理都会存在这里,最多存16个
    std::array<Ref<Texture2D>, MaxTexturesSlots> TextureSlots;
    // 当前纹理索引,从1开始。0被默认的纯色纹理占用了
    uint32_t TextureSlotIndex = 1; // 0 = white texture
};

初始化中更新纹理的处理

c++ 复制代码
void Renderer2D::Init() {
...
// 更新布局
s_Data->QuadVertexBuffer->SetLayout(
        {
                {ShaderDataType::Float3, "a_Position"},
                {ShaderDataType::Float4, "a_Color"},
                {ShaderDataType::Float2, "a_TexCoord"},
                {ShaderDataType::Float, "a_TexIndex"},
                {ShaderDataType::Float, "a_TilingFactor"},
        }
        );
}

...
// 创建1*1的纯色纹理
s_Data->WhiteTexture = Texture2D::Create(1, 1);
// 纹理颜色为白色
uint32_t whiteTextureData = 0xffffffff;
s_Data->WhiteTexture->SetData(&whiteTextureData, sizeof(uint32_t));

// 设置shader中sample2D值,将纹理数组u_Textures的值设置为[0~15],即按照[0~15]查找GPU中的纹理。
int32_t samplers[Hazel::Renderer2DData::MaxTexturesSlots];
for (uint32_t i = 0; i < Hazel::Renderer2DData::MaxTexturesSlots; i++) {
    samplers[i] = i;
}

s_Data->TextureShader = Shader::Create("../assets/shaders/Texture.glsl");
s_Data->TextureShader->Bind();
s_Data->TextureShader->SetIntArray("u_Textures", samplers, Hazel::Renderer2DData::MaxTexturesSlots);

// Set all texture slots to 0
// 安全起见,将数组中的每个值都初始化成一个默认的纹理,即单像素纹理
for (int i = 0; i < s_Data->MaxTexturesSlots; i++) {
    s_Data->TextureSlots[i] = s_Data->WhiteTexture;
}

shutdown函数中初始化s_Data

c++ 复制代码
void Renderer2D::Shutdown() {
    HZ_PROFILE_FUNCTION();

    s_Data->QuadIndexCount = 0;
    s_Data->QuadVertexBufferPtr = s_Data->QuadVertexBufferBase;
    s_Data->TextureSlotIndex = 1;
}

begin中也需要初始化纹理索引为1

c++ 复制代码
void Renderer2D::BeginScene(const OrthographicCamera &camera) {
    HZ_PROFILE_FUNCTION();

    s_Data->TextureShader->Bind();
    s_Data->TextureShader->SetMat4("u_ViewProjection", camera.GetViewProjectionMatrix());

    s_Data->QuadIndexCount = 0;
    s_Data->QuadVertexBufferPtr = s_Data->QuadVertexBufferBase;

    s_Data->TextureSlotIndex = 1;
}

flush中增加纹理绑定,这里不注意的话可能会遇到GPU警告,纹理绑定异常,实际不影响程序运行,注释中也给出了消除该警告的方法。

c++ 复制代码
void Renderer2D::Flush()
{
    // Bind Textures

    for (uint32_t i = 0; i < s_Data->TextureSlotIndex; i++) {
        s_Data->TextureSlots[i]->Bind(i);
    }

    // 这里绑定TextureSlotIndex个纹理,但是前面声明了16个纹理,并且在shader中也声明了16个纹理,运行时,会按索引查找所有的纹理,mac上会抛出如下警告日志
    // 如果你想消除这个警告,将所有的索引都绑定到gpu的纹理上,即将上面这段代码替换成下面这段。

    //UNSUPPORTED (log once): POSSIBLE ISSUE: unit 2 GLD_TEXTURE_INDEX_2D is unloadable and bound to sampler type (Float) - using zero texture because texture unloadable
//        for (uint32_t i = 0; i < s_Data->MaxTexturesSlots; i++) {
//            s_Data->TextureSlots[i]->Bind(i);
//        }

    RenderCommand::DrawIndexed(s_Data->QuadVertexArray, s_Data->QuadIndexCount);
}

更新两个DrawQuad函数,增加对纹理的支持,其实就是增加了TexIndex和TilingFactor字段

ini 复制代码
// 每个矩形对应4个顶点,即每绘制一个矩形,要添加4个顶点到s_Data中,用s_Data->QuadVertexBufferPtr标记end的地址
void Renderer2D::DrawQuad(const glm::vec3 &position, const glm::vec2 &size, const glm::vec4 &color) {
    HZ_PROFILE_FUNCTION();

    const float texIndex = 0.0f; // white Texture
    const float tilingFactor = 1.0f;

    s_Data->QuadVertexBufferPtr->Position = position;
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {0.0f, 0.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadVertexBufferPtr->Position = {position.x + size.x, position.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {1.0f, 0.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadVertexBufferPtr->Position = {position.x+size.x, position.y+size.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {1.0f, 1.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadVertexBufferPtr->Position = {position.x, position.y + size.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {0.0f, 1.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadIndexCount += 6;
}

...

void Renderer2D::DrawQuad(const glm::vec3 &position, const glm::vec2 &size, const Ref<Texture2D> &texture, float tilingFactor, const glm::vec4& tintColor) {
    HZ_PROFILE_FUNCTION();

    constexpr glm::vec4 color = {1.0f, 1.0f, 1.0f, 1.0f};

    float texIndex = 0.0f;
    for (uint32_t i = 1; i < s_Data->TextureSlotIndex; ++i) {
        if (*s_Data->TextureSlots[i].get() == *texture.get()) {
            texIndex = (float)i;
            break;
        }
    }

    if (0.0f == texIndex) {
        texIndex = (float)s_Data->TextureSlotIndex;
        s_Data->TextureSlots[s_Data->TextureSlotIndex] = texture;
        s_Data->TextureSlotIndex++;
    }

    s_Data->QuadVertexBufferPtr->Position = position;
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {0.0f, 0.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadVertexBufferPtr->Position = {position.x + size.x, position.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {1.0f, 0.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadVertexBufferPtr->Position = {position.x+size.x, position.y+size.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {1.0f, 1.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadVertexBufferPtr->Position = {position.x, position.y + size.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {0.0f, 1.0f};
    s_Data->QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data->QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data->QuadVertexBufferPtr++;

    s_Data->QuadIndexCount += 6;
}

2.5 更新Demo Sandbox2D,绘制带纹理的矩形

新增加了一张图片,我这里放了张刘德华头像

Sandbox/assets/textures/Cover.jpeg

c++ 复制代码
Hazel::Ref<Hazel::Texture2D> m_CheckerboardTexture;
Hazel::Ref<Hazel::Texture2D> m_CoverTexture;

增加带纹理的矩形

Sandbox/src/Sandbox2D.cpp

c++ 复制代码
void Sandbox2D::OnAttach() {
    HZ_PROFILE_FUNCTION();

    m_CheckerboardTexture = Hazel::Texture2D::Create("../assets/textures/Checkerboard.png");
    // 新增加一张纹理图
    m_CoverTexture = Hazel::Texture2D::Create("../assets/textures/Cover.jpeg");
}


void Sandbox2D::OnUpdate(Hazel::Timestep ts) {
    HZ_PROFILE_FUNCTION();
    // Update
    m_CameraController.OnUpdate(ts);

    // Render
    {
        HZ_PROFILE_SCOPE("Renderer Prep");
        Hazel::RenderCommand::SetClearColor({0.1f, 0.1f, 0.1f, 1.0});
        Hazel::RenderCommand::Clear();
    }

    {
        HZ_PROFILE_SCOPE("Renderer Draw");
        Hazel::Renderer2D::BeginScene(m_CameraController.GetCamera());
//        Hazel::Renderer2D::DrawQuad({-1.0f, 0.0f}, {0.8f, 0.8f}, glm::radians(-45.0f), {0.8f, 0.2f, 0.3f, 1.0f});
        Hazel::Renderer2D::DrawQuad({-1.0f, 0.0f}, {0.8f, 0.8f}, {0.8f, 0.2f, 0.3f, 1.0f});
        Hazel::Renderer2D::DrawQuad({0.5f, -0.5f}, {0.5f, 0.75f}, {0.2f, 0.3f, 0.8f, 1.0f});
        
        // 增加棋盘格子纹理的矩形
        Hazel::Renderer2D::DrawQuad({-5.0f, -5.0f, -0.1f}, {10.0f, 10.0f}, m_CheckerboardTexture, 10.0f);

        // 增加刘天王头像的矩形
        Hazel::Renderer2D::DrawQuad({-0.5f, -0.5f, 0.0f}, {1.0f, 1.f}, m_CoverTexture, 1.0f);
        Hazel::Renderer2D::EndScene();
    }
}

OK~大功告成!Run起来看看吧,正常的话,会看到文章开头的示意图。

三、代码 & 总结

本次代码修改参考

github.com/summer-go/H...

总结

  • 单次能加载的纹理上限和GPU有关,至少有8个,笔者的电脑上是16
  • 涉及到纹理的绑定和使用细节比较多,稍不注意就容易出错,glsl中纹理是绑定到0~~15,c++代码中是绑定到GL_TEXTURE0~~GL_TEXTURE15。
  • sample2D是int类型传值

细心的你肯定也发现了,单次能加载的纹理也有上限,也就是说如果纹理超过了上限,就要拆成多个批次了,那有没有其他办法呢?也是有的,可以将多个纹理合成到一张纹理图上。

相关推荐
刘好念4 天前
[OpenGL]使用OpenGL实现硬阴影效果
c++·计算机图形学·opengl
闲暇部落5 天前
Android OpenGL ES详解——纹理:纹理过滤GL_NEAREST和GL_LINEAR的区别
opengl·texture·linear·纹理过滤·nearest·邻近过滤·线性过滤
凌云行者6 天前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者6 天前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
凌云行者9 天前
OpenGL入门004——使用EBO绘制矩形
c++·cmake·opengl
Thomas_YXQ9 天前
Unity3D中管理Shader效果详解
开发语言·游戏·unity·unity3d·游戏开发
闲暇部落9 天前
Android OpenGL ES详解——模板Stencil
android·kotlin·opengl·模板测试·stencil·模板缓冲·物体轮廓
凌云行者11 天前
OpenGL入门003——使用Factory设计模式简化渲染流程
c++·cmake·opengl
凌云行者12 天前
OpenGL入门002——顶点着色器和片段着色器
c++·cmake·opengl
Ljw...12 天前
C++游戏开发
c++·c·游戏开发