第五章 DirectX 光照,材质和纹理(上)

3D场景中使用光照非常简单,我们不需要为模型物体的每个顶点指定颜色值,只要使用某种种类的光照,并设置模型物体的材质,那么Direct3D就会根据光照模型算法,计算出每个顶点的颜色值,让模型产生颜色。我们之前大致介绍过,3D模型都是在建模软件中完成的,同时也给模型赋予了材质。因此,在实际游戏开发中,我们只需要加载模型文件,并读取材质,并在渲染模型的时候,为模型设置这个材质。那么,在光源的照射下,模型就会显示自己的颜色了。我们一般不会手动使用顶点来构建一个模型,更多的是使用顶点来构建一个四边形,这种情况多用于GUI用户界面和2D游戏开发。同时,我们也不会为顶点指定颜色,基本上都是将一张图片贴到这个四边形上,也就是纹理映射。建模和纹理贴图都属于美术工作人员的事情,程序开发人员主要就是拿到模型和贴图文件后,将其加载到游戏中。

关于光照,有两个概念,大家一定要区分开。一个是光照的类型,一个是光源的类型。光照的类型之前大致讲过,可以分为环境光,漫反射光,镜面反射光(高光)。光源的类型分为点光源(Point Light,方向光(Directional Light)和聚光灯(Spot Light)。这两者其实并不冲突,前者是根据光照结构来分类,后者则是根据照明区域来分类。不管是点光源,方向光,还是聚光灯,它们都是由环境光,漫反射光和高光组成的。

首先还是先介绍光照类型。环境光是指一个模型物体即使没有直接的光源照射,但是只要有光线(其他物体反射)到达这个物体,这个模型物体就能被看见。这种基于整个自然界环境的整体亮度,称为环境光。环境光没有位置或者方向的特征,只有一个颜色亮度值,而且不会衰减。也就是说,环境光是一个全局光的概念。想要以较低的代价和开销来近似模拟光照的话,直接开启环境光是一个不错的选择。在Direct3D中环境光的设置代码如下:

D3DDevice->setRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(255,255,255));

漫反射光是最普遍的,太阳光和灯光的照射都可以看成漫反射光。这种类型的光沿着特定的方向传播,到达物体表面后,沿着各个方向均匀反射。因此,无论从哪个方向观察物体,颜色和亮度都是相同的。一般情况下,模型的颜色基本就是由漫反射光决定的。

镜面反射光(高光),该类型的光沿着特定的方向传播,到达物体表面的时候,将沿着一个方向反射,从而形成只能在一个角度范围内才能观察到的高亮度照射。需要注意的是,镜面光与其他类型的光相比,计算量很大,Direct3D默认情况是关闭镜面反射的。如果想要开启镜面发射的话,可以使用如下代码:

D3DDevice->SetRenderState(D3DRS_SPECULARENABLE, true);

另外,还有一种自发光(Emissive Light),就是模型自己发出的光,它是通过对象的自发光材质实现的。在物体的材质Emissive描述了自发光的颜色和透明度。它的使用目的是不通过光源照射从而是模型物体显得更亮一些,并且自发光不会对周围物体产生任何的光照影响,也就是说自发光并不参与光照模型计算。

上述将的是光照类型,下面要将的是光源类型, Direct3D主要有三种类型的光源,点光源(Point Light,方向光(Directional Light)和聚光灯(Spot Light)。

点光源具有颜色和位置属性,它向所有方向发射光。从一个点向周围均匀发射的光,有最大的照明范围,亮度随距离衰减,最明显的一个例子就是我们家里用的灯泡。因为点光源会衰减,因此它是一个局部光源,它只能照亮一部分区域。

方向光源是从无穷远处发出的一组平行,均匀的光线,在场景中以相同的方向传播,具有颜色和方向属性,同样不受距离衰减的影响。方向光是一组没有衰减的平行光,类似太阳光的效果。方向光也算是一种全局光。

聚光灯光源类似探照灯。聚光灯发出的光类似一个圆锥体(内外椎体),它发出的光会随着距离而衰减,内椎体向外椎体衰减。因为聚光灯收到衰减的影响,因此其对应的光照模型计算性能开销更高,所以应该尽量避免使用聚光灯。聚光灯是一个局部光源。

DirectX中使用D3DLIGHT9结构体来代表一个光源对象,该结构体中包含光源类型以及光的颜色值,对于局部光源的话,还需要设置它一个衰减范围值。这些参数的设置都比较人性化,我们在代码案例中再详细介绍。接下来,我们说一个材质,在DirectX中使用D3DMATERIAL9结构体来代表一个材质对象,它的参数基本都就是反射率的设置。接下来,我们使用VS2019创建一个新项目“D3D_05_Light”,然后复制之前的代码进来。为了更好的规划我们的代码,我们将光源的设置独立存放到新的方法initLight中。因为光的设置基本上都是一次性的,因此我们将initLight函数就放在initScene函数中。以下是全局变量的声明:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;

// 鼠标位置
int mx = 0, my = 0;

// 茶壶模型对象
LPD3DXMESH D3DTeapt;

接下来就是我们的initScene函数,代码如下:

// 初始化茶壶模型
D3DXCreateTeapot(D3DDevice, &D3DTeapt, NULL);

// 初始化投影变换
initProjection();

// 初始化光照
initLight();

在initProjection函数中,我们固定了三种类型的变换,代码如下:

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(0.0f, 2.0f, -3.0f);	    // 摄像机的位置
D3DXVECTOR3 viewLookAt(0.0f, 0.0f, 0.0f);	// 观察点的位置
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);		// 向上的向量
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

