学习目标:
- 了解 Direct3D 所支持的几种光源以及这些光源所发出光线的类型
- 理解光照的定义方式,以便了解光线与其所照射表面的交互方式
- 理解如何用数学语音描述三角形单元的朝向,以判定光线现对于该单元的射入角
为了场景的真实感,可为场景增加光照。光照有助于描述实体形状和立体感。
使用光照时,无需自行指定顶点的颜色值; Direct3D 会将顶点送入光照计算引擎,依据光源类型,材质以及物体表面相对于光源的朝向,计算出每个顶点的颜色值。基于某种光照模型计算出各顶点的颜色,会使绘制结果更逼真。
光照的组成:
在 Direct3D 的光照模型中,光源发出的光由一下3个分量或3种类型的光组成。
- 环境光: 这种类型的光经其他的表面反射到达物体表面,并照亮整个场景。例如,物体的某些部分被一定程度的照亮,但物体并没有处于光源的直接照射下。物体之所以会被照亮,是由于其他物体对光的反射。要想以较低代价粗略的模拟这类反射光,环境光是一个很好的选择。
- 漫射光: 这种类型的光沿着特定方向传播。当它到达某一表面时,将沿着各个方向均匀反射。由于漫射光沿所有方向反射,无论从哪个方向观察,表面亮度均相同,所以采用该模型时,无需考虑观察者的位置。这样,漫射光方程中,仅需考虑光传播的方向以及表面的朝向。从一个光源发出的光一般都是这种类型的。
- 镜面光: 这种类型的光沿特定方向传播。但此类光到达一个表面时,将严格的沿着另一个方向反射,从而形成只能在一定角度范围内才能观察到的高亮度照射。因为在这种模式中,光线均沿着一个方向反射,所以在镜面光照方程中不仅需要考虑光线的入射方向和图元的表面朝向,而且要需要考虑观察点的位置。镜面光可用于模拟物体上的高光点,例如光线照射到一个抛光的表面所形成的高亮照射。镜面光与其他光相比计算量要大的多。因此 Direct3D 为其提供了开关选项。
- Direct3D镜面光使用方法: 镜面光与其他光相比计算量要大的多。因此 Direct3D 为其提供了开关选项。默认状态下,Direct3D 不进行镜面反射计算;要启用镜面光,必须设置绘制状态。
direct->SetRenderState(D3DRS_SPECULARENABLE, TRUE);
光线的表示:
每种类型的光都可以用结构D3DCOLORVALUE 或 D3DXCOLOR来表示,这些类型描述了光线的颜色。
几种光线颜色的例子:
D3DXCOLOR redambient(1.0f, 0.0f, 0.0f, 1.0f);
D3DXCOLOR bluediffuse(0.0f, 0.0f, 1.0f, 1.0f);
D3DXCOLOR whitespecular(1.0f, 1.0f, 1.0f, 1.0f);
注意:描述光线颜色时,D3DXCOLOR 类中的 Alpha 值都将被忽略。
材质:
在现实世界中,我们所观察到的物体的颜色是由该物体所反射的光的颜色决定的。例如,一个纯红色的球体反射了全部的红色入射光,所以该球体呈现成红色。Direct3D 通过定义物体的材质来模拟同样的现象。材质允许我们定义物体表面对各种颜色光的反射比例。
材质的表示:
typedef struct D3DMATERIAL9{
D3DCOLORVALUE Diffuse;
D3DCOLORVALUE Ambient;
D3DCOLORVALUE Specular;
D3DCOLORVALUE Emissive;
float Power;
}D3DMATERIAL9 *LPD3DMATERIAL9;
- Diffuse: 指定材质对漫射光的反射率
- Ambient: 指定材质对环境光的反射率
- Specular: 指定材质对镜面光的反射率
- Emissive: 该分量用于增强物体的亮度,使之看起来好像可以自己发光
- Power: 指定镜面高光点的锐度,该值越大,高光点的锐度越大
- 假定有一个红色的球体,我们想将该球体的材质属性定义为只反射红色光,
D3DMATERIAL9 red;
::ZeroMemory(&red, sizeof(red));
red.Diffuse = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);
red.Ambient = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);
red.Specular = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);
red.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f);
red.Power = 5.0f;
这里将绿色和蓝色分量设为0,表明该材质对这些颜色的光的反射率为0%。将红色分量设为1,表面该材质对红色光的反射率为100%。注意,我们可以控制材质在各种类型光照下哪种颜色的光将被反射。
还需要注意,如果用一个只能发射蓝色光的光源来照射一个红色的球体,则由于蓝色光被完全吸收,而反射的红色光为0,所以该球体将无法被照亮。当一个物体吸收来所有光时,便呈现为黑色。类似的,如果一个物体能够100%的反射红色光,绿色光和蓝色光,它将呈现为白色。
为了减轻手工初始化材质结构 D3DMATERIAL9 的负担,我们可在代码中添加如下实用函数以及全局材质产量。
D3DMATERIAL9 d3d::InitMtr1(D3DXCOLOR a, D3DXCOLOR d, D3DXCOLOR s, D3DXCOLOR e, float p);
{
D3DMATERIAL9 mtr1;
mtr1.Ambient = a;
mtr1.Diffuse = d;
mtr1.Specular = s;
mtr1.Emissive = e;
mtr1.Power = p;
return mtr1;
}
namespace d3d
{
D3DMATERIAL9 InitMtr1(D3DXCOLOR a, D3DXCOLOR d, D3DXCOLOR s, D3DXCOLOR e, float p);
const D3DMATERIAL9 WHITE_MTRL = InitMtr1(WHITE, WHITE, WHITE, BLACK, 8.0f);
const D3DMATERIAL9 RED_MTRL = InitMtr1(RED, RED, RED, BLACK, 8.0f);
const D3DMATERIAL9 GREEN_MTRL = InitMtr1(GREEN, GREEN, GREEN, BLACK, 8.0f);
const D3DMATERIAL9 BLUE_MTRL = InitMtr1(BLUE, BLUE, BLUE, BLACK, 8.0f);
const D3DMATERIAL9 YELLOW_MTRL = InitMtr1(YELLOW, YELLOW, YELLOW, BLACK, 8.0f);
};
材质设定:
顶点结构中不含有材质属性,但我们必须对当前材质进行设定。下面这个函数可用来对当前材质进行设定:
IDirect3DDevice9::SetMaterial(CONST D3DMATERIAL9* pMaterial);
绘制材质不同的物体需要先更换材质设定:
D3DMATERIAL9 blueMaterial, redMaterial;
Device->SetMaterial(&blueMaterial);
drawSphere(); //蓝球
Device->SetMaterial(&redMaterial);
drawSphere(); //红球
顶点法线:
什么是顶点法线?:
面法线是一个描述多边形朝向的向量
顶点法线正是基于上述思路而产生的,但它并不用于指定每个多边形的法向量。顶点法线描述的是构成多边形的各个顶点的法线。
顶点法线有什么?:
Direct3D需要知道顶点的法线方向,以确定光线到达表面时的射入角。而且,由于光照计算是对每个顶点进行的,所以 Direct3D 需要知道表面在每个顶点处的局部朝向(法线方向)。
请注意:顶点法线与表面法线不一定相同。近似的球体或圆是说明顶点法线和三角形面片法线不一致的很好的例子。
顶点法线表示:
为了描述一个顶点的顶点法线,我们必须修改顶点的结构:
struct Vertex{
float x, y, z;
float _nx, _ny, _nz;
static const DWORD FVF;
};
const DWORD Vertex::FVF = D3DFVF_XYZ | D3DFVF_NORMAL;
注意:上述的结构中,没有表示颜色的成员变量。这是因为我们将用光照来计算顶点的颜色。
计算面法线:
对于简单的物体,如立方体和球体,可以通过观察得到顶点的法线。
对于复杂的网格面,我们需要一种机械化的方法:
假定一个三角形由顶点p0, p1, p2 构成,现在我们来计算每个顶点的顶点法相量 n0, n1, n1。
最简单的做法是求出这3个顶点构成的三角形的面法线向量,并将该面法线向量作为各顶点的法向量。
首先计算位于三角形平面内的两个向量。
p1 - p0 = u
p2 - p0 = v
则面法线为:
n = u * v
由于每个顶点的法向量与面的法向量相同,所以:
n0 = n1 = n2 = n
下面是一个用于计算一个由3个顶点构成的三角形所在面的法向量的 C语言 函数。
注意:该函数中假定顶点的指定顺序为顺时针绕序。如果顶点的指定顺序为逆时针,则计算出的法向量将指向反方向。
void ComputeNormal(D3DXVECTOR3* p0, D3DXVECTOR3* p1, D3DXVECTOR3* p2, D3DXVECTOR3* out)
{
D3DXVECTOR3 u = *p1 - *p0;
D3DXVECTOR3 v = *p2 = *p0;
D3DXVec3Cross(cout, &u, &v);
D3DXVec3Normalize(out, out);
}
计算顶点法线:
当用三角形单元逼近表示曲面时,将面片法线向量作为构成该面片的顶点法向量不可能产生很平滑的效果,一种更好的求取顶点法向量的方法是计算法向量均值,为了求出顶点 V 的顶点法向量 Vn,我们需要求出共享顶点 V 的所有三角形的面法向量。则 V 的法向量可由这些面法向量的均值取得。假定有3个三角形单元共享顶点 V,其面法向量分别为 n0,n1,n2,则 V 的法向量计算方法为:
Vn = 1/3(n0+n1+n2)
在变换过程中,顶点法线有可能不再是规范化的,所以最好的方法是在变换完成后,通过设置 D3DRS_NORMALIZENORMALS 绘制状态使所有的法向量重新规范化。
光源:
Direct3D 支持3种类型的光源
- 点光源: 该光源在世界坐标系中有固定的位置,并向所有的方向发射光线,如同太阳。
- 方向光: 该光源没有位置信息,所发射的光线相互平行的沿某一特定方向传播。
- 聚光灯: 该光源有位置信息,其发射的光线呈锥形沿着特定方向传播,该锥形可用两个角度表示:其一描述了内部锥形,其二描述来外部锥形,如同手电筒。
光源的表示:
在程序代码中,光源用结构 D3DLIGHT9 来表示:
typedef struct _D3DLIGHT9 {
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;
- Type: 定义来我们所要创建的光源类型。该参数可取以下3种枚举值:D3DLIGHT_POINT, D3DLIGHT_SPOT, D3DLIGHT_DIRECTIONAL。
- Diffuse: 该光源所发出的漫射光的颜色
- Specular: 该光源所发出的镜面光的颜色
- Ambient: 该光源所发出的环境光的颜色
- Position: 用于描述光源在世界坐标系中的位置的向量。对于方向光,该参数无意义
- Direction: 一个描述光在世界坐标系中传播方向的向量,对于点光源,该参数无意义
- Range: 光线消亡前,所能达到的最大光程。该值的最大取值为 FLT_MAX 的平方根。对于方向光,该参数无意义
- Falloff: 该值仅用于聚光灯。该参数定义来光强从内锥形到外锥形的衰减方式。该值一般取值为1.0f
- **Attenuation0,Attenuation1,Attenuation2:**这些衰减变量定义来光强随距离衰减的方式,这些变量仅用于点光源和聚光灯。Attenuation0,Attenuation1,Attenuation2 分别表示光的 常量,线性,2次距离衰减系数,衰减公式为:
attenuation = 1/(Attenuation0 + Attenuation1 * D + Attenuation2 * D*D) 其中D为光源到顶点的距离。 - Theta: 仅用于聚光灯,指定了内部锥形的圆锥角,单位为弧度
- Phi: 仅用于聚光灯,指定了外部锥形的圆锥度,单位为弧度
与 D3DMATERIAL9 结构的初始化类似,当需要一个较简单的光源时,D3DLIGHT9 结构的初始化也很繁琐。
为此,我们可在代码中添加如下函数用来初始化简单的光源:
namespace d3d{
D3DLIGHT9 InitDirectionalLight(D3DXVECTOR3* direction, D3DXCOLOR* color);
D3DLIGHT9 InitPointLight(D3DXVECTOR3* position, D3DXCOLOR* color);
D3DLIGHT9 InitSpotLight(D3DXVECTOR3* position, D3DXVECTOR3* direction, D3DXCOLOR* color);
}
这些函数实现简单,下面仅列出 InitDirectionalLight 函数的实现,其他函数的实现方法类似:
D3DLIGHT9 d3d::InitDirectionalLight(D3DXVECTOR3* direction, D3DXCOLOR* color)
{
D3DLIGHT9 light;
::ZeroMemory(&light, sizeof(light));
light.Type = D3DLIGHT_DIRECTIONAL;
light.Ambient = *color * 0.4f;
light.Diffuse = *color;
light.Specular = *color * 0.6f;
light.Direction = *direction;
return light;
}
接下来在创建一个传播方向平行于 x 轴的方向光光源,我们可以这样写:
D3DXVECTOR3 dir(1.0f, 0.0f, 0.0f);
D3DXVECTOR3 c = d3d::WHITE;
D3DLIGHT9 dirLight = d3d::InitDirectionalLight(&dir, &c);
当 D3DLIGHT9 实例初始化完毕之后,我们需要在 Direct3D 所维护的一个内部光源列表中对所要使用的光源进行注册:
Device->SetLight(
0, //要设置的灯光列表中的元素,范围是0-maxlights
&light //要设置的d3dlight9结构的地址
);
一旦光源注册成功,我们就可对其开关状态进行控制,如下列所示:
Device->LightEnable(
0; //要启用/禁用的灯光列表中的元素
true //true=开,false=关
);