65.9K
CodeProject 正在变化。 阅读更多。
Home

OpenGL ES: iPhone 上的批处理渲染

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (4投票s)

2009年10月22日

CPOL

9分钟阅读

viewsIcon

31182

downloadIcon

151

简单来说,我在开发游戏时遇到了一些帧率问题,于是我想出了一个方法,将我所有的绘制操作批量化,尽可能地减少 OpenGL 调用次数。

这花了我不少时间,仅仅是因为我做了大量的研究。这篇不是一个教程帖,而是“我发现的东西”,并请求有人能审阅一下,看看我有什么可以改进的地方,然后告诉我!:)

简单来说,我在开发游戏时遇到了一些帧率问题,于是我想出了一个方法,将我所有的绘制操作批量化,尽可能地减少 OpenGL 调用次数。我发现了一些有趣的小技巧,包括斯坦福大学的一个“iTunes U”课程(可以在 iTunes 里搜索一下,非常值得观看),尤其是 Tim Omernick 关于 OpenGL 的课程(幻灯片可以在 这里找到)。

代码副本可以在 google code 仓库找到,TextureController 的直接链接是 这里。那么,让我们来看看它!

我首先尝试做的是创建一个包含顶点、颜色信息和 UV 信息的结构体。这是 Tim 讲座的结果,并受到他代码的很大影响。遇到的问题是我希望能够基于正在绘制的 GLuint 来批量化,这样我只需要绑定一次纹理,然后绘制与该纹理关联的所有顶点。我提出了以下解决方案:

struct Vertex
{
 short v[2];
 unsigned color;
 float uv[2];
};
 
struct VertexInfo
{
 Vertex vertex[MAX_VERTICES];
 int _vertexCount;
};

这里会发生的是,VertexInfo 将存储在一个映射表中,而 Vertex 结构体包含了所有将要传递给 OpenGL 进行绘制的信息。MAX_VERTICES 在头文件的顶部进行了定义,我并没有对其进行太多调整,所以不确定在运行完整游戏时它的速度会快还是慢。目前设置为 100,000,但这只是我随便填的一个数字 :D 我估计应该可以减少。

包含顶点信息的映射表如下所示:

typedef std::map<GLuint, VertexInfo> VertexMap;
VertexMap vertices;

稍后您将看到如何使用它。我的这个类的想法是模仿 XNA 框架的 SpriteBatch 的工作方式。这是我对这个框架的赞赏,以及我多么喜欢使用 XNA 的又一例证。我希望能够调用一个 begin 函数,让 TextureController 知道我是否计划使用混合(或其他我可能添加的参数,例如排序),然后通过 Textures->draw 函数提供的大量内容进行绘制。每次调用 Textures->draw 函数都会接收要绘制的纹理和任何需要的信息(例如位置、源矩形、颜色等),并将该信息打包到 VertexMap 中,等待被传递给 OpenGL。一旦我所有的绘制命令都执行完毕,Textures->end() 将启动批量绘制。

请记住,这是组织此类事物的最佳方式吗?可能不是……尽管这是我第一次处理如此有趣的任务…… :) 让我们看看它是如何工作的!

注意:我将添加换行符来分隔一些注释……如果您想要完整文件,请务必访问代码仓库 code.google.com