// 设置透视投影变换矩阵
D3DXMATRIX projMatrix;
float angle = D3DX_PI * 0.5f; // 90度
float wh = (float)WINDOW_WIDTH / (float)WINDOW_HEIGHT;
D3DXMatrixPerspectiveFovLH(
	&projMatrix,	// 表示投影变换矩阵
	angle,		    // 表示摄像机的视域角度
	wh,			    // 表示屏幕显示区的横纵比
	1.0f,			// 表示视域体中近裁剪面
	1000.0f);		// 表示视域体中远裁剪面
D3DDevice->SetTransform(D3DTS_PROJECTION, &projMatrix);

// 设置视口变换
D3DVIEWPORT9 viewport = {
	0,				// 视口相对于窗口的X坐标,默认0即可
	0,				// 视口相对于窗口的Y坐标,默认0即可
	WINDOW_WIDTH,	// 视口的宽度
	WINDOW_HEIGHT,  // 视口的高度
	0,				// 视口在深度缓存中的最小深度值,默认0即可
	1				// 视口在深度缓存中的最大深度值,默认1即可
};
D3DDevice->SetViewport(&viewport);

接下来就是我们的initLight函数,我们逐一的介绍不同光源的创建,代码如下:

// 点光源
D3DLIGHT9 pointLight;
::ZeroMemory(&pointLight, sizeof(pointLight));
pointLight.Type = D3DLIGHT_POINT;				            // 表示光源类型
pointLight.Ambient = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	    // 表示环境光颜色值
pointLight.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f);	    // 表示漫反射光颜色值
pointLight.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);    // 表示高光反射光颜色值
pointLight.Position = D3DXVECTOR3(0.0f, 10.0f, 0.0f);       // 表示光源位置,世界原点的Y轴上面10单位处
pointLight.Attenuation0 = 0.0f;	// 恒定衰减系数,通常为0.0f
pointLight.Attenuation1 = 0.1f;	// 线性衰减系数,通常为1.0f,这里我们使用0.1
pointLight.Attenuation2 = 0.0f;	// 二次衰减系数,通常为0.0f
pointLight.Range = 1000.0f;	    // 光能够传播的最大范围
D3DDevice->SetLight(0, &pointLight);
D3DDevice->LightEnable(0, true);

所有的光源都是D3DLIGHT9结构体,不同的光源类型是同Type来区分的。所有的光源都有环境光颜色值,漫反射光颜色值和高光颜色值的设置。对于点光源来讲,还需要设置的位置,传播的最大范围,我们可以理解为一个球体。还要设置点光源的一个衰减,距离越远,光源衰减越厉害。以下是包含距离的衰减公式:

Attenuation0通常为0.0fAttenuation1通常为1.0fAttenuation2通常为0.0fD代表到光源的距离,Attenuation就是最终计算所得的衰减值。因为本案例中,我们的距离非常的小,因此我们设置Attenuation10.1即可。关于衰减的问题,大家了解一下就可以了。定义完D3DLIGHT9结构体之后,紧接着再调用SetLightLightEnable两个方法就可以在场景中使用该光源了。SetLight函数就两个参数,第一个参数是光源的索引ID,第二个参数就是刚刚构建的D3DLIGHT9结构体,LightEnable函数也是两个参数,第一个参数是光源的索引ID,第二个参数是布尔类型,true代表启用该光源,false代表禁用该光源。我们先看一看这个点光源照射茶壶的效果:

