Simple2D-21(重构)渲染部分
以前 Simple2D 的渲染方法是先设置 Pass,然后添加顶点数据,相同 Pass 的顶点数据会合并在一起。当设置新的 Pass 时,将旧的 Pass 和对应的顶点数据添加到渲染数组中。最后在帧结束时遍历渲染数组,根据 Pass 设置 OpenGL 状态,绘制对应的顶点数据。
这次改为更加简单的方法,类似状态机。设置混合状态(Blend)、着色程序(Shader Program)。渲染部分由 Graphics Context、Renderer 和 Shader Program 组成:
Graphics Context:图形上下文,设置渲染的混合状态(Blend)和使用的着色程序(Shader Program)。所有顶点数据的渲染都会使用 Graphics Context 当前使用的 Blend 和 Shader Program,直到 Graphics Context 设置新的 Blend 或 Shader Program。当 Blend 或 Shader Program 改变时会将上一个状态的顶点数据立即进行渲染,这是和以前 Simple2D 渲染方法的区别。
这次去掉了裁剪测试、深度测试、模板测试,这些是非必须使用的功能。保留了混合是因为要渲染透明纹理。
Renderer:渲染器,对顶点数据进行合并和管理。需要渲染顶点数据时,会将数据传递给 Graphics Context,Graphics Context 然后根据当前设置的 Blend 和 Program 绘制数据。
Shader Program:着色程序,使用着色器可以实现炫酷的效果。这次重构的 Simple2D 可以使用标准的 Program 和自定义的 Program 渲染顶点数据,通过自定义 Program 实现标准 Program 不能实现的效果。
因此 Simple2D 则可以去掉 Pass 类和 BlockAllocator 类,以前使用 Simple2D 渲染顶点数据时要为顶点分配空间,渲染后又要释放空间,中间的步骤十分麻烦。为什么要使用如此麻烦的方法,纯粹是我脑袋瓦特了。
实现
Renderer
Renderer 内部有两个一定大小缓冲区,用于存储顶点数据和索引数据:
static const int vertex_buffer_size = 1024 * 1024; static const int index_buffer_size = 40000; char vVertexBuffer[vertex_buffer_size]; /* 用于合并顶点的缓冲区 */ uint32 vIndexBuffer[index_buffer_size]; /* 用于合并索引的缓冲区 */
渲染一个正方形,需要 4 个顶点和 6 个索引。通过 AppendRenderData( ) 函数将顶点数据和索引数据传递给 Renderer,然后 Renderer 将顶点数据和索引数据拷贝到缓冲区中,当缓冲区的空间不足时,会调用 Flush( ) 函数将渲染数据提交给 Graphics Context 进行渲染。AppendRenderData( ) 是一个模板函数,通过模板的特性可以知道顶点结构的大小,从而进行拷贝操作:
template<class Type> void AppendRenderData(Type* vertex_data, int vertex_count, uint32* index_data, int index_count, PrimType type) { int total_vertex_count = vertex_buffer_size / sizeof(Type); if ( total_vertex_count - nVertexCount < vertex_count || index_buffer_size - nIndexCount < index_count ) { this->Flush(); } for ( int i = 0; i < index_count; i++ ) { vIndexBuffer[nIndexCount + i] = nVertexCount + index_data[i]; } char* data_header = vVertexBuffer + nVertexCount * sizeof(Type); memcpy(data_header, ( char* ) vertex_data, vertex_count * sizeof(Type)); nVertexCount += vertex_count; nIndexCount += index_count; primType = type; }
当然也可以通过函数参数传递顶点结构的大小,但这样太麻烦了。
如果要渲染纹理,同时希望减少 drawcall。因为当时局限于一个着色程序绑定一张纹理的想法,所以以前 Simple2D 通过合并相同纹理的顶点数据以达到一张纹理一个 drawcall,可以减小切换纹理而带来的开销。但是着色程序是可以绑定多张纹理的,可以在顶点数据中添加一个索引的数据,指定使用哪一个绑定的纹理,这样可以达到多张纹理一个 drawcall 了。下面是 Simple2D 定义的纹理渲染标准着色程序:
const char* Sprite_Vertex = R"( #version 330 core layout(location = 0) in vec3 Position; layout(location = 1) in vec2 Texcoord; layout(location = 2) in vec4 Color; layout(location = 3) in float Texindex; uniform mat4x4 MVPMatrix; out vec2 texcoord; out vec4 color; flat out int texindex; void main() { gl_Position = MVPMatrix * vec4(Position, 1.0f); color = Color; texcoord = Texcoord; texindex = int(Texindex); } )";
const char* Sprite_Fragment = R"( #version 330 core in vec2 texcoord; in vec4 color; flat in int texindex; uniform sampler2D Texture0; uniform sampler2D Texture1; uniform sampler2D Texture2; uniform sampler2D Texture3; uniform sampler2D Texture4; uniform sampler2D Texture5; uniform sampler2D Texture6; uniform sampler2D Texture7; uniform sampler2D Texture8; uniform sampler2D Texture9; uniform sampler2D Texture10; uniform sampler2D Texture11; uniform sampler2D Texture12; uniform sampler2D Texture13; uniform sampler2D Texture14; uniform sampler2D Texture15; vec4 SampleTexture(int index) { switch( index ) { case 0: return texture(Texture0, texcoord); case 1: return texture(Texture1, texcoord); case 2: return texture(Texture2, texcoord); case 3: return texture(Texture3, texcoord); case 4: return texture(Texture4, texcoord); case 5: return texture(Texture5, texcoord); case 6: return texture(Texture6, texcoord); case 7: return texture(Texture7, texcoord); case 8: return texture(Texture8, texcoord); case 9: return texture(Texture9, texcoord); case 10: return texture(Texture10, texcoord); case 11: return texture(Texture11, texcoord); case 12: return texture(Texture12, texcoord); case 13: return texture(Texture13, texcoord); case 14: return texture(Texture14, texcoord); case 15: return texture(Texture15, texcoord); default: return vec4(1.0, 1.0, 1.0, 1.0); } } void main() { gl_FragColor = SampleTexture(texindex) * color; } )";
这个着色程序一次可以绑定 16 张纹理,这意味着你可以一个 drawcall 渲染 16 张纹理。所以在 Renderer 中设置一个纹理数组:
int nCurrentTextureCount; static const int nMaxNumberOfTexture = 16; GLuint vTextures[nMaxNumberOfTexture];
储存渲染纹理,在渲染前绑定纹理到着色程序即可:
void Renderer::BindTexture() { for ( int i = 0; i < nCurrentTextureCount; i++ ) { glActiveTexture(GL_TEXTURE0 + i); glBindTexture(GL_TEXTURE_2D, vTextures[i]); } }
其中纹理存储在数组中的位置就是纹理在着色程序中的纹理索引。
Shader Program
Simple2D 内置有两个标准着色程序 Standard Program,分别用于渲染纹理和几何图形。如果有特别需要的话,可以自定义着色程序,为此需要处理 Uniform 数据,所以定义一个类 ProgramEffect 来管理 Uniform 数据。
Graphics Context
Graphics Context 用于设置 Blend 和 Shader Program 以及渲染顶点数据,实现较为简单。
源码下载:Simple2D-20.rar