void TextureController::draw( const Texture2D& texture, const Rectangle& destination,
 const Rectangle& source, const Color& color, const GLfloat depth )
{

正如您所见,我们正在插入一些信息片段,但这只是我实现的绘图命令之一。最简单的绘图命令,您只需要提供纹理和目标矩形……而“默认”值将被用于填充调用此函数时的空白。

 GLuint glid = texture.getId();
 
 //	if we don't have any vertices with the texture being drawn, create a
 //	vertex map for it.
 VertexMap::iterator it = vertices.find(glid);
 if (it == vertices.end())
 vertices[glid]._vertexCount = 0;		
 
 //	find all of the vertices we'll need for this sprite
 float topLeftX = destination.x;
 float topLeftY = destination.y;
 float topRightX = destination.x + destination.width;
 float topRightY = destination.y;
 float bottomLeftX = destination.x;
 float bottomLeftY = destination.y + destination.height;
 float bottomRightX = destination.x + destination.width;
 float bottomRightY = destination.y + destination.height;

以上部分很有趣,因为它首先会找出我们是否已经使用该纹理批量绘制了一个精灵。如果我们已经进行了,我们只需将顶点添加到该批次中。否则,我们将创建一个新的顶点批次并开始这个新批次。接下来,我们找到所有的顶点!这是通过传入的目标矩形完成的。目标矩形将是绘制纹理的精灵框。换句话说,它是您的画布,而您的纹理就是颜料。

 // Texture atlas
 float minUV[2];
 float maxUV[2];
 
 //	if the source rectangle of ZERO was passed in, it means the client want to just
 //	draw the texture as is.. otherwise, the client wishes to draw a portion of
 //	the rectangle
 if (source == Rectangle::ZERO())
 {
 float maxS = texture.getMaxS();
 float maxT = texture.getMaxT();
 float minS = 0;
 float minT = 0;
 
 minUV[0] = minS;
 minUV[1] = minT;
 maxUV[0] = maxS;
 maxUV[1] = maxT;
 }
 else
 {
 float minS = source.x / texture.getWidth();
 float minT = source.y / texture.getHeight();
 float maxS = source.width / texture.getWidth();
 float maxT = source.height / texture.getHeight();
 
 minUV[0] = minS;
 minUV[1] = minT;
 maxUV[0] = maxS;
 maxUV[1] = maxT;
 }

这部分花了我最长的时间来真正弄明白,因为我之前没有研究过纹理图集的工作原理。正如我所说,这很大程度上受到了 Tim 代码的影响,在我继续之前我真的很想理解这一部分。我们在这里做的是,如果函数中传入了一个空白矩形(意味着客户端没有指定矩形,因此希望“按原样”使用纹理),我们将使用纹理坐标的最大 S 和最大 T 值。我们找到最大 S 和最大 T 值的方法是,在图像经过其幂次方转换为 2 的幂纹理(请参阅我上一篇文章)后,最大 S 将变为 imageWidth / newTextureWidth。换句话说:
如果纹理以 30 像素 x 30 像素传入到您的游戏中,它将由 Texture2D 类重新调整为 32x32。最大 S 将是 30 / 32,或 0.9375f。该值表示在使用默认值时,绘制了图像的 93%,而剩余的 7% 图像仅包含填充,以确保它是幂次方纹理。

如果我们想使用一个子图像,只绘制一部分怎么办?这在我们将多个图像放在一个纹理上时很有用。假设您有一个包含 2 个动画帧的纹理,每个帧都是 24x24 像素。这意味着您的纹理宽度为 48 像素,高度为 24 像素。如果您只想绘制纹理的后半部分(您的“第二个”动画帧),则源矩形将是:

x=24, y=0, width=24, height=24
这意味着您从图片中开始偏移 24 像素,在图片的顶部像素处,并且您将绘制 24 像素的宽度和 24 像素的高度。这将是您要绘制的子图像。

为了将这些数字转换为纹理坐标,我们需要除以图像的宽度或高度。例如,24 / 48 是 0.5,所以您的 x 纹理坐标是 0.5,然后是 0,然后是 0.5 和 0.5。

 //	Convert the colors into bytes
 unsigned char red = color.red * 255.0f;
 unsigned char green = color.green * 255.0f;
 unsigned char blue = color.blue * 255.0f;
 unsigned char shortAlpha = color.alpha * 255.0f;
 
 //	pack all of the color data bytes into an unsigned int
 unsigned _color = (shortAlpha << 24) | (blue << 16) | (green << 8) | (red << 0);
 
 // Triangle #1
 addVertex(glid, topLeftX, topLeftY, minUV[0], minUV[1], _color);
 addVertex(glid, topRightX, topRightY, maxUV[0], minUV[1], _color);
 addVertex(glid, bottomLeftX, bottomLeftY, minUV[0], maxUV[1], _color);
 
 // Triangle #2
 addVertex(glid, topRightX, topRightY, maxUV[0], minUV[1], _color);
 addVertex(glid, bottomLeftX, bottomLeftY, minUV[0], maxUV[1], _color);
 addVertex(glid, bottomRightX, bottomRightY, maxUV[0], maxUV[1], _color);
}

啊,位打包……我多么爱你,让我数数方法!当我第一次学会如何操作数据位时,我问自己“这有什么用处?”……猜猜我为此付出了代价,它是一项非常有用的技能。我并没有像我想的那样完全理解它,但我每天都会做一些研究。

我们在上面的代码中做的是,获取红/绿/蓝/ alpha 颜色值(以 0 到 1 的值传入),乘以 255,并将位打包到一个名为 _color 的单一值中。显然,OpenGL 以这种方式使用数据会快得多,所以嘿嚯,嘿嚯……我们去打包吧!

void TextureController::addVertex(GLuint glid, float x, float y, float uvx, float uvy, unsigned color)
{
 VertexInfo *vertexInfo = &vertices[glid];
 Vertex *vert = &vertexInfo->vertex[vertexInfo->_vertexCount];
 vert->v[0] = x;
 vert->v[1] = y;
 vert->uv[0] = uvx;
 vert->uv[1] = uvy;
 vert->color = color;
 vertexInfo->_vertexCount++;
}

我们继续打包!这就是批量处理的地方。我们做的是传递一个单一的 Vertex 数据(x 位置、y 位置、纹理坐标、颜色和使用的纹理),并将其打包到我上面给您展示的结构体中。一旦我们这样做,我们就增加 _vertexCount,这样我们就可以跟踪我们正在批量处理的顶点数量。

那么……这就是我们如何批量处理,如何渲染?很简单!