我们可以看到,点光源在茶壶(世界原点位置)的正上方,然后照亮四周1000单位内的场景。我们设置茶壶反射红颜色,因此,茶壶就显示了红色了。茶壶底部没有光,因此是黑色。接下来,我们再定义一个平行光,让其在X轴方向,从右向左照亮整个场景。

// 平行光
D3DLIGHT9 directionalLight;
::ZeroMemory(&directionalLight, sizeof(directionalLight));
directionalLight.Type = D3DLIGHT_DIRECTIONAL;		            // 表示光源类型
directionalLight.Ambient = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 表示环境光颜色值
directionalLight.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f);	// 表示漫反射光颜色值
directionalLight.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 表示高光反射光颜色值
directionalLight.Direction = D3DXVECTOR3(-1.0f, 0.0f, 0.0f);	// 表示照射方向,向X负方向(由右向左)传播
D3DDevice->SetLight(1, &directionalLight);
D3DDevice->LightEnable(1, true);

以上定义了一个平行光,它特有的参数只有一个,就是照射的方向。同样我们有需要使用SetLight LightEnable两个方法来启用这个平行光。该平行光的索引为1,上一个点光源的索引是0,这个大家一定要区分开。在本案例中,我们让平行光从右边照向左边,那么茶壶的右边就会很亮,左下方则暗一些。平行光没有衰减,它可以照射到场景中所有的物体。因此,它勉强算一个全局光。但是,它与真正的环境全局光还是不一样的。环境全局光会从四面八方照射到场景中所有物体,因此不会给物体产生暗部。我们看看平行光的效果:

 我们可以看到茶壶的右边变亮了,因为它收到了来自右边平行光的照射。接下来,我们继续创建一个聚光灯,我们让这个聚光灯从左边照射到右边,代码如下:

// 聚光灯
D3DLIGHT9 spotLight;
::ZeroMemory(&spotLight, sizeof(spotLight));
spotLight.Type = D3DLIGHT_SPOT;					        // 表示光源类型
spotLight.Ambient = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 表示环境光颜色值
spotLight.Diffuse = D3DXCOLOR(1.0f, 1.0f, 1.0f, 0.0f);	// 表示漫反射光颜色值
spotLight.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 表示高光反射光颜色值
spotLight.Position = D3DXVECTOR3(-10.0f, 0.0f, 0.0f);   // 表示光源位置,世界原点的X轴左边10单位处
spotLight.Direction = D3DXVECTOR3(1.0f, 0.0f, 0.0f);    // 表示照射方向,向X正方向(由左向右)传播
spotLight.Attenuation0 = 0.0f;		// 恒定衰减系数,通常为0.0f
spotLight.Attenuation1 = 0.1f;		// 线性衰减系数,通常为1.0f,这里我们使用0.1
spotLight.Attenuation2 = 0.0f;		// 二次衰减系数,通常为0.0f
spotLight.Range = 1000.0f;		    // 光能够传播的最大范围
spotLight.Falloff = 0.1f;			// 光从内圆锥到外圆锥之间的强度衰减,通常设为1.0f
spotLight.Phi = D3DX_PI / 2.0f;		// 指定聚光灯外圆锥的角度,单位是弧度,我们设置为90度
spotLight.Theta = D3DX_PI / 4.0f;	// 指定聚光灯内圆锥的角度,单位是弧度,我们设置为45度
D3DDevice->SetLight(2, &spotLight);
D3DDevice->LightEnable(2, true);

以上就是一个聚光灯的定义,它的参数比较多。因为聚光灯是一个内外圆锥体,所以需要设置它的位置,方向,传播距离,内外圆锥的弧度。同时,还需要设置它的衰减。聚光灯的衰减处理距离因素之外,还需要设置内圆锥向外圆锥的衰减。定义完成后,同样需要使用SetLight LightEnable两个方法来启用这个平行光。我们来看看添加聚光灯之后的效果:

我们可以看到这个茶壶分别从上边,左边和右边三个方向被照亮了。虽然我们在三种类型的光源中都定义了环境光颜色值(都是零),但是真正的环境光效果这样设置,代码如下:

// 设置一下环境光
D3DDevice->SetRenderState(D3DRS_AMBIENT, D3DCOLOR_XRGB(255, 255, 255));

