本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
文章链接:http://blog.csdn.net/zhmxy555/article/details/8499438
作者:毛星云(浅墨)邮箱:[email protected]
本篇文章里,我们对Direct3D之中固定功能流水线中的3D光照编程相关的知识进行了详尽的剖析,文章末尾依旧是提供文章配套的详细注释的demo源代码的欣赏,并在文章末尾提供了源代码下载。
一、引言
光,乃万物之源。我们根本无法想象,这个美丽怡人的世界,如果没有光的陪伴,会是怎样的一副满目疮痍。计算机3D世界作为现实世界的高度逼真的模仿,必然也少不了光的陪伴。回到我们的Direct3D应用程序中来,在Direct3D中运用光照,能有效地增强3D场景的真实感。在3D场景中使用光照其实非常地简单,我们不需要为物体的每个顶点都去指定颜色值,只要我们告诉Direct3D我们使用的是什么类型的光照,我们的物体的材质的具体参数以及物体表面相对于光源的朝向,Direct3D就会根据其内置的算法计算出每个顶点的颜色值,产生出逼真的光照效果。
当然随着我们学习的深入,功力的加深,就可以不单单依赖于Direct3D中内建的光照算法,可以根据各种功能的着色器的编写,自己写出更加优化更加逼真的光照效果来。
作为目前刚刚接触到Direct3D中的光照和材质这一块内容,我们还是老老实实地先把固定功能流水线中的这一套非常好学好掌握的光照与材质的体系系统地进行讲解,先把基础打牢,先把走学会,这样才能为后面我们的腾飞做铺垫。
说到一套完整的光照体系,有两对组成方面,第一,光照,第二,材质。这两者天生就是一对好搭档,我们可以把它们看做光照计算的两要素,想要绘制出具有光照的真实三维世界,两者缺一不可。
下面就开始正式讲解,首先我们来看看四大光照类型:
二、四大光照类型
1.环境光(Ambient Light)
一个物体即使没有直接被光源照射,但是只要有光线通过其他物体的折射、反射到达物体,它也可能被看见。这种基于整个自然界环境的整体亮度,称为环境光(Ambient Light)或者背景光。环境光没有位置或者方向上的特征,只有一个颜色亮度值,而且不会衰减,所以在所有方向和所有物体表面上投射的环境光的数量是恒定不变的。想要以较低的代价和开销来近似模拟光照的话,直接开启环境光是一个不错的选择。
在Direct3D中环境光的设置非常简单,也就是用一下SetRenderState,代码如下:
pd3dDevice->SetRenderState(D3DRS_AMBIENT,D3DCOLOR_XRGB(36, 36, 36)); //设置一下环境光
其中第一个参数填D3DRS_AMBIENT,代表环境光的设置,而第二个参数填一个颜色值就可以了。
2.漫反射光(DiffuseLight)
漫反射光在我们的生活中最为普遍,太阳的直射,日光灯的照射都可以看成漫反射的近似。这种类型的光沿着特定的方向传播。当它到达某一表面时,将沿着各个方向均匀反射,所以我们无论从哪个方向观察,物体表面的亮度都是相同的,所以采用漫反射这种光照模型时,无需考虑观察者的位置,但是需要考虑漫反射光的空间位置和方向。从一个光源发出的光一般都是这种类型的。漫反射光并没有简洁的设置方法,具体下文会讲到的,请大家继续往下看。
3.镜面反射光(SpecularLight)
镜面反射光,顾名思义,沿着特定的方向传播,当此类光到达一个表面时,将严格地沿着另一个方向反射,从而形成只能在一个角度范围内才能观察到的高亮度照射。这种光照模型模拟了从光滑发光面如镜子、一块金属或者一块发光塑料等材料来进行光线反射的情形。如果我们移动一下光源的话,就会发现镜面亮光区所发生的变化,这意味着镜面反射取决于观察者的角度。我们可以这样来归纳,漫反射与视觉无关,而镜面反射与视觉相关。
需要注意的是,镜面光与其他类型的光相比,计算量要大得多,Direct3D默认情况是把镜面反射关起来的。如果我们想启用镜面反射的话,用下面的代码,即把渲染状态D3DRS_SPECULARENABLE设为true:
pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE,true);
4.自发光
自发光就是对象自己发出的光,其实他是根据通过对象的自发光材质实现的,下面我们在讲解材质时讲到的D3DMATERIAL9结构体,这个结构体的成员Emissive描述自发光的颜色和透明度。自发光影响着一个对象的颜色,比如我们可以通过设置自发光的颜色属性,把一些灰暗的材质变得明亮一些。需要提出来的是,我们可以使用材质的自发光属性来“照亮”这个对象,而不用在场景中内部添加灯光,从而缩小了计算量。自发光属性创建的材质并不发射出能被场景内其他对象反射的光,也就是说,它发出的光并不参与光运算,而为了实现反射光,需要在场景中添加额外的灯光。
讲解完四大光照类型,接下来当然少不了三大光源类型。
三、三大光源类型
在Direct3D中的光源类型和光照类型是两个完全不同的概念,光照模型描述的是光线的反射特征,而光源类型主要强调的是能够产生这些光照模型的方式以及光线的位置、方向、强度等特征。
Direct3D中主要有3种类型的的光源,我们把他们合称为三大光源:点光源(Point Light)、方向光(Directional Light)和聚光灯(Spot Light)。
而在Direct3D 9.0c中,讲到光源,必须讲到一个结构体,那就是D3DLIGHT9结构体,在展开讲解各中光源类型之前,先让我们看看这个结构体的具体内容,我们可以在MSDN中查到这个结构体有如下原型:
typedef structD3DLIGHT9 { D3DLIGHTTYPE Type; D3DCOLORVALUE Diffuse; D3DCOLORVALUE Specular; D3DCOLORVALUE Ambient; D3DVECTOR Position; D3DVECTOR Direction; float Range; float Falloff; float Attenuation0; float Attenuation1; float Attenuation2; float Theta; float Phi; } D3DLIGHT9,*LPD3DLIGHT;
■ 第一个参数,D3DLIGHTTYPE类型的Type,表示光源的类型,在D3DLIGHTTYPE这个枚举体中取值,而D3DLIGHTTYPE枚举体有如下定义:
typedef enumD3DLIGHTTYPE { D3DLIGHT_POINT = 1, D3DLIGHT_SPOT = 2, D3DLIGHT_DIRECTIONAL = 3, D3DLIGHT_FORCE_DWORD = 0x7fffffff } D3DLIGHTTYPE,*LPD3DLIGHTTYPE;
我们可以由英语单词很容易理解到,D3DLIGHT_POINT为点光源类型,D3DLIGHT_SPOT为聚光灯类型,D3DLIGHT_DIRECTIONAL为方向光源类型,而最后一个参数依然是前面我们几次提到的没有存在感的那个,我们不用去纠结它。
■ 第二个参数到第四个参数是一个类型的,我们结合起来讲解。这三个参数都为D3DCOLORVALUE类型的颜色值,而参数名分别为Diffuse,Specular,Ambient,分别表示我们这个光源的漫反射,镜面反射和环境光的颜色值。
■ 第五个参数,D3DVECTOR类型的Position,表示光源的位置。
■ 第六个参数,D3DVECTOR类型的Direction,表示光源的光照方向。
■ 第七个参数,float类型的Range,表示光源的光照范围,只在某些光源类型中有意义。
■ 第八个参数以及第十二,第十三个参数又是一个类型的,我们依然一起来介绍。也就是同为float类型的Falloff,Theta以及Phi,这三个参数都是用在聚光灯光源类型中的,也就是说只有我们把D3DLIGHT9的第一个参数Type设为D3DLIGHT_SPOT聚光灯类型的时候,这三个参数才有意义。
■第九、第十、第十一这三个参数显然也是同一阵营的,我们也一起介绍,Attenuation0~ Attenuation2都为衰减系数,定义了光强随着距离衰减的方式,衰减公式如下:
其中,D为光源到顶点的距离,A0~A2分别对应于Attenuation0~ Attenuation2。
讲解完成,下面我们看看具体怎么使用。在Direct3D中使用光照的话,也就是用我们这个D3DLIGHT9结构体实例化一个具体的光源类型,然后无脑地进行喜闻乐见的填空题操作,对这个结构体的参数进行赋值,赋值完成后调用IDirect3DDevice9接口的SetLight方法设置光源,然后调用IDirect3DDevice9接口的LightEnable方法启用光照就可以了。下面我们来分别看看这两个方法。首先是SetLight,SetLight方法用于设置光源:
HRESULT SetLight( [in] DWORD Index, [in] const D3DLIGHT9 *pLight );
■ 第一个参数,DWORD类型的Index,取值于0到7之间,表示选择第1到8个光源。
■ 第二个参数,const D3DLIGHT9类型的*pLight,显然就是指向D3DLIGHT9结构体的指针,包含设置好的灯光类型,我们在使用的时候,在这里就填我们之前实例化的D3DLIGHT9结构体的名称,并在名称之前加一个“&”取地址符号就可以了。
然后是LightEnable方法,LightEnable方法用于启用光照:
HRESULTLightEnable( [in] DWORD LightIndex, [in] BOOL bEnable );
■ 第一个参数,DWORD类型的Index,取值于0到7之间,表示选择第1到8个光源。
■ 第二个参数,BOOL类型的bEnable,填true或者flase表示启用或者禁用第一个参数里面指定的光照。
讲解完需要用到的一个结构体和两个方法,下面我们就来进入三大光源的讲解。
1.点光源
点光源(Point Light)具有颜色和位置,但没有方向,它向所有方向发射的光都一样。
它是一个从中心向空间中各个方向发射相等强度光线的光源,且光的亮度会不随着距离而衰减。要定义一个点光源的话,实例化一个D3DLIGHT9结构体,将第一个参数设为D3DLIGHT_POINT然后进行其余参数的设置即可,这样实例化出的这个结构体就是一个点光源了。下面我们看一个点光源设置的实例:
D3DLIGHT9 light; ::ZeroMemory(&light,sizeof(light)); light.Type = D3DLIGHT_POINT;//点光源 light.Ambient = D3DXCOLOR(0.8f, 0.8f, 0.8f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular =D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); light.Position = D3DXVECTOR3(0.0f, 200.0f, 0.0f); light.Attenuation0 = 1.0f; light.Attenuation1 = 0.0f; light.Attenuation2 = 0.0f; light.Range = 300.0f; pd3dDevice->SetLight(0,&light); //设置光源 pd3dDevice->LightEnable(0,true); //启用光照
上面的这段代码非常好理解,首先实例化一个D3DLIGHT9结构体,然后先把这个结构体用ZeroMemory置零,接着开始做填空题填充这个D3DLIGHT9结构体,因为这里是点光源,所以Light.Type这个参数需要为D3DLIGHT_POINT,表示点光源。设置完之后,接着用SetLight设置光源,用LightEnable启用光照。
2.方向光源
方向光源是从无穷远处发出的一组平行、均匀的光线,在场景中以相同的方向传播,只具有颜色和方向,不受到衰减和范围的影响。同样地,要定义一个方向光源的话,实例化一个D3DLIGHT9结构体,将第一个参数设为D3DLIGHT_DIRECTIONAL然后进行其余参数的设置即可,这样实例化出的这个结构体就是一个方向光源了。下面我们看一个方向光源设置的实例:
D3DLIGHT9 light; ::ZeroMemory(&light,sizeof(light)); light.Type = D3DLIGHT_DIRECTIONAL;//方向光源 light.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); light.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f); pd3dDevice->SetLight(0,&light); //设置光源 pd3dDevice->LightEnable(0,true); //启用光照
3.聚光灯光源
这里的聚光灯光源,就是我们在演唱会时看到的那样的聚光灯,也如我们生活中的探照灯。聚光灯发出的光由一个明亮的内椎体(inner cone)和大一点的外椎体(outer cone)组成。显然内锥体中的光是最亮的,内锥体到外椎体外围的光强逐渐衰减,到了外椎体以外,已经衰减得没有光了。
上面我们讲到,光线强度从内锥体到外锥体逐渐衰减,是通过聚光灯的Falloff、Theta、和Phi这三个属性共同来控制其衰减规律。下图显示了这Phi和Theta参数之间的关系和他们如何影响着一个聚光灯的内外锥体的:
而Falloff用于控制光强如何从内锥体的外侧向外锥体的内侧减弱的,通常我们将其设为1.0f,来让光线在两个圆锥间平滑地减弱。下图清晰地显示了Falloff参数是如何来取决内锥体和外锥体之间的光强变化的。
因为聚光灯受到衰减规律和光照范围的影响,场景中的每个顶点在计算光照时,都要考虑这些因素,这使得聚光灯成为在Direct3D中首屈一指的高开销光源,因此我们要谨慎使用聚光灯。
同样地,要定义一个聚光灯光源的话,实例化一个D3DLIGHT9结构体,将第一个参数设为D3DLIGHT_SPOT然后进行其余参数的设置即可,这样实例化出的这个结构体就是一个聚光灯光源了。
讲完聚光灯的概念,依然是一个设置聚光灯光源的实例:
D3DLIGHT9 light; ::ZeroMemory(&light,sizeof(light)); light.Type = D3DLIGHT_SPOT;//聚光灯光源 light.Position = D3DXVECTOR3(100.0f, 100.0f, 100.0f); light.Direction = D3DXVECTOR3(-1.0f, -1.0f, -1.0f); light.Ambient = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f); light.Attenuation0 = 1.0f; light.Attenuation1 = 0.0f; light.Attenuation2 = 0.0f; light.Range = 300.0f; light.Falloff = 0.1f; light.Phi = D3DX_PI / 3.0f; light.Theta = D3DX_PI / 6.0f; pd3dDevice->SetLight(0,&light); //设置光源 pd3dDevice->LightEnable(0,true); //启用光照
读到这里,大家如果觉得累了,不妨来欣赏2012年11月30日刚刚发售的法国育碧游戏公司的当家大作《刺客信条3》的游戏美图,这款刚发行的游戏大作采用了全新的游戏引擎AnvilNext,显示效果非常细腻,同屏人数达到了惊人的2000人:
四、材质
对于光照计算,光照和材质两者缺一不可。物体表面的材质属性决定了它能反射什么颜色的光线以及能反射多少,而在Direct3D中,关于物体表面的材质属性,由一个结构体D3DMATERIAL9来负责管理。
我们可以在MSDN中查到这个结构体有如下原型:
typedef struct D3DMATERIAL9 { D3DCOLORVALUEDiffuse; D3DCOLORVALUEAmbient; D3DCOLORVALUESpecular; D3DCOLORVALUEEmissive; float Power; } D3DMATERIAL9, *LPD3DMATERIAL9;
■ 第一个参数,D3DCOLORVALUE类型的Diffuse,表示物体表面对漫反射光的反射率,我们可以发现这个参数类型为D3DCOLORVALUE,也就是Direct3D中的颜色类型。我们可以用像D3DXCOLOR(A ,R, G, B)这样的句式来表示某种颜色,其中A ,R, G, B分别表示0.0f到1.0f之间的红色,绿色,蓝色,透明色的分量值,比如这样写:D3DXCOLOR(0.5f, 0.5f, 0.7f, 1.0f)。
■ 第二个参数,D3DCOLORVALUE类型的Ambient,表示物体表面对环境光的反射率,填一个颜色值。
■ 第三个参数,D3DCOLORVALUE类型的Specular,表示物体表面对镜面反射光的反射率,同样是填一个颜色值
■ 第四个参数,D3DCOLORVALUE类型的Emissive,表示物体的自发光颜色值,同样是填一个颜色值。
■ 第五个参数,float类型的Power,表示镜面反射指数,他的值越大,高光强度和周围亮度相差就越大。
我们还需要知道,物体的顶点颜色的亮度总和有一个公式:
其中等式左边的I total表示物体最终的颜色值,通过这个式子我们可以知道,物体的颜色总和=物体反射环境光+物体反射漫反射光+物体反射镜面反射光+自发光。也就是说,物体的最终颜色值由D3DMATERIAL9结构体中设置的四种颜色值共同决定。
在做完填空题,设置好我们的材质属性后,就需要调用一个SetMaterial方法来设置我们当前使用的材质属性,我们可以在MSDN中查到这个参数有如下原型:
HRESULT SetMaterial( [in] const D3DMATERIAL9 *pMaterial );
这个方法唯一的一个参数就是指向D3DMATERIAL9结构体的指针,我们在使用的时候要记得在前面加上一个“&”取地址符号。
讲解完相关概念,下面我们依然是看一个设置材质的实例:
// 设置材质 D3DMATERIAL9 mtrl; ::ZeroMemory(&mtrl,sizeof(mtrl)); mtrl.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.7f, 1.0f); mtrl.Diffuse = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f); mtrl.Specular =D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f); mtrl.Emissive =D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f); g_pd3dDevice->SetMaterial(&mtrl);
另外需要注意,我们如果没有在程序中用代码来指定材质属性的话,Direct3D有自己的一套默认材质,默认材质反射所有的漫反射光,但没有环境反射光和镜面反射光,也没有自发光颜色。我们可以理解为Direct3D在内部为我们默认写了如下代码:
D3DMATERIAL9 mtrl; ::ZeroMemory(&mtrl,sizeof(mtrl)); mtrl.Ambient = D3DXCOLOR(0.0, 0.0f, 0.0f, 0.0f); mtrl.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f); mtrl.Specular =D3DXCOLOR(0.0f, 0.0f, 0.3f, 0.0f); mtrl.Emissive =D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f); mtrl.Power = 0.0f; g_pd3dDevice->SetMaterial(&mtrl);
另外再提一个与
SetMaterial
非常相似的GetMaterial方法,用于获取Direct3D使用的当前材质,原型如下:
HRESULT GetMaterial( [out] D3DMATERIAL9 *pMaterial );
五、关于顶点法线
在使用光照所绘制的3D场景中,计算物体顶点的颜色值除了需要光源和物体的材质信息外,还需要知道每个顶点的法向量,以便于根据光线的入射方向与法向量的夹角计算发射光线的最终颜色值。
在这里提出了顶点法线和面法线这两个概念,我们首先需要清楚的理解并区分这两个概念。
面法线很容易理解,即垂直于三角形面的一条法线。那顶点法线又从何而来呢,严格的从法线的定义上来说,其实顶点是不存在法线的,那为何又有顶点法线这个概念的呢?让顶点也拥有法线,是为了在光照计算时,能够知晓光线到达表面时的入射角,以在多面体的表面获得一种平滑的效果。
面法线:
顶点法线:
在一般情况下,顶点法线与面法线的方向是相同的。但是在某些特殊的情况下,顶点法线并不与面法线相同。比如一个近似的球体或圆的顶点法线和面法线就不一致:
顶点法线可以在定义的顶点结构中进行描述。我们需要在前面已经拥有的顶点结构体中添加一组用于描述顶点法向量的数据成员。当然修改了顶点结构体,对应的FVF灵活顶点格式的宏需要和结构体对应,也就是添加一句D3DFVF_NORMAL。
下面我们拿之前笔记三十八里关于顶点格式设计的代码来做演示,首先上笔记三十八里面的原版代码:
//***************************************************************************************** // 【顶点缓存、索引缓存绘图四步曲之一】:设计顶点格式 //***************************************************************************************** structCUSTOMVERTEX { FLOAT x, y, z; DWORD color; }; #defineD3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE) //FVF灵活顶点格式
然后与之对应的是今天我们学的添加顶点法线坐标后的代码:
//***************************************************************************************** // 【顶点缓存、索引缓存绘图四步曲之一】:设计顶点格式 //***************************************************************************************** structCUSTOMVERTEX { FLOAT x, y, z; FLOAT nx,ny,nz; DWORD color; }; #defineD3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_DIFFUSE) //FVF灵活顶点格式
既然前面在设计顶点格式的时候我们添加了顶点法线这个顶点属性,在后面的【顶点缓存、索引缓存绘图四步曲之三】:访问顶点缓存和索引缓存这一步里面除了顶点坐标和顶点颜色外,当然也需要填写顶点法向量的坐标了。
对于简单的物体而言,比如立方体和球体,我们完全可以通过观察来得到这些顶点的法向量。然而,对于不规则或者复杂的物体,则需要另寻高招。
对于复杂的物体,我们可以认为每个顶点的法向量与该顶点构成的三角形面的法向量相同。
假设一个三角形由顶点p0,p1,p2这三个顶点构成的,现在我们要计算每个顶点的法向量的话,就是求这个三角形面的法向量罢了。
求法如图:
也就是首先计算位于三角形平面内代表两条边的向量,然后对这两条向量做叉乘运算就可以了。
当然,当我们使用一组三角形渐进来表示曲面时,使用上述方法计算出的顶点法向量将会产生不光滑的效果。因此,另一种计算顶点法向量的方式应运而生——计算法向量的均值(normal averaging):首先我们求出共享该顶点的3个三角形的面法向量,然后取他们的平均值作为该顶点的顶点法向量,如图:
也就是说np=(n1+n2+n3)/3
另外,在变换过程中,我们的顶点法线有可能不再是规范化的了。所以,最好的方法是,在变换完成之后,通过在SetRenderState方法中将D3DRS_NORMALIZENORMALS这个参数设为true来把所有的法向量规范化,也就是这样写:
pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS,true);
关于绕序,有朋友曾经请教过浅墨,浅墨在这里简明扼要地说明一下,由于DirectX用的是左手坐标系,我们用左手除了大拇指的四个手指按顺时针方向对某个三角形绕圈,大拇指指的方向就是三角形正面了。
六、总结
上面讲解了洋洋洒洒六千字,下面我们来做一下总结,关于Direct3D中的光照,知识网络也就如下这些:
光照计算两要素: 光照和材质
四大光照:环境光,镜面反射光,漫反射光,自发光
三大光源:点光源,平行光源,聚光灯。
光源属性:一个结构体D3DLIGHT9,两个方法SetLight和LightEnable
材质属性:一个结构体D3DMATERIAL9,一个方法SetMaterial,法线向量的计算。
七、详细注释的源代码欣赏
在上篇文章末尾我们提到过,写这篇文章的初衷就是为上篇文章中demo的显示更加逼真。在上篇文章中demo的基础上,我们自定义了一个Light_Set()函数,把光照相关的代码在其中进行了封装,且我们在Objects_Init()函数中先进行了材质的设置,并调用了一次Light_Set(),将默认情况下的光源设置为点光源。接着我们在Direct3D_Render()自定义函数中渲染五步曲的第三步里添加了如下代码,以实现通过键盘上按键的按下,当前使用的光源在三种光源类型之间切换:
// 重新设置光照 if (::GetAsyncKeyState(0x51) &0x8000f) // 若键盘上的按键Q被按下,光源类型设为点光源 Light_Set(g_pd3dDevice, 1); if (::GetAsyncKeyState(0x57) &0x8000f) // 若键盘上的按键W被按下,光源类型设为平行光源 Light_Set(g_pd3dDevice, 2); if (::GetAsyncKeyState(0x45) &0x8000f) // 若键盘上的按键E被按下,光源类型设为聚光灯 Light_Set(g_pd3dDevice, 3);
程序的大体思路讲解完成,下面我们贴出详细注释的源代码:
//***************************************************************************************** // //【Visual C++】游戏开发笔记系列配套源码 四十 浅墨DirectX提高班之八 绘制真实质感的三维世界:光照与材质专场 // VS2010版 // 2013年 1月13日 Create by 浅墨 //图标素材: 质量效应3 MassEffect 3 //此刻心情:如果你看到了前面的黑暗,不要担心,那是因为你的背后有阳光。 // //***************************************************************************************** //***************************************************************************************** // Desc: 头文件定义部分 //***************************************************************************************** #include
#include #include #include //***************************************************************************************** // Desc: 库文件定义部分 //***************************************************************************************** #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "winmm.lib ") //***************************************************************************************** // Desc: 宏定义部分 //***************************************************************************************** #define SCREEN_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define SCREEN_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 #define WINDOW_TITLE _T("【Visual C++游戏开发笔记】博文配套demo之四十 浅墨DirectX提高班之八 绘制真实质感的三维世界:光照与材质专场") //为窗口标题定义的宏 #define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } } //自定义一个SAFE_RELEASE()宏,便于资源的释放 //***************************************************************************************** // Desc: 全局变量声明部分 //***************************************************************************************** LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; //Direct3D设备对象 ID3DXFont* g_pFont=NULL; //字体COM接口 float g_FPS = 0.0f; //一个浮点型的变量,代表帧速率 wchar_t g_strFPS[50]; //包含帧速率的字符数组 LPDIRECT3DVERTEXBUFFER9 g_pVertexBuffer = NULL; //顶点缓存对象 LPDIRECT3DINDEXBUFFER9 g_pIndexBuffer = NULL; // 索引缓存对象 LPD3DXMESH g_teapot = NULL; //茶壶对象 LPD3DXMESH g_cube = NULL; //立方体(盒子)对象 LPD3DXMESH g_sphere = NULL; //球面体对象 LPD3DXMESH g_torus = NULL; //圆环对象 D3DXMATRIX g_WorldMatrix[4],R; //定义一些全局的世界矩阵 //***************************************************************************************** // Desc: 全局函数声明部分 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT Direct3D_Init(HWND hwnd); HRESULT Objects_Init(); void Direct3D_Render( HWND hwnd); void Direct3D_CleanUp( ); float Get_FPS(); void Matrix_Set(); void Light_Set(LPDIRECT3DDEVICE9 pd3dDevice, UINT nType); //***************************************************************************************** // Name: WinMain( ) // Desc: Windows应用程序入口函数 //***************************************************************************************** int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) { //开始设计一个完整的窗口类 WNDCLASSEX wndClass = { 0 }; //用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINDCLASSEX,用于之后窗口的各项初始化 wndClass.cbSize = sizeof( WNDCLASSEX ) ; //设置结构体的字节数大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //设置窗口的样式 wndClass.lpfnWndProc = WndProc; //设置指向窗口过程函数的指针 wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //指定包含窗口过程的程序的实例句柄。 wndClass.hIcon=(HICON)::LoadImage(NULL,_T("icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //从全局的::LoadImage函数从本地加载自定义ico图标 wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口类的光标句柄。 wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrBackground成员指定一个灰色画刷句柄 wndClass.lpszMenuName = NULL; //用一个以空终止的字符串,指定菜单资源的名字。 wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop"); //用一个以空终止的字符串,指定窗口类的名字。 if( !RegisterClassEx( &wndClass ) ) //设计完窗口后,需要对窗口类进行注册,这样才能创建该类型的窗口 return -1; HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE, //喜闻乐见的创建窗口函数CreateWindow WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, SCREEN_WIDTH, SCREEN_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D资源的初始化,调用失败用messagebox予以显示 if (!(S_OK==Direct3D_Init (hwnd))) { MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口 } MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true); //调整窗口显示时的位置,窗口左上角位于屏幕坐标(200,50)处 ShowWindow( hwnd, nShowCmd ); //调用Win32函数ShowWindow来显示窗口 UpdateWindow(hwnd); //对窗口进行更新,就像我们买了新房子要装修一样 //消息循环过程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循环 { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。 { TranslateMessage( &msg ); //将虚拟键消息转换为字符消息 DispatchMessage( &msg ); //该函数分发一个消息给窗口程序。 } else { Direct3D_Render(hwnd); //调用渲染函数,进行画面的渲染 } } UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); return 0; } //***************************************************************************************** // Name: WndProc() // Desc: 对窗口消息进行处理 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口过程函数WndProc { switch( message ) //switch语句开始 { case WM_PAINT: // 客户区重绘消息 Direct3D_Render(hwnd); //调用Direct3D_Render函数,进行画面的绘制 ValidateRect(hwnd, NULL); // 更新客户区的显示 break; //跳出该switch语句 case WM_KEYDOWN: // 键盘按下消息 if (wParam == VK_ESCAPE) // ESC键 DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM_DESTROY消息 break; case WM_DESTROY: //窗口销毁消息 Direct3D_CleanUp(); //调用Direct3D_CleanUp函数,清理COM接口对象 PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM_DESTROY消息 break; //跳出该switch语句 default: //若上述case条件都不符合,则执行该default语句 return DefWindowProc( hwnd, message, wParam, lParam ); //调用缺省的窗口过程来为应用程序没有处理的窗口消息提供缺省的处理。 } return 0; //正常退出 } //***************************************************************************************** // Name: Direct3D_Init( ) // Desc: 初始化Direct3D // Point:【Direct3D初始化四步曲】 // 1.初始化四步曲之一,创建Direct3D接口对象 // 2.初始化四步曲之二,获取硬件设备信息 // 3.初始化四步曲之三,填充结构体 // 4.初始化四步曲之四,创建Direct3D设备接口 //***************************************************************************************** HRESULT Direct3D_Init(HWND hwnd) { //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之一,创接口】:创建Direct3D接口对象, 以便用该Direct3D对象创建Direct3D设备对象 //-------------------------------------------------------------------------------------- LPDIRECT3D9 pD3D = NULL; //Direct3D接口对象的创建 if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口对象,并进行DirectX版本协商 return E_FAIL; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之二,取信息】:获取硬件设备信息 //-------------------------------------------------------------------------------------- D3DCAPS9 caps; int vp = 0; if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) { return E_FAIL; } if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) vp = D3DCREATE_HARDWARE_VERTEXPROCESSING; //支持硬件顶点运算,我们就采用硬件顶点运算,妥妥的 else vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件顶点运算,无奈只好采用软件顶点运算 //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之三,填内容】:填充D3DPRESENT_PARAMETERS结构体 //-------------------------------------------------------------------------------------- D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; d3dpp.BackBufferCount = 1; d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; d3dpp.MultiSampleQuality = 0; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hwnd; d3dpp.Windowed = true; d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; d3dpp.Flags = 0; d3dpp.FullScreen_RefreshRateInHz = 0; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之四,创设备】:创建Direct3D设备接口 //-------------------------------------------------------------------------------------- if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice))) return E_FAIL; if(!(S_OK==Objects_Init())) return E_FAIL; SAFE_RELEASE(pD3D) //LPDIRECT3D9接口对象的使命完成,我们将其释放掉 return S_OK; } HRESULT Objects_Init() { //创建字体 if(FAILED(D3DXCreateFont(g_pd3dDevice, 30, 0, 0, 1, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("宋体"), &g_pFont))) return E_FAIL; // 物体的创建 if(FAILED(D3DXCreateBox(g_pd3dDevice, 2, 2, 2, &g_cube, NULL))) //立方体的创建 return false; if(FAILED(D3DXCreateTeapot(g_pd3dDevice, &g_teapot, NULL))) //茶壶的创建 return false; if(FAILED(D3DXCreateSphere(g_pd3dDevice, 1.5, 25, 25, //球面体的创建 &g_sphere, NULL))) return false; if(FAILED(D3DXCreateTorus(g_pd3dDevice, 0.5f, 1.2f, 25, 25, //圆环体的创建 &g_torus, NULL))) return false; // 设置材质 D3DMATERIAL9 mtrl; ::ZeroMemory(&mtrl, sizeof(mtrl)); mtrl.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.7f, 1.0f); mtrl.Diffuse = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f); mtrl.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f); mtrl.Emissive = D3DXCOLOR(0.3f, 0.0f, 0.1f, 1.0f); g_pd3dDevice->SetMaterial(&mtrl); // 设置光照 Light_Set(g_pd3dDevice, 1); g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, true); g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true); g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true); g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); //开启背面消隐 return S_OK; } //***************************************************************************************** // Name:Matrix_Set() // Desc: 设置世界矩阵 // Point:【Direct3D四大变换】 // 1.【四大变换之一】:世界变换矩阵的设置 // 2.【四大变换之二】:取景变换矩阵的设置 // 3.【四大变换之三】:投影变换矩阵的设置 // 4.【四大变换之四】:视口变换的设置 //***************************************************************************************** VOID Matrix_Set() { //-------------------------------------------------------------------------------------- //【四大变换之一】:世界变换矩阵的设置 //-------------------------------------------------------------------------------------- //-------------------------------------------------------------------------------------- //【四大变换之二】:取景变换矩阵的设置 //-------------------------------------------------------------------------------------- D3DXMATRIX matView; //定义一个矩阵 D3DXVECTOR3 vEye(0.0f, 0.0f, -15.0f); //摄像机的位置 D3DXVECTOR3 vAt(0.0f, 0.0f, 0.0f); //观察点的位置 D3DXVECTOR3 vUp(0.0f, 1.0f, 0.0f);//向上的向量 D3DXMatrixLookAtLH(&matView, &vEye, &vAt, &vUp); //计算出取景变换矩阵 g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView); //应用取景变换矩阵 //-------------------------------------------------------------------------------------- //【四大变换之三】:投影变换矩阵的设置 //-------------------------------------------------------------------------------------- D3DXMATRIX matProj; //定义一个矩阵 D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4.0f, 1.0f, 1.0f, 1000.0f); //计算投影变换矩阵 g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj); //设置投影变换矩阵 //-------------------------------------------------------------------------------------- //【四大变换之四】:视口变换的设置 //-------------------------------------------------------------------------------------- D3DVIEWPORT9 vp; //实例化一个D3DVIEWPORT9结构体,然后做填空题给各个参数赋值就可以了 vp.X = 0; //表示视口相对于窗口的X坐标 vp.Y = 0; //视口相对对窗口的Y坐标 vp.Width = SCREEN_WIDTH; //视口的宽度 vp.Height = SCREEN_HEIGHT; //视口的高度 vp.MinZ = 0.0f; //视口在深度缓存中的最小深度值 vp.MaxZ = 1.0f; //视口在深度缓存中的最大深度值 g_pd3dDevice->SetViewport(&vp); //视口的设置 } //-------------------------------------------------------------------------------------- // Name: Light_Set() // Desc: 设置光源类型 //-------------------------------------------------------------------------------------- VOID Light_Set(LPDIRECT3DDEVICE9 pd3dDevice, UINT nType) { static D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); switch (nType) { case 1: //点光源 light.Type = D3DLIGHT_POINT; light.Ambient = D3DXCOLOR(0.6f, 0.6f, 0.6f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); light.Position = D3DXVECTOR3(0.0f, 200.0f, 0.0f); light.Attenuation0 = 1.0f; light.Attenuation1 = 0.0f; light.Attenuation2 = 0.0f; light.Range = 300.0f; break; case 2: //平行光 light.Type = D3DLIGHT_DIRECTIONAL; light.Ambient = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); light.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f); break; case 3: //聚光灯 light.Type = D3DLIGHT_SPOT; light.Position = D3DXVECTOR3(100.0f, 100.0f, 100.0f); light.Direction = D3DXVECTOR3(-1.0f, -1.0f, -1.0f); light.Ambient = D3DXCOLOR(0.3f, 0.3f, 0.3f, 1.0f); light.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular = D3DXCOLOR(0.3f, 0.3f, 0.3f, 0.3f); light.Attenuation0 = 1.0f; light.Attenuation1 = 0.0f; light.Attenuation2 = 0.0f; light.Range = 300.0f; light.Falloff = 0.1f; light.Phi = D3DX_PI / 3.0f; light.Theta = D3DX_PI / 6.0f; break; } pd3dDevice->SetLight(0, &light); //设置光源 pd3dDevice->LightEnable(0, true);//启用光照 pd3dDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(36, 36, 36)); //设置一下环境光 } //***************************************************************************************** // Name: Direct3D_Render() // Desc: 进行图形的渲染操作 // Point:【Direct3D渲染五步曲】 // 1.渲染五步曲之一,清屏操作 // 2.渲染五步曲之二,开始绘制 // 3.渲染五步曲之三,正式绘制 // 4.渲染五步曲之四,结束绘制 // 5.渲染五步曲之五,翻转显示 //***************************************************************************************** void Direct3D_Render(HWND hwnd) { //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之一】:清屏操作 //-------------------------------------------------------------------------------------- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0); //定义一个矩形,用于获取主窗口矩形 RECT formatRect; GetClientRect(hwnd, &formatRect); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之二】:开始绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->BeginScene(); // 开始绘制 Matrix_Set(); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之三】:正式绘制,利用顶点缓存绘制图形 //-------------------------------------------------------------------------------------- //-------------------------------------------------------------------------------------- // 【顶点缓存、索引缓存绘图四步曲之四】:绘制图形 //-------------------------------------------------------------------------------------- // 获取键盘消息并给予设置相应的填充模式 if (::GetAsyncKeyState(0x31) & 0x8000f) // 若数字键1被按下,进行线框填充 g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME); if (::GetAsyncKeyState(0x32) & 0x8000f) // 若数字键2被按下,进行实体填充 g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID); // 重新设置光照 if (::GetAsyncKeyState(0x51) & 0x8000f) // 若键盘上的按键Q被按下,光源类型设为点光源 Light_Set(g_pd3dDevice, 1); if (::GetAsyncKeyState(0x57) & 0x8000f) // 若键盘上的按键W被按下,光源类型设为平行光源 Light_Set(g_pd3dDevice, 2); if (::GetAsyncKeyState(0x45) & 0x8000f) // 若键盘上的按键E被按下,光源类型设为聚光灯 Light_Set(g_pd3dDevice, 3); D3DXMatrixRotationY(&R, ::timeGetTime() / 720.0f); // 进行立方体的绘制 D3DXMatrixTranslation(&g_WorldMatrix[0], 3.0f, -3.0f, 0.0f); g_WorldMatrix[0] = g_WorldMatrix[0]*R; g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[0]); g_cube->DrawSubset(0); //进行茶壶的绘制 D3DXMatrixTranslation(&g_WorldMatrix[1], -3.0f, -3.0f, 0.0f); g_WorldMatrix[1] = g_WorldMatrix[1]*R; g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[1]); g_teapot->DrawSubset(0); // 进行圆环的绘制 D3DXMatrixTranslation(&g_WorldMatrix[2], 3.0f, 3.0f, 0.0f); g_WorldMatrix[2] = g_WorldMatrix[2]*R; g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[2]); g_torus->DrawSubset(0); // 进行球面体的绘制 D3DXMatrixTranslation(&g_WorldMatrix[3], -3.0f, 3.0f, 0.0f); g_WorldMatrix[3] = g_WorldMatrix[3]*R; g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_WorldMatrix[3]); g_sphere->DrawSubset(0); //在窗口右上角处,显示每秒帧数 int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pFont->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_XRGB(255,239,136)); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之四】:结束绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->EndScene(); // 结束绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之五】:显示翻转 //-------------------------------------------------------------------------------------- g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻转与显示 } //***************************************************************************************** // Name:Get_FPS()函数 // Desc: 用于计算帧速率 //***************************************************************************************** float Get_FPS() { //定义四个静态变量 static float fps = 0; //我们需要计算的FPS值 static int frameCount = 0;//帧数 static float currentTime =0.0f;//当前时间 static float lastTime = 0.0f;//持续时间 frameCount++;//每调用一次Get_FPS()函数,帧数自增1 currentTime = timeGetTime()*0.001f;//获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间 //如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零 if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟 { fps = (float)frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值 lastTime = currentTime; //将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间 frameCount = 0;//将本次帧数frameCount值清零 } return fps; } //***************************************************************************************** // Name: Direct3D_CleanUp() // Desc: 对Direct3D的资源进行清理,释放COM接口对象 //***************************************************************************************** void Direct3D_CleanUp() { //释放COM接口对象 SAFE_RELEASE(g_torus) SAFE_RELEASE(g_sphere) SAFE_RELEASE(g_cube) SAFE_RELEASE(g_teapot) SAFE_RELEASE(g_pFont) SAFE_RELEASE(g_pd3dDevice) }
编译并运行上面的代码。我们可以得到如下截图中所示的显示效果。因为我们按键盘上的“1”和“2”在线框模式和实体填充模式之间切换,而按下键盘上的“Q”“W”“E”在点光源,方向光源和聚光灯光源之间切换。所以在这里浅墨截了六张图片,分别代表了两种填充模式和三种光源能得到的六种排列组合的显示效果:
第一张,实体填充模式+点光源:
第二张,线框填充模式+点光源:
第三张,实体填充模式+方向光源:
第四张,线框填充模式+方向光源:
第五张,实体填充模式+聚光灯光源:
第六张,线框填充模式+聚光灯光源:
文章最后,依旧是放出本篇文章配套源代码的下载:
本节笔记配套源代码请点击这里下载:
【浅墨DirectX提高班】配套源代码之八下载
其中图标素材取自的游戏大作 质量效应3
以上就是本节笔记的全部内容,更多精彩内容,且听下回分解。
浅墨在这里,希望喜欢游戏开发系列文章的朋友们能留下你们的评论,每次浅墨登陆博客看到大家的留言的时候都会非常开心,感觉自己正在传递一种信仰,一种精神。
文章最后,依然是【每文一语】栏目,今天的句子是:
执著和放弃,选择哪个并不重要;重要的是,一旦作出决定,给自己一个把这个决定坚持下去的理由。否则,当你仰望苍穹的时候,你的眼神也许和那些茫然战死的沙漠佣兵,没什么区别。我们都是怀揣梦想、穿越沙漠的流浪者。电脑屏幕前的你,早安~
下周一,让我们离游戏开发的梦想更近一步。
下周一,游戏开发笔记,我们,不见不散。