本文由哈利_蜘蛛侠原创,转载请注明出处!有问题请联系[email protected]
这一次我们继续来讲述Jim Adams老哥的RPG编程书籍第二版第二章的第7节:Lighting,也就是光照。这一节的内容有点多,不过还是争取一次性讲完吧!
我们先将这一节的各小节的标题列在下面,以供大家参考:
1、 Using Point Lights (使用点光源)
2、 UsingSpotlights (使用聚光灯)
3、 Using Directional Lights (使用方向光)
4、 Ambient Light (环境光)
5、 Setting the Light (设置光照)
6、 Using Normals (使用法向量)
7、 Let There Be Light! (要有光!)
注意:原书并没有单独的这一节,而是我从Alpha Blending这一节后半部分抽取出来的——这样显得更加合理。
原文翻译:
===============================================================================
高级图形技术列表中的下一项是光照的使用。与在现实生活中不同,大多数游戏完全照亮整个场景,这虽然让图形看上去很清晰(look sharp),但是却不真实。为了得到一个更加逼真的场景,为了给你的图形增加游戏玩家会go ga-ga over(不知道怎么个翻译)的微妙的光照效果,你需要使用Direct3D的光照功能。你在Direct3D中可以使用四种类型的光:环境光、点光源、聚光灯以及方向光。
环境光(ambient light)是一个不变的光源,它用同样的光强照亮场景中的所有物体。因为它是设备组件的一部分,环境光是唯一一种不受光照引擎操控的光照成分。
其他三种光(如图2.18所示)具有独特的性质。点光源(pointlight)将它周围的所有物体照亮(就像电灯泡那样子)。聚光灯(spotlight)位于一个特定的方向(实际上,应该是位置)并发出一个圆锥形光。位于锥体内部的物体会被照亮,而位于锥体外部的则不会被照亮。方向光(directional light)(简化的聚光灯)只朝着一个方向投射光线。
(实际上,上面的说法有一点不科学:聚光灯与方向光并不是特殊与一般的关系;聚光灯有一个光源,而方向光没有光源。)
光就像其他的3-D物体那样被放置在场景中——通过使用x-, y-和z-坐标。有一些光,例如聚光灯,还有一个方向向量来决定它们指向哪里。每一个光具有一个强度值、范围、衰减因子以及颜色。对的,甚至有色光源在Direct3D中都是可能的!
除了环境光以外,每种光使用一个D3DLIGHT9数据结构来储存其特有的信息。这个结构体定义如下:
typedef struct_D3DLIGHT9 { D3DLIGHTTYPE Type; // Type of light D3DCOLORVALUE Diffuse; // Diffuse color D3DCOLORVALUE Specular; // Specular color D3DCOLORVALUE Ambient; // Ambient color D3DVECTOR Position; //Position of light D3DVECTOR Direction; //Direction light is pointing float Range; //Range of light float Falloff; //Falloff of spotlight float Attenuation0; // Light attenuation 0 float Attenuation1; // Light attenuation 1 float Attenuation2; // Light attenuation 2 float Theta; // Angle of inner cone float Phi; // Angle of outer cone } D3DLIGHT9;
哇哦!这是一个big puppy(puppy是小狗的意思;这句话不知道怎么翻译。),但是它包含了你需要用来描述一个光的所有信息。虽然光并不一定要到D3DLIGHT9中的所有变量,但是所有的光都共享一些成员。
你设置的第一个变量是Type,这是你要使用的光源的类型。这可以是D3DLIGHT_POINT(对于点光源)、D3DLIGHT_SPOT(对于聚光灯)或者D3DLIGHT_DIRECTIONAL(对于方向光)。
下一行是光的颜色。你会最常使用Diffuse成员;它决定了光发出来的颜色。注意这些颜色成员是以D3DCOLORVALUE 的形式出现的,它是一个看上去如下的结构体:
typedef struct _D3DCOLORVALUE { float r; // Red value (0.0 to 1.0) float g; // Green value (0.0 to 1.0) float b; // Blue value (0.0 to 1.0) float a; // Alpha value (Unused) } D3DCOLORVALUE;
你可将结构体中的颜色成分设为0.0(表示关闭)到1.0(表示满值)之间的范围。红光是r=1.0, g=0.0, b=0.0,而白光是r=1.0, g=1.0, b=1.0。因为你在处理光,所以这里不会用到alpha值。D3DLIGHT9结构体中的Specular和Ambient光成员分别决定了高光颜色和环境光颜色。你可以安全地将两个成员的各个颜色成分设为1.0(除了Specular,如果你不想使用高光的话,那么你可以将其每个颜色成分都设为0.0)。
建议
===============================================================================
除了使用光的颜色等级来照亮一个物体以外,你还可以用之来弄暗一个物体。不要使用正的颜色值,而是使用负的颜色值,然后观察结果吧!
===============================================================================
正如我前面提到的那样,每一个光都可以通过使用xyz-坐标(世界坐标)而被放置在3-D场景中,而这个坐标是储存在向量Position中。Direction也是一个向量,但是是用来让光指向特定方向的。你可以在“Using Spotlights”这一节中发现更多关于使用方向向量的内容。
Range变量决定光在飞行多远之后完全衰减(而falloff是用于决定光线从内锥体到外锥体衰减的速度的值。一般来说,将falloff值设为1.0可以产生很平滑的转换。)在此距离之外的物体不会被此光照亮。
三个衰减成员决定了光是如何随着距离进行衰减的——这三个衰减成员一般都设为0(其实,不能够把它们都设为0的。)。而你是否要使用其余的变量取决于你正在使用的光源类型。
点光源是最容易使用的光源;你只需要设定它们的位置、颜色成分以及范围。为了建立一个点光源,实例化一个D3DLIGHT9 结构体并用需要的信息对其进行填充:(我觉得下面的代码中还需要加上一句:PointLight.Type = D3DLIGHT_POINT;)
D3DLIGHT9 PointLight; // Clear out the light data ZeroMemory(&PointLight, sizeof(D3DLIGHT9)); // Position the light at 0.0, 100.0, 200.0 PointLight.Position = D3DVECTOR3(0.0f, 100.0f, 200.0f); // Set the diffuse and ambient colors to white PointLight.Diffuse.r = PointLight.Ambient.r = 1.0f; PointLight.Diffuse.g = PointLight.Ambient.g = 1.0f; PointLight.Diffuse.b = PointLight.Ambient.b = 1.0f; // Set the range to 1000 units PointLight.Range = 1000.0f;
聚光灯运作起来与其他的光有点不一样,因为聚光灯从光源出发、在一个锥体内投射光线。在中心处光是最亮的,而当它靠近锥体的外部时会逐渐变暗。锥体外部的物体不会被照亮。
当心
===============================================================================
聚光灯是你能够使用的最耗费计算量的光源,所以在场景中不要使用太多的聚光灯为好。
===============================================================================
你通过在D3DLIGHT9 结构体中设定位置、方向、颜色成分、范围、衰减值、衰减因子、内锥体半径和外锥体半径来定义一个聚光灯。你不需要担心衰减值和衰减因子,但是你确实需要考虑内外锥体的半径。
D3DLIGHT9结构体中的Phi变量确定了外锥体的大小。Phi,以及Theta,用(以弧度为单位的)角度来表示。光传播得离聚光灯光源越远,那么投影的半径就越大。程序员们决定要使用什么样的值,而你只需多尝试几个值,直到你找到你喜欢的为止。
下面的代码建立了一个聚光灯,它设立了位置、颜色、范围、衰减值以及内外锥体半径:(同样,我觉得下面的代码中还需要加上一句:PointLight.Type = D3DLIGHT_SPOT;)
D3DLIGHT9 Spotlight; // Clear out the light data ZeroMemory(&SpotLight, sizeof(D3DLIGHT9)); // Position the light at 0.0, 100.0, 200.0 SpotLight.Position = D3DVECTOR3(0.0f, 100.0f, 200.0f); // Set the diffuse and ambient colors to white SpotLight.Diffuse.r = SpotLight.Ambient.r = 1.0f; SpotLight.Diffuse.g = SpotLight.Ambient.g = 1.0f; SpotLight.Diffuse.b = SpotLight.Ambient.b = 1.0f; // Set the range SpotLight.Range = 1000.0f; // Set the falloff SpotLight.Falloff = 1.0f; // Set the cone radiuses Spotlight.Phi = 0.3488; // outer 20 degrees Spotlight.Theta = 0.1744; // inner 10 degrees
现在,你要让聚光灯朝着一个特定的方向发光。D3DX又一次出手相助,用一对函数来帮助你设置聚光灯(还有任意其他光)的方向。一个函数是D3DXVECTOR3对象的重载的构造函数,它让你设定三个坐标。
对于这三个坐标,你使用世界空间的坐标来定义到原点的距离。如果你在场景的任意位置有一个聚光灯,并且你想将它往上指向一个位于灯光上方500单位的目标,那么你将向量对象的值设为X=0, Y=500, Z=0(注意到这三个坐标是相对于灯光的位置的)。例如,下面的代码设定了向量的值:
D3DXVECTOR3 Direction= D3DXVECTOR3(0.0f, 500.0f, 0.0f);
前面的Direction向量声明的唯一问题是Direct3D喜欢这些向量被归一化(normalized),这表示坐标需要位于0和1之间。没问题,因为第二个D3DX函数,D3DXVec3Normalize,为你处理这件事:
D3DXVECTOR3*D3DXVec3Normalize( D3DXVECTOR3 *pOut, // normalized vector CONST D3DXVECTOR3 *pV); // source vector
当你把原始向量(比如,前面的那个包含了坐标X=0,Y=500, Z=0 的向量)和指向新向量的指针传递进去后,D3DXVec3Normalize 函数将坐标转换为0到1之间的坐标。这个新的向量现在包含了你可以用于赋给D3DLIGHT9结构体的光源方向成员的方向值。
继续前面的例子,我们通过将聚光灯的方向设为向上,并将其归一化,再储存到D3DLIGHT9结构体中:
D3DXVECTOR3 Dir = D3DXVECTOR3(0.0f, 500.0f,0.0f); D3DXVec3Normalize((D3DXVECTOR3*)&Spotlight.Direction,&Dir);
就处理的角度来说,方向光是你能够使用的最快的光源类型了。它们照亮每一个面朝它们的多边形。为了准备一个方向光以供使用,你设定D3DLIGHT9结构体中的方向以及颜色成分成员。
如果你好奇为何不使用位置向量,那么答案是很符合逻辑的。将方向光想象成是跑向一个方向的无限长的一条河流。尽管河流中的物体的位置彼此不同,但是河水的流苏保持不变;起作用的是河流的方向。将光照的情形与之类比,河水代表光线,而河流的方向代表光的角度。世界中的任何物体,不管位于何方,都会接收到光线。
回忆一下前两种类型的光的技术,然后看看这个例子,它建立了一个照向场景的下方的淡黄色的光:(还是一样,我觉得下面的代码中还需要加上一句:PointLight.Type = D3DLIGHT_DIRECTIONAL;)
D3DLIGHT9 DirLight; // Clear out the light data ZeroMemory(&DirLight, sizeof(D3DLIGHT9)); // Set the diffuse and ambient colors to yellow DirLight.Diffuse.r = DirLight.Ambient.r = 1.0f; DirLight.Diffuse.g = DirLight.Ambient.g = 1.0f; DirLight.Diffuse.b = DirLight.Ambient.b = 0.0f; D3DXVECTOR3 Dir = D3DXVECTOR3(0.0f, 500.0f, 0.0f); D3DXVec3Normalize((D3DXVECTOR3*)&Dirlight.Direction, &Dir);
当心
===============================================================================
一个光的方向向量必须包含至少一个非零的值。也就是说,你不能够设定一个方向为X=0, Y=0, Z=0。
===============================================================================
环境光是唯一一种Direct3D处理方式不一样的光源类型。Direct3D将环境光运用于所有的多边形,不管它们的角度或者光源类型,所以不会产生明暗变化。环境光是不变的光,并且就像其他类型的光(点光源、聚光灯和方向光)一样,你可以随意设定其颜色。
你通过设定D3DRS_AMBIENT 渲染状态并传递进一个你想要使用的D3DCOLOR(使用D3DCOLOR_COLORVALUE 宏来设定要使用的位于0.0到1.0之间的红、绿和蓝色的强度)值来设定环境光:
g_pD3DDevice->SetRenderState(D3DRS_AMBIENT, \ D3DCOLOR_COLORVALUE(0.0f, Red, Green, Blue));
在你建立了D3DLIGHT9结构体之后,你使用IDirect3DDevice9::SetLight 函数来将其传递进Direct3D中:
HRESULT IDirect3DDevice9::SetLight( DWORD Index, // Index of light to set CONST D3DLIGHT9 *pLight); // D3DLIGHT9 structure to use
你知道pLight传递的是D3DLIGHT9结构体,但是Index成员却是另外的东西。Direct3D允许你在一个场景中设置多个光照,所以Index是一个以0开始的、代表你想要设定的光的指标。例如,如果你想要在一个场景中使用4个光照,那么index 0就是第一个光,index 1就是第二个,index 2是第三个,而index 4就是第四个也就是最后一个光照。
看上去Direct3D似乎对于你在一个场景中可以使用的光照的数量没有限制,但是我建议保持光照数在4个或以下。(实际上,是有数量限制的,这个是与显卡相关的。)你在场景中间增加的每一个光照都会增加渲染的复杂性以及所需的时间。
为了让Direct3D根据你提供的光源正确地照亮多边形面,你必须首先给多边形中的每个顶点提供一个法向量。法向量(normal)是一个3-D向量,它定义了与一个向量相连的一个物体面朝的方向。你一般在一个确定物体从任意给定的光照中获得多少亮度的复杂的计算中使用法向量。
如果你近距离观察一个多边形面(比如图2.19中的那个),你会看到这三个顶点有一个方向,这正是法向量。当光线击中这些顶点时,它会基于这些法向量以一个角度反射出去。使用法向量保证所有的多边形面被正确地照亮,也就是说它们会根据它们相对于观察者和光线的角度而被着色。
向你的自定义顶点信息中添加法向量就像提供纹理信息一样简单。你只需要以D3DVECTOR3类型插入法向量,然后重新定义灵活顶点格式(也就是要包含D3DFVF_NORMAL),如下所示:
typedef struct { D3DVECTOR3 Position; // Vector coordinates D3DVECTOR3 Normal; // Normal D3DCOLOR Color; // Color } sVertex; #define VERTEXFMT (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_NORMAL)
你计算法向量的方法与前面“Using Spotlights”那一节中处理光照时创建方向向量的方法相同。在你开始处理3-D模型的时候,计算法向量的任务就与你无关了——因为用于生成模型的3-D建模程序一般都已经计算好法向量了。
下面的函数(借自DirectXSDK的示例)生成一个圆柱体,并给予每个顶点一个指向远离圆柱体中心方向的法向量(参见图2.20):
// g_pD3DDevice = pre-initialized 3-D device object IDirect3DVertexBuffer9 *GenerateCylinder() { IDirect3DVertexBuffer9 *pD3DVertexBuffer; sVertex *pVertex; DWORD i; FLOAT theta; // Create the vertex buffer if(SUCCEEDED(g_pD3DDevice->CreateVertexBuffer( \ 50 * 2 * sizeof(sVertex), 0, VERTEXFMT, \ D3DPOOL_MANAGED, &pD3DVertexBuffer, NULL))) { // Fill the vertex buffer with the cylinder information if(SUCCEEDED(pD3DVertexBuffer->Lock(0, 0, \ (void**)&pVertex, 0))) { for(i=0; i<50; i++) { theta = (2 * D3DX_PI * i) / (50 - 1); pVertex[2*i+0].Position = D3DXVECTOR3(sinf(theta), \ -1.0f, cosf(theta)); pVertex[2*i+0].Normal = D3DXVECTOR3(sinf(theta), \ 0.0f, cosf(theta)); pVertex[2*i+1].Position = D3DXVECTOR3(sinf(theta), \ 1.0f, cosf(theta)); pVertex[2*i+1].Normal = D3DXVECTOR3(sinf(theta), \ 0.0f, cosf(theta)); } pD3DVertexBuffer->Unlock(); // Return a pointer to new vertex buffer return pD3DVertexBuffer; } } // Return NULL on error return NULL; }
现在你已经决定了要使用什么类型的光,还建立了对应的结构体,那么是时候激活光照管道(lighting pipeline)并打开灯光了。为了激活光照管道,你将一个渲染状态D3DRS_LIGHTING设为TRUE:
// g_pD3DDevice = pre-initializing deviceobject g_pD3DDevice->SetRenderState(D3DRS_LIGHTING,TRUE);
为了关闭光照管道,使用下列代码:
// g_pD3DDevice = pre-initializing deviceobject g_pD3DDevice->SetRenderState(D3DRS_LIGHTING,FALSE);
在激活了光照管道之后,你通过使用IDirect3DDevice9::LightEnable 函数将独立的灯光进行开或闭。这是LightEnable 函数的原型:
IDirect3DDevice9::LightEnable( DWORD LightIndex, // Light index, 0 – max # lights BOOL bEnable); // TRUE to turn on, FALSE to turn off
如果你已经建立了一个LightIndex 为0的点光源,你可以用下列代码来将之开闭:
// g_pD3DDevice = pre-initializing deviceobject // Turn light on g_pD3DDevice->LightEnable(0, TRUE); // Turn light off g_pD3DDevice->LightEnable(0, FALSE);
以上就是使用Direct3D的光照系统的内容了!在继续前进之前再最后警告一句:Direct3D在图形系统中使用灯光时做的工作很得体,但是如果使用者的图形卡不支持光照的话,那么Direct3D不得不模拟光照效果。虽然这不是什么坏事,但是使用光照的话,模拟会减慢渲染的速度。不过,不要让光照模拟的威胁来阻止你,因为在你的游戏中使用光照效果可以大大提高你的图形效果。
===============================================================================
好了,这一节讲完了!其实我觉得这里的安排不太科学,因为光照是与材质结合在一起的,二者不能够孤立地存在;但是讲述材质相关的知识却是在第10期。其实大家应该看看“龙书”第二版的第10章,那里讲述地非常透彻!
对了,我的“龙书”第二版修正版已经更新到了第12章了,稍后我会分享给大家的。
然后是相关的代码。这一部分的代码示例叫做Lights,但是实际上只有一个光源,是点光源。我同样也使用的是shader方法,因为我觉得这样能够让我们看得更清楚,而且也可以选择实现光照的其他方法。前面说了,将光照和材质分开是不太科学的。原始的示例程序中,作者并没有定义材质,而是在顶点结构中定义一个diffuse颜色成分。其实这样并没有真正地用到光照的精髓。所以我按照“龙书”第二版的方法将光照和材质都定义了出来,然后在.fx文件中实现二者的互动。并且,我还增加了镜面反射光。不过,与“龙书”第二版的方法不同的是,我这里使用了Direct3D内置的D3DLIGHT9结构体(但是并不是像书上那样用的)。
下面是程序运行时的截图:
最后我给出代码,包含了原始代码和本人更新的代码:
Lights代码