注意:请记住,这只是代码的一部分。这个函数是从我之前提到的“end()”函数调用的。end() 将确保 beginning 已被调用,然后将任务交给 render 函数。

void TextureController::renderToScreen()
{
 glPushMatrix();
 glMatrixMode(GL_MODELVIEW);
 
 //	Texture Blending fuctions
 if ( blendAdditive )
 {
 glEnable(GL_BLEND);
 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 }
 
 //	needed to draw textures using Texture2D
 glEnable(GL_TEXTURE_2D);		
 
 //	enables alpha for transparent textures
 //	I forget where I got these commands, iDevGames.net I think
 glAlphaFunc(GL_GREATER, 0.1f);
 glEnable(GL_ALPHA_TEST);
 
 //	Enable the various arrays used to draw texture to screen
 glEnableClientState(GL_VERTEX_ARRAY);
 glEnableClientState(GL_NORMAL_ARRAY);
 glEnableClientState(GL_TEXTURE_COORD_ARRAY);
 glEnableClientState(GL_COLOR_ARRAY);
 
 glLoadIdentity();

上面只是准备渲染的标准步骤。事实上,我认为这只是从我的 Texture2D 类中复制/粘贴过来的。真正有趣的部分在后面!

 //	loop through all of the elements of the map and draw the vertices
 VertexMap::iterator it = vertices.begin();
 for (/* none */; it != vertices.end(); it++)
 {
 //	easy access to our data
 VertexInfo *vertexInfo = &it->second;
 Vertex *vert = vertexInfo->vertex;
 
 //	bind the texture for the following vertices
 bindTexture( (*it).first );
 
 //	throw everything to OpenGL
 glVertexPointer(2, GL_SHORT, sizeof(Vertex), &vert->v);
 glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vert->uv);
 glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &vert->color);
 glDrawArrays(GL_TRIANGLES, 0, vertexInfo->_vertexCount);
 
 //	reset this batches vertex count
 vertexInfo->_vertexCount = 0;
 }

我们在这里做的是遍历每一个键(如果您还记得,那就是纹理的 GLuit),绑定该纹理,以便 OpenGL 知道要用哪个纹理进行绘制,然后传递所有用于渲染批次的顶点、纹理坐标和其他所有信息。一些人可能不知道的一些说明(在我研究时我不知道):

glVertexPointer(2, GL_SHORT, sizeof(Vertex), &vert->v);
这里的 2 表示将有 2 个顶点,一个 x 和一个 y。这个数字可以是 3,如果您也存储了 z 值。我们将使用 GL_SHORT(记住在结构体中,所有顶点都声明为 short),这里有趣的部分是 sizeof(Vertex)。这被称为 STRIDE,基本上告诉 OpenGL 内存中数据之间的距离。(希望我解释得正确!)最后一个值是顶点数组!

glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vert->uv);
这与上面非常相似……除了我们使用了不同的值。这里的 2 指的是每个顶点有两个纹理坐标点(一个 x 和一个 y),但如果您要使用 z 值,这个数字将变为 3。请记住,我们使用的是浮点数(检查结构体),STRIDE 值保持不变。然后只需将其传入信息数组。

glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &vert->color);
同样的概念,只是我们在这里对它做了一些不同的处理。请记住,我们的颜色方案是 4 种颜色(红、绿、蓝和 alpha),但我们将所有信息打包到一个单一值中?我们通过 GL_UNSIGNED_BYTE 来指定。否则,其他所有内容都保持相似。

glDrawArrays(GL_TRIANGLES, 0, vertexInfo->_vertexCount);
这就是我们跟踪顶点数量的原因!我们在这里做的是告诉 OpenGL 使用三角形绘制数组,0 指的是起始索引(显然我们将从索引 0 开始,就像我们自己遍历索引一样),以及数组中有多少个元素。

这样做将把信息组合成一个单一的信息流……有点像

XY RGBA USUV, XY RGBA USUV, XY RGBA USUV
这对于 OpenGL 渲染来说效率要高得多,因此它会以比您一次渲染每个纹理的数据快得多的速度处理数据。(也就是说,我旧的方法)

现在剩下要做的就是清理,禁用我们启用的任何状态

 //	disable all the stuff we enabled eariler
 glDisableClientState(GL_VERTEX_ARRAY);
 glDisableClientState(GL_TEXTURE_COORD_ARRAY);
 glDisableClientState(GL_COLOR_ARRAY);
 glDisable( GL_BLEND );
 glDisable( GL_TEXTURE_2D );
 glDisable( GL_ALPHA_TEST );
 
 glPopMatrix();
}

然后就完成了!

再说一遍,这是我第一次编写这样的系统……我仍在学习图形编程的技巧。我强烈建议您从 svn 仓库 (http://code.google.com/p/djinnengine) 下载代码并进行尝试。如果您发现任何我可以改进的地方,或者有助于我更好地理解这个过程的东西,请随时发布,以便大家从中学习!

祝大家编码愉快!

© . All rights reserved.