在将第五章每个示例代码过了一遍之后,大致明白了光照这一章的内容,主要分为四点:
一、光照的类型分为三种,并且均通过结构D3DCOLORVALUE或D3DXCOLOR来表示光线的颜色
- 环境光(Ambient Light)经其它表面反射到达物体表面,并照亮整个场景,通常用做较低代价的粗略模拟。
- 漫射光(Diffuse Light)沿着特定的方向传播,到达某个表面后将沿着各个方向均匀反射,因此从各个方向观察物体表面亮度均相同。
- 镜面光(Specular Light)沿着特定的方向传播,到达一表面后将沿着另一个方向严格反射,从而形成只有在一定角度范围内才可以观察到的高亮度照射。相比其它类型光而言,镜面光的计算量要大得多,并且Direct3D默认不进行镜面光计算,因此需要利用以下代码进行绘制状态设置。
Device->SetRenderState(D3DRS_SPECULARENABLE, true);
二、物体的颜色由其反射的光的颜色决定,D3D则通过设置材质来定义物体表面对各类颜色光的反射比例,其结构体如下:
typedef struct D3DMATERIAL9 { D3DCOLORVALUE Diffuse; // 对漫射光的反射率 D3DCOLORVALUE Ambient; // 对环境光的反射率 D3DCOLORVALUE Specular; // 对镜面光的反射率 D3DCOLORVALUE Emissive; // 增强物体的亮度,让它看起来好像在发光 float Power; // 增加镜面高光点的锐度 }
三、D3D通过顶点的法线方向来确定光线到达表面时的入射角,而由于光照计算是对每个顶点进行的,所以D3D需要知道同一个顶点在不同的三角元中的局部朝向。这使得同一坐标的顶点无法贡献其法线信息,因为在不同的三角元中它的法线信息是不同的,相应的,在这种情况下,使用索引的优势也就不怎么明显了。
通常情况下,某个三角元中的每个顶点,其法向量均与面的法向量相同(即该三角元两条边的向量的叉乘结果)。而有时为了表示曲面时,则会将顶点法向量取值为所有共享该顶点的三角元面法向量的平均值。
同时,考虑到在变换过程中顶点法线有可能不再是规范化的(不清楚这句话的含义......),所以在变换完成之后,需要通过设置绘制状态来使其重新规范化。
Device->SetRenderState(D3DRS_NORMALIZENORMALS, true);
四、D3D支持三种类型的光源:
- 点光源(Point lights),在世界坐标系中有固定的位置,并向所有方向发射光线。
- 方向光(Directional lights),没有位置信息,是一种所发射光线相互平行的、沿着某一特定方向传播的光线。
- 聚光灯(Spot lights),就是手电筒,分为较亮的内圆锥形光柱和较暗的外圆锥形光柱,也是沿着某一特定方向传播。
光源的结构体定义如下:
typedef struct D3DLIGHT9 { // 需要的光源类型,可以设置为D3DLIGHT_POINT、D3DLIGHT_SPOT和D3DLIGHT_DIRECTIONAL三种取值 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;
之后是东拼西凑起来弄出来的光照测试程序,在找到效率更高的图形创建方案之前,我不想再一个一个的拼接三角形了。
这次完成的是三个三角锥,目的是测试三种光源类型的区别,其中坐标和法向量均直接复制第五章的示例,而材质分别设置为反射白色光、反射蓝色光和反射绿色光。另外,在点光源和方向光的测试中,摄影机的位置将会随着按压键盘方向键而产生改变,在聚光灯的测试中,则会根据同样的方式来改变聚光灯的照射方向。
首先,需要在d3dUtility.h头文件中添加某些函数声明,目的是简化光源和材质的初始化难度:
namespace d3d { . . . // 光源的初始化设置 D3DLIGHT9 InitDirectionalLight(D3DXVECTOR3* direction, D3DXCOLOR* color); D3DLIGHT9 InitPointLight(D3DXVECTOR3* position, D3DXCOLOR* color); D3DLIGHT9 InitSpotLight(D3DXVECTOR3* position, D3DXVECTOR3* direction, D3DXCOLOR* color); // 材质的初始化设置 D3DMATERIAL9 InitMtrl(D3DXCOLOR a, D3DXCOLOR d, D3DXCOLOR s, D3DXCOLOR e, float p); const D3DMATERIAL9 WHITE_MTRL = InitMtrl(WHITE, WHITE, WHITE, BLACK, 2.0f); const D3DMATERIAL9 RED_MTRL = InitMtrl(RED, RED, RED, BLACK, 2.0f); const D3DMATERIAL9 GREEN_MTRL = InitMtrl(GREEN, GREEN, GREEN, BLACK, 2.0f); const D3DMATERIAL9 BLUE_MTRL = InitMtrl(BLUE, BLUE, BLUE, BLACK, 2.0f); const D3DMATERIAL9 YELLOW_MTRL = InitMtrl(YELLOW, YELLOW, YELLOW, BLACK, 2.0f); }
之后是这些函数的实际定义,值得注意的是,不同分类的光源,其结构体所包含的数据成员也是不同的,下列代码需要添加到d3dUtility.cpp文件当中:
// 方向光的设置 D3DLIGHT9 d3d::InitDirectionalLight(D3DXVECTOR3* direction, D3DXCOLOR* color) { D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); light.Type = D3DLIGHT_DIRECTIONAL; light.Ambient = *color * 0.6f; light.Diffuse = *color; light.Specular = *color * 0.6f; light.Direction = *direction; return light; } // 点光源的设置 D3DLIGHT9 d3d::InitPointLight(D3DXVECTOR3* position, D3DXCOLOR* color) { D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); light.Type = D3DLIGHT_POINT; light.Ambient = *color * 0.6f; light.Diffuse = *color; light.Specular = *color * 0.6f; light.Position = *position; light.Range = 1000.0f; light.Falloff = 1.0f; light.Attenuation0 = 1.0f; light.Attenuation1 = 0.0f; light.Attenuation2 = 0.0f; return light; } // 聚光灯的设置 D3DLIGHT9 d3d::InitSpotLight(D3DXVECTOR3* position, D3DXVECTOR3* direction, D3DXCOLOR* color) { D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); light.Type = D3DLIGHT_SPOT; light.Ambient = *color * 0.0f; light.Diffuse = *color; light.Specular = *color * 0.6f; light.Position = *position; light.Direction = *direction; light.Range = 1000.0f; light.Falloff = 1.0f; light.Attenuation0 = 1.0f; light.Attenuation1 = 0.0f; light.Attenuation2 = 0.0f; light.Theta = 0.4f; light.Phi = 0.9f; return light; } // 物体材质的设置 D3DMATERIAL9 d3d::InitMtrl(D3DXCOLOR a, D3DXCOLOR d, D3DXCOLOR s, D3DXCOLOR e, float p) { D3DMATERIAL9 mtrl; mtrl.Ambient = a; mtrl.Diffuse = d; mtrl.Specular = s; mtrl.Emissive = e; mtrl.Power = p; return mtrl; }
之后是主要的源代码,依旧是从全局变量Device、屏幕高度Height和屏幕宽度Width的定义开始::
/* * * File: lightTest.cpp * * Author: EnoWang * * Desc: 绘制了三个不同材质的三棱锥,用于测试三种不同的光源 * */ #include "d3dUtility.h" IDirect3DDevice9* Device = 0; const int Width = 640; const int Height = 480; IDirect3DVertexBuffer9* Pyramid = 0; D3DXMATRIX Worlds[3]; D3DMATERIAL9 Marl[3];
相比之前的程序而言,这次我新设置了两个全局数组,一是Worlds数组,用于存储不同的世界坐标变换值,将分别作用于三个材质不同的三棱锥;二是Marl数组,它存储了三种不同的材质信息,在绘制时才会依此数据对三棱锥的材质进行设置。而由于Worlds数组和Marl数组的建立,这个光照测试程序只需创建一个三棱锥顶点缓存。
接下来创建顶点结构体:
// 顶点结构体 struct Vertex { Vertex(){ } Vertex(float x, float y, float z, float nx, float ny, float nz) { _x = x; _y = y; _z = z; _nx = nx; _ny = ny; _nz = nz; } float _x, _y, _z; float _nx, _ny, _nz; static const DWORD FVF; }; const DWORD Vertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL;
与之前的程序相比,这次的顶点结构体没有颜色数据成员,新增了顶点法线数据成员,以确定光线到达表面时的入射角,FVF的值也变为D3DFVF_XYZ | D3DFVF_NORMAL。
接下来定义框架函数:
// 框架函数 bool Setup() { // 创建三棱锥的顶点缓存 Device->CreateVertexBuffer( 12 * sizeof(Vertex), D3DUSAGE_WRITEONLY, Vertex::FVF, D3DPOOL_MANAGED, &Pyramid, 0); // 向三棱锥的顶点缓存中写入数据 Vertex* vertex; Pyramid->Lock(0, 0, (void**)&vertex, 0); vertex[0] = Vertex(-1.0f, 0.0f, -1.0f, 0.0f, 0.707f, -0.707f); vertex[1] = Vertex( 0.0f, 1.0f, 0.0f, 0.0f, 0.707f, -0.707f); vertex[2] = Vertex( 1.0f, 0.0f, -1.0f, 0.0f, 0.707f, -0.707f); vertex[3] = Vertex(-1.0f, 0.0f, 1.0f, -0.707f, 0.707f, 0.0f); vertex[4] = Vertex( 0.0f, 1.0f, 0.0f, -0.707f, 0.707f, 0.0f); vertex[5] = Vertex(-1.0f, 0.0f, -1.0f, -0.707f, 0.707f, 0.0f); vertex[6] = Vertex( 1.0f, 0.0f, -1.0f, 0.707f, 0.707f, 0.0f); vertex[7] = Vertex( 0.0f, 1.0f, 0.0f, 0.707f, 0.707f, 0.0f); vertex[8] = Vertex( 1.0f, 0.0f, 1.0f, 0.707f, 0.707f, 0.0f); vertex[9] = Vertex( 1.0f, 0.0f, 1.0f, 0.0f, 0.707f, 0.707f); vertex[10] = Vertex( 0.0f, 1.0f, 0.0f, 0.0f, 0.707f, 0.707f); vertex[11] = Vertex(-1.0f, 0.0f, 1.0f, 0.0f, 0.707f, 0.707f); Pyramid->Unlock();
可以看到,对于由几个三角元共享的顶点,随着所组成三角元的改变,虽然它的位置信息不会变化,但是法向量却会随之改变,即:顶点的法向量不会被共享。这时如果使用索引来绘制图形,相比直接用顶点绘图而言,它的优势也就不是那么明显了。正因为如此,这里抛弃了索引缓存,直接采用顶点缓存来记录图像绘制信息。
接下来设置一开始创建的世界坐标变换数组和材质信息数组:
// 设置世界坐标系 D3DXMatrixTranslation(&Worlds[0], 0.0f, 2.0f, 0.0f); D3DXMatrixTranslation(&Worlds[1],-1.4f,-1.4f, 0.0f); D3DXMatrixTranslation(&Worlds[2], 1.4f,-1.4f, 0.0f); // 创建材质 Marl[0] = d3d::WHITE_MTRL; Marl[1] = d3d::BLUE_MTRL; Marl[2] = d3d::GREEN_MTRL;
对于之后将要绘制的三棱锥,它们的世界坐标呈现正上方、左下方、右下方的分布,且三个三棱锥的局部中心坐标与原点的距离均为2.0f;而它们材质则分别为:反射白色光,反射蓝色光和反射绿色光。
之后设置光源,因为首先要进行测试的是方向光,所以进行如下设置:
// 设置方向光光源 D3DXVECTOR3 dir(1.0f, 0.3f, 0.6f); D3DXCOLOR color = d3d::WHITE; D3DLIGHT9 dirLight = d3d::InitDirectionalLight(&dir, &color); // 注册并设置光照开关状态 Device->SetLight(0, &dirLight); Device->LightEnable(0, true); // 重新规范化法向量,并启用镜面高光 Device->SetRenderState(D3DRS_NORMALIZENORMALS, true); Device->SetRenderState(D3DRS_SPECULARENABLE, true);
可以看到,这次设置的方向光光源为白光,其中环境光、漫射光和镜面光的亮度值分别设置为:WHITE(255, 255, 255)、0.3WHITE和0.6WHITE。
另外,SetLight函数的作用是,在Direct3D所维护的光源列表当中,对要使用的光源进行注册;而LightEnable函数的作用则是开灯和光灯。
接下来跳过摄影机,直接设置投影矩阵:
// 设置投影矩阵 D3DXMATRIX proj; D3DXMatrixPerspectiveFovLH( &proj, D3DX_PI * 0.5f, (float)Width / (float)Height, 1.0f, 1000.0f); Device->SetTransform(D3DTS_PROJECTION, &proj); return true; } void Cleanup() { d3d::Release<IDirect3DVertexBuffer9*>(Pyramid); }
现在开始定义绘制函数:
bool Display(float timeDelta) { if( Device ) { // 更新场景和摄影机位置 static float angle = (3.0f * D3DX_PI) / 2.0f; static float height = 5.0f; if (::GetAsyncKeyState(VK_LEFT) & 0x8000f) angle -= 1.0f * timeDelta; if (::GetAsyncKeyState(VK_RIGHT) & 0x8000f) angle += 1.0f * timeDelta; if (::GetAsyncKeyState(VK_UP) & 0x8000f) height += 3.0f * timeDelta; if (::GetAsyncKeyState(VK_DOWN) & 0x8000f) height -= 3.0f * timeDelta; D3DXVECTOR3 position(cosf(angle) * 3.0f, height, sinf(angle) * 4.0f); D3DXVECTOR3 target(0.0f, 0.0f, 0.0f); D3DXVECTOR3 up(0.0f, 1.0f, 0.0f); D3DXMATRIX Camera; D3DXMatrixLookAtLH(&Camera, &position, &target, &up); Device->SetTransform(D3DTS_VIEW, &Camera);
首先可以看到,这次设置的镜头更新方式并非之前两个程序所定义的随时间自动旋转,而是会对某些由键盘传递的信息进行响应,从而改变摄影机的位置。简单来说,按住上下左右任意方向键,即可使摄影机转向相应的方向。至于0x8000f的由来,可以在这里看到详细的解释:http://blog.sina.com.cn/s/blog_868579d801011d6l.html
之所以不在Setup函数中设置摄影机,而是选择将摄影机的设置放在将会于主函数中一遍又一遍执行Display函数中,也是因为摄影机的位置会因为angel和height的改变而随时产生改变。
接下来开始正式的绘制:
// 开始绘制 Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0); Device->BeginScene(); Device->SetStreamSource(0, Pyramid, 0, sizeof(Vertex)); Device->SetFVF(Vertex::FVF); for (int cnt = 0; cnt < 3; ++cnt) { Device->SetMaterial(&Marl[cnt]); Device->SetTransform(D3DTS_WORLD, &Worlds[cnt]); Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 4); } Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }
可以看到,正式的绘制在for循环中发生,每次的for循环,都会从设置新的材质和设置新的世界坐标变换开始,在绘制图像结束。三次for循环结束之后,正好绘制了三个材质不同、中心点坐标也不同的三棱锥。
最后,回调函数和Windows主函数依然和前两个程序没有区别,故省略。
方向光的实际效果如下:
接下来测试点光源,对Setup函数最后的光源设置进行修改即可:
// 设置点光源 D3DXVECTOR3 position(0.0f, 0.0f, 0.0f); D3DXCOLOR color = d3d::WHITE; D3DLIGHT9 poiLight = d3d::InitPointLight(&position, &color); // 注册并设置光照开关状态 Device->SetLight(0, &poiLight); Device->LightEnable(0, true);
如上所示,点光源的坐标为(0, 0, 0),白色三棱锥的上方正好处于镜面光和漫射光无法照射到的位置。
接下来测试聚光灯,相比点光源和方向光而言,其作用方式比较特殊,代码改动较多,因此给出除回调函数和主函数之外的所有代码,并标记出修改位置:
#include "d3dUtility.h" IDirect3DDevice9* Device = 0; const int Width = 640; const int Height = 480; IDirect3DVertexBuffer9* Pyramid = 0; D3DXMATRIX Worlds[3]; D3DMATERIAL9 Marl[3]; D3DLIGHT9 spotLight; // 第一处修改
第一处修改,将spotLight设置为全局变量,因为它的初始化设置只需要一次,所以放置在Setup函数中,而由于它的照射方向会随时改变,所以剩余部分的设置放置在Display函数中。
// 顶点结构体 struct Vertex { Vertex(){ } Vertex(float x, float y, float z, float nx, float ny, float nz) { _x = x; _y = y; _z = z; _nx = nx; _ny = ny; _nz = nz; } float _x, _y, _z; float _nx, _ny, _nz; static const DWORD FVF; }; const DWORD Vertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL; // 框架函数 bool Setup() { // 创建三棱锥的顶点缓存 Device->CreateVertexBuffer( 12 * sizeof(Vertex), D3DUSAGE_WRITEONLY, Vertex::FVF, D3DPOOL_MANAGED, &Pyramid, 0); // 向三棱锥的顶点缓存中写入数据 Vertex* vertex; Pyramid->Lock(0, 0, (void**)&vertex, 0); vertex[0] = Vertex(-1.0f, 0.0f, -1.0f, 0.0f, 0.707f, -0.707f); vertex[1] = Vertex( 0.0f, 1.0f, 0.0f, 0.0f, 0.707f, -0.707f); vertex[2] = Vertex( 1.0f, 0.0f, -1.0f, 0.0f, 0.707f, -0.707f); vertex[3] = Vertex(-1.0f, 0.0f, 1.0f, -0.707f, 0.707f, 0.0f); vertex[4] = Vertex( 0.0f, 1.0f, 0.0f, -0.707f, 0.707f, 0.0f); vertex[5] = Vertex(-1.0f, 0.0f, -1.0f, -0.707f, 0.707f, 0.0f); vertex[6] = Vertex( 1.0f, 0.0f, -1.0f, 0.707f, 0.707f, 0.0f); vertex[7] = Vertex( 0.0f, 1.0f, 0.0f, 0.707f, 0.707f, 0.0f); vertex[8] = Vertex( 1.0f, 0.0f, 1.0f, 0.707f, 0.707f, 0.0f); vertex[9] = Vertex( 1.0f, 0.0f, 1.0f, 0.0f, 0.707f, 0.707f); vertex[10] = Vertex( 0.0f, 1.0f, 0.0f, 0.0f, 0.707f, 0.707f); vertex[11] = Vertex(-1.0f, 0.0f, 1.0f, 0.0f, 0.707f, 0.707f); Pyramid->Unlock(); // 设置世界坐标系 D3DXMatrixTranslation(&Worlds[0], 0.0f, 2.0f, 0.0f); D3DXMatrixTranslation(&Worlds[1],-1.4f,-1.4f, 0.0f); D3DXMatrixTranslation(&Worlds[2], 1.4f,-1.4f, 0.0f); // 创建材质 Marl[0] = d3d::WHITE_MTRL; Marl[1] = d3d::BLUE_MTRL; Marl[2] = d3d::GREEN_MTRL; // 第二处修改 for (int cnt = 0; cnt < 3; ++cnt) Marl[cnt].Power = 20.0f;
第二处修改,增强高光点的锐度。
// 第三处修改,设置聚光灯 D3DXVECTOR3 position(0.0f, 0.0f, -5.0f); D3DXVECTOR3 direction(0.0f, 0.0f, 1.0f); D3DXCOLOR color = d3d::WHITE; spotLight = d3d::InitSpotLight(&position, &direction, &color);
第三处修改,设置聚光灯
// 注册并设置光照开关状态 Device->SetLight(0, &spotLight); Device->LightEnable(0, true); // 重新规范化法向量,并启用镜面高光 Device->SetRenderState(D3DRS_NORMALIZENORMALS, true); Device->SetRenderState(D3DRS_SPECULARENABLE, true); // 第四处修改:设置摄影机 D3DXVECTOR3 pos(0.0f, 0.0f,-5.0f); D3DXVECTOR3 target(0.0f, 0.0f, 0.0f); D3DXVECTOR3 up(0.0f, 1.0f, 0.0f); D3DXMATRIX Camera; D3DXMatrixLookAtLH(&Camera, &pos, &target, &up); Device->SetTransform(D3DTS_VIEW, &Camera);
为了凸显聚光灯的特殊性,这次演示不需要更新摄影机的位置,而是更新聚光灯的照射方向,因此摄影机的设置放回了Setup函数,同时这也是第四处修改。
// 设置投影矩阵 D3DXMATRIX proj; D3DXMatrixPerspectiveFovLH( &proj, D3DX_PI * 0.5f, (float)Width / (float)Height, 1.0f, 1000.0f); Device->SetTransform(D3DTS_PROJECTION, &proj); return true; } void Cleanup() { d3d::Release<IDirect3DVertexBuffer9*>(Pyramid); } bool Display(float timeDelta) { if( Device ) { // 第五处修改,改变聚光灯的照射方向 static float angle = (3.0f * D3DX_PI) / 2.0f; if (::GetAsyncKeyState(VK_LEFT) & 0x8000f) spotLight.Direction.x -= 1.0f * timeDelta; if (::GetAsyncKeyState(VK_RIGHT) & 0x8000f) spotLight.Direction.x += 1.0f * timeDelta; if (::GetAsyncKeyState(VK_UP) & 0x8000f) spotLight.Direction.y += 1.0f * timeDelta; if (::GetAsyncKeyState(VK_DOWN) & 0x8000f) spotLight.Direction.y -= 1.0f * timeDelta; Device->SetLight(0, &spotLight); Device->LightEnable(0, true);
第五处修改则是在改变聚光灯的照射方向,本来随着方向键改变的是摄影机的位置,在这里修改为改变聚光灯照射的方向,当然,每次修改之后,同样要先对聚光灯进行注册,然后再开启光照。
修改的部位置一共就只有这五处,其余部分和点光源测试/方向光源测试的代码没有区别。
// 开始绘制 Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0); Device->BeginScene(); Device->SetStreamSource(0, Pyramid, 0, sizeof(Vertex)); Device->SetFVF(Vertex::FVF); for (int cnt = 0; cnt < 3; ++cnt) { Device->SetMaterial(&Marl[cnt]); Device->SetTransform(D3DTS_WORLD, &Worlds[cnt]); Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 4); } Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }
最终运行结果的截图如下(充分说明了这玩意就是一个手电筒):
最后给出龙书源码的下载位置:http://www.d3dcoder.net