那么,在上面的三个光源中设置环境光难道不起作用吗?答案,起作用。但是不能同时设置两种类型的环境光,会出问题的。一般情况下,我们不设置光源中环境光,而是直接使用上面的一句代码来统一的设置环境光颜色值。使用光源照亮场景,最简单且性能最佳的方式就是设置环境光,其实是平行光,再次是点光源,最后是聚光灯。我们之前讲过,光照模型消耗大量的硬件性能。但是,不同的光照组合使用会模拟真实的光影效果,让用户的体验度增加。所以,场景中光照的使用,还是依据游戏的设计需求来定。在本案例中,我们也没有设置高光的效果,因为高光的效果也需要硬件性能的支撑。最后,我们为了确保光照的使用,我们还要开启光照,代码如下:

// 开启光照
D3DDevice->SetRenderState(D3DRS_LIGHTING, true);

光照介绍完之后,我们就来设置一个默认的材质。实际上,在游戏开发中,不同的模型会有不同的材质。本案例只是演示灯光效果,因此只做一个默认材质即可。上文中也提到了,我们做一个反射红光的材质,并赋予场景中的模型即可。代码如下:

// 设置默认材质
D3DMATERIAL9 defaultMaterial;
::ZeroMemory(&defaultMaterial, sizeof(defaultMaterial));
defaultMaterial.Ambient = D3DXCOLOR(1.0f, 0.5f, 0.5f, 0.0f);	// 环境光反射:100%反射红光,50%反射绿光,50%反射蓝光
defaultMaterial.Diffuse = D3DXCOLOR(1.0f, 0.5f, 0.5f, 0.0f);	// 漫反射:    100%反射红光,50%反射绿光,50%反射蓝光
defaultMaterial.Specular = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不反射高光
defaultMaterial.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f);	// 不自发光
defaultMaterial.Power = 0.0f;							        // 没有高光区
D3DDevice->SetMaterial(&defaultMaterial);

我们可以使用D3DXCOLOR(R, G, B, A)来表示材质反射颜色值(D3DCLORVALUE),其中R, G, B, A分别表示0.0f1.0f之间的红色,绿色,蓝色, 透明度的分量值。0代表不反射该分量的颜色光,1代表全部反射该分量的颜色光。定义完D3DMATERIAL9的结构体之后,我们需要调用SetMaterial方法来设置当前使用的材质。另外如果没有在程序中用代码来指定材质的话,Direct3D有默认的材质,默认材质反射所有的漫反射光,但不反射环境光和镜面发射光,也没有自发光颜色。至此,我们的initLight函数已经完毕了。这里我们不设置用户交互事件,因此update函数为空。剩下的就是renderScene函数,里面就一句代码:

// 绘制茶壶
D3DTeapt->DrawSubset(0);

最后运行代码,看到茶壶的真实效果,因为我们添加了全局的环境光,因此,茶壶所有的地方,都会变亮。效果如下图所示:

因为全局光照的添加,使得茶壶的整体红色更加亮了。如果我们只开启全局环境光,去掉其他光源的效果是这样的:

全局的环境光从四面八方同时照向茶壶,使其表面的颜色都是一样的,没有光影效果。

在光照模型中,还有一个法线的概念,它决定了反射光的方向。每一个面都有自己的法线,在3D游戏中一个三角形代表一个面。面法线很容易理解,即垂直于三角形面的一条法线。面法线可以通过他的三个点求出两条边,两条边再叉乘来获得面法线。那顶点法线又从何而来呢,严格的从法线的定义上来说,其实顶点是不存在法线的,那为何又有顶点法线这个概念的。让顶点也拥有法线,是为了在光照计算时,能够计算入射光线到达表面的入射角,能够在多面体的表面获得一种平滑的效果。一般情况下,顶点法线和面法线是相同的,但是在一些近似圆球表面(平滑面),顶点法线和面法线就不一致了。如果不使用顶点法线,一个三角形面的三个顶点的光照计算按照其所在面的面法线来计算,因为三个顶点的法线相同,那么三个顶点将会获得相同的光照效果。这样的光照会造成面与面之间颜色没有过渡,显得很生硬。如果使用顶点法线,同一个面的三个顶点的法线就不一定相同了,就能在多面体的表面获得一种平滑过渡的光照效果。顶点法线就是把共享该顶点的几个面的法线相加,然后除以面的数量,得到的平均值法线,这就是顶点法线。说白了,顶点法线就是让大量三角形去逼近模拟光滑曲面的时候,颜色是平滑过渡的,显得自然而真实一些。如果我们是手动计算顶点法线的话,需要把顶点法线单位化!

 本课程的所有代码案例下载地址:

workspace.zip

备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咆哮的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值