原文链接:TetroGL: An OpenGL Game Tutorial in C++ for Win32 Platforms - Part 2
在这个系列的第一部分中,作者介绍了窗口的创建以及OpenGL环境的创建,在接下来这一部分中,作者将介绍如何处理游戏中的资源以及如何显示简单的动画
简介
这个系列的第一篇文章关注于窗口的创建和OpenGL环境的创建,本文将有趣的多,因为我们将尝试加载显示图片文件,并且显示一些动画效果.你将会看到如何才能有效地操纵这些资源.当然本文完成的项目还不是一个游戏,因为它还没有加入任何游戏逻辑,它唯一能做的仅仅是在屏幕上移动人物角色,并且用动画的效果显示(没有实现碰撞检测)
文件的组织
首先来考虑如何更好地组织文件资源.作者一般会创建一个src文件夹来放置所有的源文件(.h和.cpp),一个bin文件夹来放置最终的可执行文件和所有所需要的资源,一个obj文件夹
用来放置编译所得到的中间文件,一个dependencies文件夹放置用到的第三方库.如果你有许多资源(图片,音乐,配置文件等),你甚至可以将bin文件夹进一步划分为子文件夹.
现在我们就来按照上面的文件组织形式来更改项目设置.对于源文件,只需要将它们复制到src文件夹中,并将其加入项目就行.为了配置输出文件夹和中间文件夹,更改如下图:
$(SolutionDir) 和$(ConfigurationName) 是预先定义的宏.前一个指向解决方案所在文件夹,后一个指向当前活动配置(debug or release):在obj文件夹中,会创建出两个子文件夹,一个配置一个文件夹
加载图片
很不幸,OpenGL对于加载图片没有提供任何帮助.因此我们必须借助第三方库的帮助.有很多第三方库可供选择,作者提供了两个建议: DevIL 和FreeImage.DevIL更适合于OpenGL,因此作者选择了它.
首先要做的是将所需要的DevIL文件拷贝到dependencies文件夹中:首先创建一个子文件夹DevIL,并将DevIL官网上的文件拷贝至此.要正确地使用它,我们必须修改一个文件的名字:在”include/IL”文件夹中,有一个名为config.h.win的文件,将其重命名为config.h.然后拷贝DevIL.dll到你的bin文件夹中,因为它将会被你的可执行文件使用到.
然后我们必须在项目属性中进行配置,以便使用DevIL.如下图所示:
这将会告诉编译器到哪里去寻找所需要的DevIL头文件,这样设置,我们就可以不必提供DevIL头文件的全路径.
上图配置就告诉链接器到哪里去寻找附加的文件夹(这个文件夹中包含了要链接的库文件).
上图配置会告诉编译器此项目必须链接DevIL库和OpenGL库.
资源管理
现在使用DevIL的环境已经搭建好了,我们现在开始加载一些图片并显示它们.但在此之前,我们先考虑下如何更有效地管理这些资源文件.假设我们需要显示一棵树,它包含在个名为tree.png的文件中,最暴力的方法是简单地加载文件并保存在内存中,这样在每次重绘帧时可以重用它.这种方法看起来不错,但它有一个小问题:假设我们现在需要显示此树的次数超过一次,那么我们就必须几次在内存中加载纹理文件,而这显然是低效的.我们必须要想一个办法,即使我们在不同位置的代码中也能使用同一份纹理文件.这通过将加载资源文件代理给一个特定的类:纹理文件管理者就可以轻松地解决.让我们首先来看看这个类:
CTextureManager资源管理类
这个类是以单例模式实现的.
CTextureManager* CTextureManager::GetInstance()
{
// Returns the unique class instance.
static CTextureManager Instance;
return &Instance;
}
这样就可以拥有一个全局唯一的实例,并且访问它也十分简单:
CTexture* pTexture = CTextureManager::GetInstance()->GetTexture("MyTexture.bmp");
这个类的构造函数负责对DevIL库进行初始化:
复制代码
CTextureManager::CTextureManager() : m_Textures()
{
// Initialize DevIL
ilInit();
// Set the first loaded point to the
// upper-left corner.
ilOriginFunc(IL_ORIGIN_UPPER_LEFT);
ilEnable(IL_ORIGIN_SET);
}
复制代码
在调用DevIL库函数前,必须先调用ilInit以便对库进行初始化.此外,我们还需要指明图片如何进行加载:先是左上方.这样做的目的是我们就不需要对纹理图片进行翻转.默认情况下这个选项是禁止的,因此我们需要调用ilEnable(IL_ORIGIN_SET);来使之设置为允许.
现在来看看GetTexture方法:
获取纹理资源
很简单的实现代码:首先根据给定的文件名在映射表中查找文件是否已经加载进来了,若是则直接返回,否则就从文件中进行加载.待会我们会看到在CTexture类的构造函数中会尝试加载文件,若失败则抛出异常.因此,在纹理文件管理者类中,若捕获到此异常,就删除纹理文件(这是为了避免内存泄露)并且再次抛出异常.若文件加载成功,则将其保存到映射表中(以其文件名作为键值).
此外,我们还提供了释放已加载资源的方法,非常简单的实现:在映射表中查找,若存在就删除它,并且从映射表中移除.
复制代码
bool CTextureManager::ReleaseTexture(const std::string& strTextName)
{
// Retrieve the texture from the map
bool bFound = false;
TTextureMap::iterator iter = m_Textures.find(strTextName);
if (iter != m_Textures.end())
{
// If it was found, we delete it and remove the
// pointer from the map.
bFound = true;
if (iter->second)
delete iter->second;
m_Textures.erase(iter);
}
return bFound;
}
复制代码
资源包装类CTexture
复制代码
CTexture类
#include <Windows.h>
#include "GL/gl.h"
#include <string>
class CTextureManager;
// Class that wraps information about a texture. This class
// won't be used directly by the users. Instead, they will
// manipulate the CImage class.
class CTexture
{
friend class CTextureManager;
public:
// Specifies a color key to be used for the texture. The color
// specifed as arguments will be transparent when the texture
// is rendered on the screen.
void SetColorKey(unsigned char Red, unsigned char Green, unsigned char Blue);
// Returns the width of the texture
unsigned int GetWidth() const { return m_TextData.nWidth; }
// Returns the height of the texture.
unsigned int GetHeight() const { return m_TextData.nHeight; }
// Adds/release a reference for the texture. When ReleaseReference
// is called and decreases the reference count to 0, the texture
// is released from the texture manager.
void AddReference();
void ReleaseReference();
// Bind this texture with openGL: this texture becomes
// the 'active' texture in openGL.
void Bind() const;
protected:
// Constructor which takes the filename as argument.
// It loads the file and throw an exception if the load
// failed.
CTexture(const std::string& strFileName);
~CTexture();
private:
// Loads the texture from the specifed file. Throws an
// exception if the load failed.
void LoadFile(const std::string& strFileName);
// Structure that contains the information about the texture.
struct STextureData
{
// Width of the texture
unsigned int nWidth; //纹理宽度
// Height of the texture
unsigned int nHeight; //纹理高度
// Byte array containing the texture data
unsigned char* pData; //包含纹理数据的字节数组
};
STextureData m_TextData;
// The openGL id associated with this texture.
mutable GLuint m_glId;
// Reference count of the number of images that still hold a reference
// to this texture. When no images reference the texture anymore, it is
// released.
int m_iRefCount; //引用计数
// The filename from which the texture was loaded from.
std::string m_strTextName;
};
复制代码
我们可以看到此类的构造函数是受保护的,这是因为只允许CTextureManager类能够创建纹理,这也是为什么将其设为此类的友元类.CTexture类的核心是STextureData结构体,它包含了从文件加载进的所有信息:包含文件数据的字节数组,纹理的宽度和高度.
下面看看究竟是如何加载文件的:
加载资源文件
正如你看到的,我们使用DevIL来加载文件.首先要做的是创建一个新的图片id,并将其绑定到当前图片上.如果你想使用id对某个特定图片进行一些操作时,这是必需的.实际上,我们只需要在删除图片时使用它.然后,我们使用ilLoadImage尝试加载文件.这个函数负责处理各种不同的文件格式,当加载失败时返回false(你还可以调用ilGetError来查询其错误代码).若是这种情况,我们简单地抛出一个异常.如果你还记得,在第一篇文章中这些异常将会在main函数中被捕获,并且在退出程序前显示一个错误信息.接下来,我们获取图片的宽度和高度(ilGetInteger和ilCopyPixels函数对当前活动图片总是有效的).然后,我们为m_TextData.pData域分配空间:每个像素由4个字节编码(因为是RGBA类型).然后,调用ilCopyPixels函数来拷贝缓冲区中的图片数据.前三个参数分别是开始拷贝点的x,y,z位置,接下来的参数是这些方向上待拷贝的像素数目.然后指定图片格式:RGBA意味着每个颜色通道一个字节(RGB),以及alpha通道一个字节(A).Alpha通道用于指明像素的透明度,值为0表示全透明,值为255表示不透明.然后指明了每个部分的类型:它们必须以无符号字节进行编码.最后一个参数是包含像素数据的缓冲区指针.最后,由于我们不再需要DevIL图片数据,因此将其删除.
注:在OpenGL中使用DevIL加载纹理图片有更加简单的方式.ILUT库允许你调用ilutGLLoadImage函数加载图片并直接联系到一个OpenGL纹理上,此函数会返回OpenGL纹理的id.这是最简单的方式,但如此一来你就无法对原始字节数据进行操作,而这是接下来进行抠色(Color Keying)时要做的.
一旦数据从文件中加载出来后,我们就需要产生一个新的OpenGL纹理,并为之提供数据.这在纹理被首次要请求时,在CTexture::Bind函数中实现:
纹理绑定
OpenGL重要的一点是它每次只能使用一个纹理.因此,要想对一个多边形贴纹理,就必须选中活动纹理(也叫”绑定”).这通过调用glBindTexture来完成.每个OpenGL纹理都有其id,这里我们将其存储在 CTexture类的m_glId成员变量中.id为0表明纹理还没有被OpenGL产生出来.因此,当此函数第一次被调用时,m_glId将会是0.此时我们将会调用glGenTextures来请求OpenGL产生一个id.
m_glId是mutable的,这是因为我们想让Bind函数是const的,而这个成员变量只被修改一次(当纹理被产生时对其修改).glGenTextures函数可以允许你产生多个Id(第一个参数就是要产生的Id个数),但我们只想要单个Id.然后我们调用glBindTexture:这将绑定纹理(通过其Id)到活动的2维纹理上.这是必须的,因为接下来的纹理操作将会影响到你这里指定的特定纹理.
接下来的纹理操作就不解释了,可以参考红宝书…
抠色(Color Keying)
我们总是blit矩形区域的图片,但是很显然,几乎没有一个游戏的角色图片是矩形的。美工把图片画到一个矩形范围内,如果设定了特定的背景颜色,我们就可以把矩形图片上的角色“抠”下来,相对于背景来说,我们就是把不属于角色的背景颜色扣掉,故称抠色。有些文件格式不支持透明通道(比如bmp文件),因此如果你想让纹理图片的某些部分透明,唯一的选择就是使用一个特定的颜色来欺骗玩家.OpenGL并不支持抠色,但通过纹理图片的Alpha通道可以很轻松地加入这个特性.这就是CTexture::SetColorKey函数所做的:
抠色(Color Keying)
它的实现很简单:遍历所有纹理数据,寻找指定颜色的像素点,将其Alpha通道设置为0,它就变得透明了.而对于其他像素点,将其Alpha通道设置为255.在这样做之前,我们必须先检查纹理是否已经指定给OpenGL了.若是,则必须在OpenGL中重新加载纹理.这只需要通过设置m_glId为0就可以完成(还记得吗?在Bind函数中会首先检查这个变量是否为0!).
最后,纹理是引用计数的,并且它的构造函数是受保护的,因此你无法直接创建一个CTexture对象.引用计数是通过下面两个函数实现的:
复制代码
void CTexture::AddReference()
{
// Increase the reference count.
m_iRefCount++;
}
void CTexture::ReleaseReference()
{
// Decrease the reference count. If it reaches 0,
// the texture is released from the texture manager.
m_iRefCount--;
if (m_iRefCount == 0)
CTextureManager::GetInstance()->ReleaseTexture(m_strTextName);
}
复制代码
之所以要使用引用计数,是因为多个CImage对象可以引用同一个纹理.我们必须知道此时有多少个CImage对象在使用此纹理,而不是当一个CImage对象销毁时就任意释放纹理资源.
本文转自Phinecos(洞庭散人)博客园博客,原文链接:http://www.cnblogs.com/phinecos/archive/2008/08/19/1271571.html,如需转载请自行联系原作者