`
phinecos
  • 浏览: 341519 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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

 
阅读更多

原文链接:TetroGL: An OpenGL Game Tutorial in C++ for Win32 Platforms - Part 2

CImage

现在我们来看看CImage类究竟是如何使用纹理图片的.早前已经看到,用户无法直接对CTexture对象进行操作.这是因为它仅仅是对一个资源文件进行包装,而这样的文件可以由多个图片组成:假设你想在游戏中显示各种类型的树,那么将它们存储在同一个文件是很方便的.因此,纹理类本身并没有任何用来在屏幕上绘制图片的函数,而仅仅只有加载文件的函数.CImage类才是负责在屏幕上绘制纹理(或者仅其一部分).多个图片可以引用同一个纹理,但使用其不同部分.

CImage类
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->#include"Texture.h"
#include
"Rectangle.h"
#include
"SmartPtr.h"
classCImage;
//TypedefofaCImageclassthatiswrappedinsideasmart
//pointer.
typedefCSmartPtr<CImage>TImagePtr;
//Animageismanipulateddirectlybytheenduser(insteadof
//thetexture).Themaindifferencebetweenanimageandatexture
//isthatthetexturecancontainmultipleimages(itisthe
//completefile).
classCImage
{
public:
//Blittheimageatthespecifiedlocation
voidBlitImage(intiXOffset=0,intiYOffset=0)const;
//Returnsthetexturethatthisimageisusing.
CTexture*GetTexture()const{returnm_pTexture;}
//Helperfunctionstocreateannewimage.Asmartpointer
//holdingthenewimageisreturned.strFileNameisthe
//nameofthefilecontainingthetextureandtextCoordis
//therectangleinthistexturewhichcontainstheimage.
staticTImagePtrCreateImage(conststd::string&strFileName);
staticTImagePtrCreateImage(conststd::string&strFileName,
constTRectanglei&textCoord);
~CImage();
protected:
//Protectedconstructorstoavoidtobeabletocreatea
//CImageinstancedirectly.
CImage(conststd::string&strFileName);
CImage(
conststd::string&strFileName,constTRectanglei&textCoord);
private:
//Thetexturefromwhichthisimageispartof.
CTexture*m_pTexture;
//Therectanglethatspecifiesthepositionoftheimage
//inthefulltexture.
TRectangleim_rectTextCoord;//纹理坐标
};

此类有两个成员变量:图片所在的纹理,一个表明指明了图片占了纹理的那些部分的矩形.下面再来看BlitImage函数如何实现的,它将在参数指定的位置处绘制纹理:

绘制纹理
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCImage::BlitImage(intiXOffset,intiYOffset)const
{
if(m_pTexture)
{
m_pTexture
->Bind();
//Getthecoordinatesoftheimageinthetexture,expressed
//asavaluefrom0to1.
floatTop=((float)m_rectTextCoord.m_Top)/m_pTexture->GetHeight();
floatBottom=((float)m_rectTextCoord.m_Bottom)/m_pTexture->GetHeight();
floatLeft=((float)m_rectTextCoord.m_Left)/m_pTexture->GetWidth();
floatRight=((float)m_rectTextCoord.m_Right)/m_pTexture->GetWidth();
//Drawthetexturedrectangle.
glBegin(GL_QUADS);
glTexCoord2f(Left,Top);glVertex3i(iXOffset,iYOffset,
0);
glTexCoord2f(Left,Bottom);glVertex3i(iXOffset,iYOffset
+m_rectTextCoord.GetHeight(),0);
glTexCoord2f(Right,Bottom);glVertex3i(iXOffset
+m_rectTextCoord.GetWidth(),iYOffset+m_rectTextCoord.GetHeight(),0);
glTexCoord2f(Right,Top);glVertex3i(iXOffset
+m_rectTextCoord.GetWidth(),iYOffset,0);
glEnd();
}
}

我们首先对纹理进行绑定(让其在OpenGL中是活动的),然后图片在纹理中的坐标.这些值在01之间,0表示纹理的左上方,1表示纹理的右下方.然后如第一篇文章所示绘制一个矩形,所不同的在于指明各个点之前,我们调用了glTexCoord2f函数,它指明了当前绑定的OpenGL纹理的纹理点坐标.通过这样做,OpenGL就能够使用活动纹理来显示出贴纹理后的矩形.

现在来看看构造函数和析构函数.这里有两个受保护的构造函数.

构造函数
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->CImage::CImage(conststring&strFileName):m_pTexture(NULL),m_rectTextCoord()
{
//Thislinewillthrowanexceptionifthetextureisnotfound.
m_pTexture=CTextureManager::GetInstance()->GetTexture(strFileName);
m_pTexture
->AddReference();
//Setthetexturecoordinatetothefulltexture
m_rectTextCoord.m_Top=m_rectTextCoord.m_Left=0;
m_rectTextCoord.m_Bottom
=m_pTexture->GetHeight();
m_rectTextCoord.m_Right
=m_pTexture->GetWidth();
}
CImage::CImage(
conststring&strFileName,constTRectanglei&textCoord)
:m_pTexture(NULL),m_rectTextCoord(textCoord)
{
//Thislinewillthrowanexceptionifthetextureisnotfound.
m_pTexture=CTextureManager::GetInstance()->GetTexture(strFileName);
m_pTexture
->AddReference();
}

析构函数仅仅简单地释放纹理,而这将会减少纹理的引用计数(如前所示):

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->CImage::~CImage()
{
if(m_pTexture)
m_pTexture
->ReleaseReference();
}

这里作者将此类的构造函数设置为受保护,这是为了强迫用户使用包装了CImage类的智能指针.出于内存泄露的缘故,这点是值得考虑的.关于智能指针的内容就不多介绍了在本文中作者使用了自己开发的一个智能指针,但使用boost::shared_ptr则更好.

最后,CImage类提供了两个静态方法,从而允许我们创建此类的实例.方法实现很简单:创建一个新实例,传递给一个智能指针,并返回此指针.

创建CImage实例
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->TImagePtrCImage::CreateImage(conststring&strFileName)
{
TImagePtrimgPtr(
newCImage(strFileName));
returnimgPtr;
}
TImagePtrCImage::CreateImage(
conststring&strFileName,constTRectanglei&textCoord)
{
TImagePtrimgPtr(
newCImage(strFileName,textCoord));
returnimgPtr;
}

显示动画

2D游戏动画背后的思想很简单:就像卡通一样,将动作分解为离散的图片.最直观的方法就是使用一个循环,在循环中每显示一幅图片后就休眠一段时间.也许你已经猜到了,这根本不可行.有几个问题:首先,因为你从来不交换缓冲区(这在CMainWindow::Draw函数中做的),根本不会显示任何东西.第二,如果你那样做了,你程序的其他部分就无法执行,也就意味着你只能每次显示一个动画.正确的方法是:让每幅动画记住自己的状态(例如,当前显示的是那副图片),并且请求它们去绘制它们的当前图片.当一个新的帧需要被绘制时,每幅动画就会被要求跳转到动画的下一副图片去.

下面我们来看看CImageList.此类是std::list的一个简单包装类,它用来保存图片,并且提供了一些帮助函数来播放图片.

CImageList类
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->#include"Image.h"
#include
<list>
//Wrapsalistofimageswhichisusedtoplayanimations.
classCImageList
{
public:
//Defaultconstructor:constructanemptylist.
CImageList();
//Copyconstructor:copiesthecontentofthe
//listpassedinargument.
CImageList(constCImageList&other);
//Defaultdestructor.
~CImageList();
//Assignementoperator:emptythecurrentcontent
//andcopiesthecontentofthelistpassedinargument.
CImageList&operator=(constCImageList&other);
//Emptythecontentofthelist
voidClear();
//Appendanewimagetothelist
voidAppendImage(TImagePtrpImage);
//Returnthenumberofimagesinthislist
unsignedGetImagesCount()const;
//Makethefirstimageactive
voidGoToFirstImage();
//Makethenextimageactive.Ifthelastimage
//wasactive,wegobacktothefirstimage.In
//thatcase,thefunctionreturnstrue.
boolGoToNextImage();
//Getthecurrentimage
TImagePtrGetCurrentImage()const;
private:
//Typedefforastd::listcontainingTImagePtrobjects
typedefstd::list<TImagePtr>TImageList;
//Thelistofimages
TImageListm_lstImages;
//Iteratorpointingtothecurrentimage
TImageList::iteratorm_iterCurrentImg;
};

它的实现很简单:在需要的时候增加图片到std::list<TImagePtr>,并且维护一个迭代器,它指明了当前活动的图片.GoToNextImage为例:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->boolCImageList::GoToNextImage()
{
if(m_iterCurrentImg!=m_lstImages.end())
m_iterCurrentImg
++;
else
returnfalse;
if(m_iterCurrentImg==m_lstImages.end())
{
m_iterCurrentImg
=m_lstImages.begin();
returntrue;
}
returnfalse;
}

下面我们来看看CAnimatedSprite,此类允许你将多个动画组合到一起.举个例子说明下:假设你写一个游戏,在游戏里玩家扮演一位骑士.骑士当然就有多种动作啦:行走,攻击,静止站立等等.一般来说,你需要为这些动作在每个方向都提供动画.这个类就可以用来表示你的骑士:你可以加载多个动画,并按照命令不断播放它们.

CAnimatedSprite类
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->#include"Image.h"
#include
"ImageList.h"
#include
<string>
#include
<map>
#include
<list>
//Thisclassrepresentananimatedsprite:itisabletoplay
//differentanimationsthatwerepreviouslyloaded.
classCAnimatedSprite
{
public:
//Defaultconstructoranddestructor.
CAnimatedSprite();
~CAnimatedSprite();
//Addsanewanimationforthesprite.ThestrAnimName
//isastringthatidentifiestheanimationandshould
//beuniqueforthissprite.
voidAddAnimation(conststd::string&strAnimName,
constCImageList&lstAnimation);
//Playsapreviouslyloadedanimation.ThestrAnimName
//isthenamethatwaspassedwhencallingAddAnimation.
voidPlayAnimation(conststd::string&strAnimName);
//Drawthecurrentframeoftheanimationatthesprite
//currentposition.
voidDrawSprite();
//Gotothenextanimationframe.
voidNextFrame();
//Setthepositionofthesprite
voidSetPosition(intXPos,intYPos)
{
m_iXPos
=XPos;
m_iYPos
=YPos;
}
//Movethespritefromitscurrentposition
voidOffsetPosition(intXOffset,intYOffset)
{
m_iXPos
+=XOffset;
m_iYPos
+=YOffset;
}
private:
typedefstd::map
<std::string,CImageList>TAnimMap;
typedefTAnimMap::iteratorTAnimMapIter;
//Mapcontainingalltheanimationsthatcanbe
//played.
TAnimMapm_mapAnimations;
//Iteratortothecurrentanimationbeingplayed
TAnimMapIterm_iterCurrentAnim;
//Positionofthesprite
intm_iXPos;
intm_iYPos;
};

此类的实现如下:它包含了一个map,其中存储了可以用于sprite的所有动画,键为一个动画名称,值为一个CImageList对象,这个对象包含了动画.AddAnimationPlayAnimation函数用于在map中增加或查询动画:

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCAnimatedSprite::AddAnimation(conststring&strAnimName,constCImageList&lstAnimation)
{
m_mapAnimations[strAnimName]
=lstAnimation;
}
voidCAnimatedSprite::PlayAnimation(conststring&strAnimName)
{
m_iterCurrentAnim
=m_mapAnimations.find(strAnimName);
if(m_iterCurrentAnim==m_mapAnimations.end())
{
stringstrError="Unabletoplay:"+strAnimName;
strError
+=".Animationnotfound.";
throwCException(strError);
}
}

当想播放一个不存在的动画时,则会抛出一个异常. 成员变量m_iterCurrentAnim是一个指向当前动画的迭代器.它在DrawSpriteNextFrame函数中用来访问当前动画:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCAnimatedSprite::DrawSprite()
{
if(m_iterCurrentAnim==m_mapAnimations.end())
return;
m_iterCurrentAnim
->second.GetCurrentImage()
->BlitImage(m_iXPos,m_iYPos);
}
voidCAnimatedSprite::NextFrame()
{
if(m_iterCurrentAnim==m_mapAnimations.end())
return;
m_iterCurrentAnim
->second.GoToNextImage();
}
示例代码

现在是用一个具体的示例来展示我们将如何使用上面这些类的时候了.示例很简单,将有一个动画角色(一个骑士),它能通过方向键控制行走,它在一副简单的地图上行进.现在还有碰撞检测,这就意味着它能穿过树木,另一个没有实现的是sprites被绘制的顺序:骑士总是绘制在场景的上面,无论他在哪里,这当然在有些情况下是不对的(比如说他隐藏在树后面).

所有代码都实现在CMainWindow类中,我们首先在类中加入一些成员变量:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->//Theimageforthegrass.
TImagePtrm_pGrassImg;
//Imagesforthetrees
TImagePtrm_pTreesImg[16];
//Theanimatedspriteoftheknight
CAnimatedSprite*m_pKnightSprite;
//Whichkeysarecurrentlypressed
boolm_KeysDown[4];
//Thelastdirectionoftheknight
std::stringm_strLastDir;

我们首先定义了一些TImagePtr,它们用来表示将要绘制的图片(草地和树木).然后定义了CAnimatedSprite,它用来绘制骑士.最后定义了一个bool数组,用来存储方向键的当前状态,此外还有一个字符串用来表明骑士的当前方向.这些成员变量在主窗口类的构造函数中进行初始化:

初始化
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->//Loadthegrassimageandsetthecolorkey.
m_pGrassImg=CImage::CreateImage("GrassIso.bmp");
m_pGrassImg
->GetTexture()->SetColorKey(0,128,128);
//Loadthetreeimages
for(inti=0;i<16;i++)
{
TRectangleiimgRect(i
/4*128,(i/4+1)*128,(i%4)*128,(i%4+1)*128);
m_pTreesImg[i]
=CImage::CreateImage("Trees.bmp",imgRect);
}
CTextureManager::GetInstance()
->GetTexture("Trees.bmp")
->SetColorKey(191,123,199);
//Loadallthe'walk'animationsfortheknight.
m_pKnightSprite=newCAnimatedSprite;
CAnimFileLoaderfileLoader1(
"KnightWalk.bmp",8,96,96);
CTextureManager::GetInstance()
->GetTexture("KnightWalk.bmp")
->SetColorKey(111,79,51);
m_pKnightSprite
->AddAnimation("WalkE",
fileLoader1.GetAnimation(
0,7));
m_pKnightSprite
->AddAnimation("WalkSE",
fileLoader1.GetAnimation(
8,15));
m_pKnightSprite
->AddAnimation("WalkS",
fileLoader1.GetAnimation(
16,23));
m_pKnightSprite
->AddAnimation("WalkSW",
fileLoader1.GetAnimation(
24,31));
m_pKnightSprite
->AddAnimation("WalkW",
fileLoader1.GetAnimation(
32,39));
m_pKnightSprite
->AddAnimation("WalkNW",
fileLoader1.GetAnimation(
40,47));
m_pKnightSprite
->AddAnimation("WalkN",
fileLoader1.GetAnimation(
48,55));
m_pKnightSprite
->AddAnimation("WalkNE",
fileLoader1.GetAnimation(
56,63));
//Loadallthe'pause'animationsfortheknight.
CAnimFileLoaderfileLoader2("KnightPause.bmp",8,96,96);
CTextureManager::GetInstance()
->GetTexture("KnightPause.bmp")
->SetColorKey(111,79,51);
m_pKnightSprite
->AddAnimation("PauseE",
fileLoader2.GetAnimation(
0,7));
m_pKnightSprite
->AddAnimation("PauseSE",
fileLoader2.GetAnimation(
8,15));
m_pKnightSprite
->AddAnimation("PauseS",
fileLoader2.GetAnimation(
16,23));
m_pKnightSprite
->AddAnimation("PauseSW",
fileLoader2.GetAnimation(
24,31));
m_pKnightSprite
->AddAnimation("PauseW",
fileLoader2.GetAnimation(
32,39));
m_pKnightSprite
->AddAnimation("PauseNW",
fileLoader2.GetAnimation(
40,47));
m_pKnightSprite
->AddAnimation("PauseN",
fileLoader2.GetAnimation(
48,55));
m_pKnightSprite
->AddAnimation("PauseNE",
fileLoader2.GetAnimation(
56,63));
m_pKnightSprite
->PlayAnimation("PauseE");
for(inti=0;i<4;i++)
m_KeysDown[i]
=false;
//Settheinitialdirectiontotheeast.
m_strLastDir="E";
m_pKnightSprite
->SetPosition(350,250);

这里我们使用了一个新的类:CAnimFileLoader,它是一个用来从文件中加载一个图片列表的帮助类.它的构造函数以文件名称,每行图片数目,图片宽度和高度作为参数,过后你可以通过指定在文件中图片的起始索引位置和结束索引位置来查询图片列表(它会返回一个CImageList对象).再回过头来看上面的代码,首先我们加载草地图片,并对其进行抠色,然后为骑士加载所有的行走动画.每个动画的名字取决于其方向, 比如,向东行走,动画名称就是WalkE.最后指明默认的动画是”PauseE”动画.

现在来看看我们是如何处理键盘事件的:

事件处理
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCMainWindow::ProcessEvent(UINTMessage,WPARAMwParam,LPARAMlParam)
{
switch(Message)
{
//Quitwhenweclosethemainwindow
caseWM_CLOSE:
PostQuitMessage(
0);
break;
caseWM_SIZE:
OnSize(LOWORD(lParam),HIWORD(lParam));
break;
caseWM_KEYDOWN:
switch(wParam)
{
caseVK_UP:
m_KeysDown[
0]=true;
break;
caseVK_DOWN:
m_KeysDown[
1]=true;
break;
caseVK_LEFT:
m_KeysDown[
2]=true;
break;
caseVK_RIGHT:
m_KeysDown[
3]=true;
break;
}
UpdateAnimation();
break;
caseWM_KEYUP:
switch(wParam)
{
caseVK_UP:
m_KeysDown[
0]=false;
break;
caseVK_DOWN:
m_KeysDown[
1]=false;
break;
caseVK_LEFT:
m_KeysDown[
2]=false;
break;
caseVK_RIGHT:
m_KeysDown[
3]=false;
break;
}
UpdateAnimation();
break;
}
}

当方向键按下时,我们只是简单地在bool数组中设置或重设标志位,以此来表明对应键的状态,然后调用UpdateAnimation函数:

UpdateAnimation
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCMainWindow::UpdateAnimation()
{
//Firstcheckifatleastonekeyispressed
boolkeyPressed=false;
for(inti=0;i<4;i++)
{
if(m_KeysDown[i])
{
keyPressed
=true;
break;
}
}
stringstrAnim;
if(!keyPressed)
strAnim
="Pause"+m_strLastDir;
if(keyPressed)
{
stringvertDir;
stringhorizDir;
if(m_KeysDown[0])
vertDir
="N";
elseif(m_KeysDown[1])
vertDir
="S";
if(m_KeysDown[2])
horizDir
="W";
elseif(m_KeysDown[3])
horizDir
="E";
m_strLastDir
=vertDir+horizDir;
strAnim
="Walk"+m_strLastDir;
}
m_pKnightSprite
->PlayAnimation(strAnim);
}

我们首先检查是否至少有一个键被按下.若没有,则指定播放的动画应该是”Pause”+最后一次骑士方向的名字.若至少有一个键被按下,我们检查哪些被按下了,并构建出上一个方向字符串.我们再看看Draw函数:

绘制代码
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCMainWindow::Draw()
{
//Clearthebuffer
glClear(GL_COLOR_BUFFER_BIT);
//Drawthegrass
intxPos=0,yPos=0;
for(inti=0;i<8;i++)
{
for(intj=0;j<6;j++)
{
xPos
=i*256/2-128;
if(i%2)
yPos
=(j*128)-128/2;
else
yPos
=(j*128);
m_pGrassImg
->BlitImage(xPos,yPos);
}
}
//Drawsometrees
m_pTreesImg[0]->BlitImage(15,25);
m_pTreesImg[
1]->BlitImage(695,55);
m_pTreesImg[
2]->BlitImage(15,25);
m_pTreesImg[
3]->BlitImage(300,400);
m_pTreesImg[
4]->BlitImage(125,75);
m_pTreesImg[
5]->BlitImage(350,250);
m_pTreesImg[
6]->BlitImage(400,350);
m_pTreesImg[
7]->BlitImage(350,105);
m_pTreesImg[
8]->BlitImage(530,76);
m_pTreesImg[
9]->BlitImage(125,450);
m_pTreesImg[
10]->BlitImage(425,390);
m_pTreesImg[
11]->BlitImage(25,125);
m_pTreesImg[
12]->BlitImage(550,365);
m_pTreesImg[
13]->BlitImage(680,250);
m_pTreesImg[
14]->BlitImage(245,325);
m_pTreesImg[
15]->BlitImage(300,245);
//Drawtheknight
m_pKnightSprite->DrawSprite();
//Movetothenextframeoftheanimation
m_pKnightSprite->NextFrame();
//Swapthebuffers
SwapBuffers(m_hDeviceContext);
}

移动骑士精灵是在Update函数中完成的:

更新骑士坐标
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->voidCMainWindow::Update(DWORDdwCurrentTime)
{
intxOffset=0;
intyOffset=0;
if(m_KeysDown[0])
yOffset
-=5;
if(m_KeysDown[1])
yOffset
+=5;
if(m_KeysDown[2])
xOffset
-=5;
if(m_KeysDown[3])
xOffset
+=5;
m_pKnightSprite
->OffsetPosition(xOffset,yOffset);
}
若方向键按下,我们就在其方向上移动一定的偏移量.其实由于时间也传递进来了,我们也可以根据流逝的时间值来计算偏移量.

小结

到此为止,这个系列的第二部分就结束了.在本文中,我们学习了如何加载图片文件并将其绘制到屏幕上,以及如何绘制动画.下一篇文章,同时也是这个系列的最后一篇文章中,我们将看到如何在屏幕上输出文本,如何管理游戏的不同状态,并最终实现一个具体的实例:一个类似俄罗斯方块的游戏.敬请期待….

References

[1] Singleton article: a good introduction to the singleton pattern.
[2] Shared pointers: an extensive article about shared pointers.
[3] Boost shared_ptr: the boost library about shared_ptr.
[4] Reiner's tileset: free resources from which the images of the example were taken from.
[5] DevIL: DevIL library.
[6] FreeImage: FreeImage library.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics