`
dato0123
  • 浏览: 913679 次
文章分类
社区版块
存档分类
最新评论

【译】TetroGL: An OpenGL Game Tutorial in C++ for Win32 Platforms - Part 2 (上)

 
阅读更多

原文链接: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资源管理类
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->#include"Texture.h"
#include
<string>
#include
<map>
//Thetexturemanageravoidasametexturetobeloadedmultiple
//times.Itkeepsamapcontainingallthealreadyloadedtextures.
classCTextureManager
{
public:
//Loadsatexturespecifiedbyitsfilename.Ifthetextureisnot
//loadedalready,thetexturemanagerwillloadit,storeitand
//returnit.Otherwiseitsimplyreturnstheexistingone.
CTexture*GetTexture(conststd::string&strTextName);
//Releasethetexturespecifiedbyitsfilename.Returnstrueif
//thetexturewasfound,otherwisefalse.
boolReleaseTexture(conststd::string&strTextName);
//Returnsthesingleinstanceofthetexturemanager.
//Themanagerisimplementedasasingleton.
staticCTextureManager*GetInstance();
protected:
//Bothconstructoranddestructorareprotectedtomake
//itimpossibletocreateaninstancedirectly.
CTextureManager();
~CTextureManager();
private:
typedefstd::map
<std::string,CTexture*>TTextureMap;
//Themapofalreadyloadedtextures.Thereareindexed
//usingtheirfilename.
TTextureMapm_Textures;//已加载的资源文件映射表
};

这个类是以单例模式实现的.

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->CTextureManager*CTextureManager::GetInstance()
{
//Returnstheuniqueclassinstance.
staticCTextureManagerInstance;
return&Instance;
}

这样就可以拥有一个全局唯一的实例,并且访问它也十分简单:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->CTexture*pTexture=CTextureManager::GetInstance()->GetTexture("MyTexture.bmp");

这个类的构造函数负责对DevIL库进行初始化:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->CTextureManager::CTextureManager():m_Textures()
{
//InitializeDevIL
ilInit();
//Setthefirstloadedpointtothe
//upper-leftcorner.
ilOriginFunc(IL_ORIGIN_UPPER_LEFT);
ilEnable(IL_ORIGIN_SET);
}

在调用DevIL库函数前,必须先调用ilInit以便对库进行初始化.此外,我们还需要指明图片如何进行加载:先是左上方.这样做的目的是我们就不需要对纹理图片进行翻转.默认情况下这个选项是禁止的,因此我们需要调用ilEnable(IL_ORIGIN_SET);来使之设置为允许.

现在来看看GetTexture方法:

获取纹理资源
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->CTexture*CTextureManager::GetTexture(conststring&strTextName)
{
//Lookinthemapifthetextureisalreadyloaded.
TTextureMap::const_iteratoriter=m_Textures.find(strTextName);
if(iter!=m_Textures.end())
returniter->second;
//Ifitwasnotfound,trytoloaditfromfile.Iftheload
//failed,deletethetextureandthrowanexception.
CTexture*pNewText=NULL;
try
{
pNewText
=newCTexture(strTextName);
}
catch(CException&e)
{
deletepNewText;
throwe;
}
//Storethenewlyloadedtextureandreturnit.
m_Textures[strTextName]=pNewText;
returnpNewText;
}

很简单的实现代码:首先根据给定的文件名在映射表中查找文件是否已经加载进来了,若是则直接返回,否则就从文件中进行加载.待会我们会看到在CTexture类的构造函数中会尝试加载文件,若失败则抛出异常.因此,在纹理文件管理者类中,若捕获到此异常,就删除纹理文件(这是为了避免内存泄露)并且再次抛出异常.若文件加载成功,则将其保存到映射表中(以其文件名作为键值).

此外,我们还提供了释放已加载资源的方法,非常简单的实现:在映射表中查找,若存在就删除它,并且从映射表中移除.

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->boolCTextureManager::ReleaseTexture(conststd::string&strTextName)
{
//Retrievethetexturefromthemap
boolbFound=false;
TTextureMap::iteratoriter
=m_Textures.find(strTextName);
if(iter!=m_Textures.end())
{
//Ifitwasfound,wedeleteitandremovethe
//pointerfromthemap.
bFound=true;
if(iter->second)
deleteiter
->second;
m_Textures.erase(iter);
}
returnbFound;
}

资源包装类CTexture

CTexture类
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->#include<Windows.h>
#include"GL/gl.h"
#include<string>
classCTextureManager;
//Classthatwrapsinformationaboutatexture.Thisclass
//won'tbeuseddirectlybytheusers.Instead,theywill
//manipulatetheCImageclass.
classCTexture
{
friendclassCTextureManager;
public:
//Specifiesacolorkeytobeusedforthetexture.Thecolor
//specifedasargumentswillbetransparentwhenthetexture
//isrenderedonthescreen.
voidSetColorKey(unsignedcharRed,unsignedcharGreen,unsignedcharBlue);
//Returnsthewidthofthetexture
unsignedintGetWidth()const{returnm_TextData.nWidth;}
//Returnstheheightofthetexture.
unsignedintGetHeight()const{returnm_TextData.nHeight;}
//Adds/releaseareferenceforthetexture.WhenReleaseReference
//iscalledanddecreasesthereferencecountto0,thetexture
//isreleasedfromthetexturemanager.
voidAddReference();
voidReleaseReference();
//BindthistexturewithopenGL:thistexturebecomes
//the'active'textureinopenGL.
voidBind()const;
protected:
//Constructorwhichtakesthefilenameasargument.
//Itloadsthefileandthrowanexceptioniftheload
//failed.
CTexture(conststd::string&strFileName);
~CTexture();
private:
//Loadsthetexturefromthespecifedfile.Throwsan
//exceptioniftheloadfailed.
voidLoadFile(conststd::string&strFileName);
//Structurethatcontainstheinformationaboutthetexture.
structSTextureData
{
//Widthofthetexture
unsignedintnWidth;//纹理宽度
//Heightofthetexture
unsignedintnHeight;//纹理高度
//Bytearraycontainingthetexturedata
unsignedchar*pData;//包含纹理数据的字节数组
};
STextureDatam_TextData;
//TheopenGLidassociatedwiththistexture.
mutableGLuintm_glId;
//Referencecountofthenumberofimagesthatstillholdareference
//tothistexture.Whennoimagesreferencethetextureanymore,itis
//released.
intm_iRefCount;//引用计数
//Thefilenamefromwhichthetexturewasloadedfrom.
std::stringm_strTextName;
};

我们可以看到此类的构造函数是受保护的,这是因为只允许CTextureManager类能够创建纹理,这也是为什么将其设为此类的友元类.CTexture类的核心是STextureData结构体,它包含了从文件加载进的所有信息:包含文件数据的字节数组,纹理的宽度和高度.

下面看看究竟是如何加载文件的:

加载资源文件
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCTexture::LoadFile(conststd::string&strFileName)
{
//GenerateanewimageIdandbinditwiththe
//currentimage.
ILuintimgId;
ilGenImages(
1,&imgId);
ilBindImage(imgId);
//Loadthefiledatainthecurrentimage.
if(!ilLoadImage(strFileName.c_str()))
{
stringstrError="Failedtoloadfile:"+strFileName;
throwCException(strError);
}
//StorethedatainourSTextureDatastructure.
m_TextData.nWidth=ilGetInteger(IL_IMAGE_WIDTH);
m_TextData.nHeight
=ilGetInteger(IL_IMAGE_HEIGHT);
unsigned
intsize=m_TextData.nWidth*m_TextData.nHeight*4;//字节数,RGBA类型
m_TextData.pData=newunsignedchar[size];
ilCopyPixels(
0,0,0,m_TextData.nWidth,m_TextData.nHeight,
1,IL_RGBA,IL_UNSIGNED_BYTE,m_TextData.pData);
//Finally,deletetheDevILimagedata.
ilDeleteImage(imgId);
}

正如你看到的,我们使用DevIL来加载文件.首先要做的是创建一个新的图片id,并将其绑定到当前图片上.如果你想使用id对某个特定图片进行一些操作时,这是必需的.实际上,我们只需要在删除图片时使用它.然后,我们使用ilLoadImage尝试加载文件.这个函数负责处理各种不同的文件格式,当加载失败时返回false(你还可以调用ilGetError来查询其错误代码).若是这种情况,我们简单地抛出一个异常.如果你还记得,在第一篇文章中这些异常将会在main函数中被捕获,并且在退出程序前显示一个错误信息.接下来,我们获取图片的宽度和高度(ilGetIntegerilCopyPixels函数对当前活动图片总是有效的).然后,我们为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函数中实现:

纹理绑定
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCTexture::Bind()const
{
//IfthetexturehasnotbeengeneratedinOpenGLyet,
//generateit.
if(!m_glId)
{
//GenerateonenewtextureId.
glGenTextures(1,&m_glId);
//Makethistexturetheactiveone,sothateach
//subsequentglTex*callswillaffectit.
glBindTexture(GL_TEXTURE_2D,m_glId);
//Specifyalinearfilterforboththeminificationand
//magnification.
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
//SetsdrawingmodetoGL_MODULATE
glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);
//Finally,generatethetexturedatainOpenGL.
glTexImage2D(GL_TEXTURE_2D,0,4,m_TextData.nWidth,m_TextData.nHeight,
0,GL_RGBA,GL_UNSIGNED_BYTE,m_TextData.pData);
}
//MaketheexistingtexturespecifiedbyitsOpenGLid
//theactivetexture.
glBindTexture(GL_TEXTURE_2D,m_glId);
}

OpenGL重要的一点是它每次只能使用一个纹理.因此,要想对一个多边形贴纹理,就必须选中活动纹理(也叫绑定”).这通过调用glBindTexture来完成.每个OpenGL纹理都有其id,这里我们将其存储在 CTexture类的m_glId成员变量中.id0表明纹理还没有被OpenGL产生出来.因此,当此函数第一次被调用时,m_glId将会是0.此时我们将会调用glGenTextures来请求OpenGL产生一个id.

m_glIdmutable,这是因为我们想让Bind函数是const,而这个成员变量只被修改一次(当纹理被产生时对其修改).glGenTextures函数可以允许你产生多个Id(第一个参数就是要产生的Id个数),但我们只想要单个Id.然后我们调用glBindTexture:这将绑定纹理(通过其Id)到活动的2维纹理上.这是必须的,因为接下来的纹理操作将会影响到你这里指定的特定纹理.

接下来的纹理操作就不解释了,可以参考红宝书

抠色(Color Keying

我们总是blit矩形区域的图片,但是很显然,几乎没有一个游戏的角色图片是矩形的。美工把图片画到一个矩形范围内,如果设定了特定的背景颜色,我们就可以把矩形图片上的角色下来,相对于背景来说,我们就是把不属于角色的背景颜色扣掉,故称抠色。有些文件格式不支持透明通道(比如bmp文件),因此如果你想让纹理图片的某些部分透明,唯一的选择就是使用一个特定的颜色来欺骗玩家.OpenGL并不支持抠色,但通过纹理图片的Alpha通道可以很轻松地加入这个特性.这就是CTexture::SetColorKey函数所做的:

抠色(Color Keying)
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCTexture::SetColorKey(unsignedcharRed,unsignedcharGreen,unsignedcharBlue)
{
//IfthetexturehasalreadybeenspecifiedtoOpenGL,
//wedeleteit.
if(m_glId)
{
glDeleteTextures(
1,&m_glId);
m_glId
=0;
}
//Forallthepixelsthatcorrespondtothespecifedcolor,
//setthealphachannelto0(transparent)andresettheother
//onesto255.
unsignedlongCount=m_TextData.nWidth*m_TextData.nHeight*4;
for(unsignedlongi=0;i<Count;i+=4)
{
if((m_TextData.pData[i]==Red)&&(m_TextData.pData[i+1]==Green)
&&(m_TextData.pData[i+2]==Blue))
m_TextData.pData[i
+3]=0;//将指定颜色的像素点设置透明的
else
m_TextData.pData[i
+3]=255;//其他颜色的像素点设置为不透明
}
}

它的实现很简单:遍历所有纹理数据,寻找指定颜色的像素点,将其Alpha通道设置为0,它就变得透明了.而对于其他像素点,将其Alpha通道设置为255.在这样做之前,我们必须先检查纹理是否已经指定给OpenGL.若是,则必须在OpenGL中重新加载纹理.这只需要通过设置m_glId0就可以完成(还记得吗?Bind函数中会首先检查这个变量是否为0!).

最后,纹理是引用计数的,并且它的构造函数是受保护的,因此你无法直接创建一个CTexture对象.引用计数是通过下面两个函数实现的:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCTexture::AddReference()
{
//Increasethereferencecount.
m_iRefCount++;
}
voidCTexture::ReleaseReference()
{
//Decreasethereferencecount.Ifitreaches0,
//thetextureisreleasedfromthetexturemanager.
m_iRefCount--;
if(m_iRefCount==0)
CTextureManager::GetInstance()
->ReleaseTexture(m_strTextName);
}

之所以要使用引用计数,是因为多个CImage对象可以引用同一个纹理.我们必须知道此时有多少个CImage对象在使用此纹理,而不是当一个CImage对象销毁时就任意释放纹理资源.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics