上一篇实现了"简陋"的批渲染,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起来看看吧,正常的话,会看到文章开头的示意图。
三、代码 & 总结
本次代码修改参考
总结
- 单次能加载的纹理上限和GPU有关,至少有8个,笔者的电脑上是16
- 涉及到纹理的绑定和使用细节比较多,稍不注意就容易出错,glsl中纹理是绑定到0~~15,c++代码中是绑定到GL_TEXTURE0~~GL_TEXTURE15。
- sample2D是int类型传值
细心的你肯定也发现了,单次能加载的纹理也有上限,也就是说如果纹理超过了上限,就要拆成多个批次了,那有没有其他办法呢?也是有的,可以将多个纹理合成到一张纹理图上。