【D3D11游戏编程】学习笔记十二:光照模型
从这一篇开始,我们逐渐进入D3D11中有意思的部分。之前的场景绘制,要么为每个顶点指定单一的颜色,要么在线框模式下渲染。从现在起我们开始学习光照,这样场景就更加具有真实感了。 1. 法线的引入 1.1 顶点信息
在之前的绘图当中,每个顶点包含两个信息:位置坐标和颜色值。进入光照计算之后,我们不再需要顶点的颜色信息,而是新增了法线信息。即在光照模型中,一个顶点至少包含位置坐标和法线两种信息。给定一个顶点的坐标、法线、材质等信息,再通过光源进行计算而得出该顶点的颜色值。
1.2 法线的变换在对一个顶点进行空间变换时,它的法线也需要相应地进行变换,因此我们需要得到一个顶点所对应的法线的变换矩阵。注意法线与顶点不共享相同的变换!如下为一个直观的例子:
三个图中,n为V0、V1顶点所在表面对应的法线。图a为变换前状态,图b为经过x轴方向的2倍伸缩变换(scaling)之后的状态,nA为经过同一个变换后的新法线。显然,这时法线与表面不垂直,因此是不正确的!正确的情形当该是图c所示。
实际上,对于一个顶点的坐标变换A,其对应的法线的正确变换是A的逆矩阵的转置,即。
2. 环境光、漫反射光与全反射光
在3D计算机图形学,对光照计算的处理分为三个部分:环境光、漫反射光和全反射光(或称为高光)。
2.1 环境光(Ambient light)在现实当中,光照是一个很复杂的物理现象。一个物体所接受的光,除了直接来自光源的部分外,还包括光源经过环境中其他各个物体的反射而来的部分。而在图形学中,我们默认的光照模型为局部光模型,即一个顶点的光照计算只跟该点信息与光源信息有关,而不考虑环境中其他物体的影响,比如阴影等。与局部光照模型相对应的全局光照,这属于高级话题,这里暂时不考虑。为了近似地模拟现实当中来自周围环境的光,在图形学中引入的“环境光”这一概念,即“Ambient Light"。
环境光不需要进行特殊的物理计算,即直接将光源中的环境光部分与材质中的环境光部分相乘,其结果适用于物体上的任一顶点。
2.2 漫反射光(Diffuse light)光照射在物体表面后,其反射光沿随机方向均匀的分布,即"漫反射”。反射光的强度与光照方向与表面法线的夹角theta相关,满足比例关系:I = Io * cos(theta)。由于反射光方向随机,因此该部分的计算与观察点无关,而只与光线方向与法线相关。
2.3 全反射光(Specular light)光线照射在光滑物体表面后,在特定方向上会有很强的反射,即发生全反射。全反射光主要集中在一个近似圆锥角的范围内。如下图所示:
n为法线,l为光线入射方向,r为全反射方向,E为观察点,因此v为视角方向。全反射光进入眼睛的强度与v和r的角度theta有关,随着该角度增大,全反射强度下降,其下降辐度与物体表面光滑程序相关。因此,对于该部分光的计算,除了需要光线方向、法线等信息外,还与观察点的位置有很大关系。具体计算公式在本文后面会详细给出。
3. 材质
为了表示物体与照射在其表面的光的交互作用,我们需要定义其材质。与光源的三个成分相对应,我们对材质了指定相应的环境光部分、漫反射光部分和全反射光部分,这些属性分别代表光的每一部分在其表面的反射比例。此外,还需要指定物体表面的光滑程度,以用于计算全反射。该值越大,全反射光衰减越迅速。
4. 三种光源模型在学习光照计算前,需要先了解3D中常见的几种光源模型。主要分为三种,由简单到复杂分别为:平行光、点光源和聚光灯。
4.1 平行光平行光是最简单的一种模型,这种光照具有单一的照射方向,且光照强度不随空间位置而变化。现实当中的太阳光就可以认为是这种类型。
4.2 点光源一个具有点光源特性的典型例子是电灯泡。首先该光源在空间具有一个位置,其次它发出的光以球面形式向四周均匀的传播(尽管实际的电灯光在各个方向上并不均匀,我们此外姑且可以这样理解。)。还有一个重要的特性即光强随着与光源的距离的增大而逐渐减小。理论上光强与距离的平方成反比,即I(d) = I0/(d2)。因此在无穷远处光强接近为0;在光源所在处,光强为无穷大。这样显然不适合在计算机中进行处理。于是在3D图形学中,我们对点光源模型有如下定义:用三个系数A0、A1、A2来控制光强随距离的衰减,分别为常量系数、一次系数和二次系数,这样光强计算公式为:I = I0/(A0+A1*d+A2*d2)。其次,对光照范围有一个限制,超过特定范围后,光照强度定义为0。
4.3 聚光灯与聚光灯最为接近的现实模型为手电筒。该光源在空间具有一个位置,其次还有一个照射方向,以该方向为中心对称地向周围发散一定的角度,这样光线被限制在一个圆锥内,如下图所示:
我们称这个最大的发散角为theta。给定光源位置与照射点位置,从光源到顶点的射线与光源照射方向的夹角如果位于最大发散角之内,则进行光照计算,否则该点不进行计算。与点光源一样,聚光灯光强也随着的距离的增大而减小,衰减方式完全一样。
5. 模型定义
下面我们通过C++程序来实现上述三种光源及材质的定义:
5.1 平行光首先是光源的三种成分,分别用一个4D的向量来表示,其次是光照的方向,为3D向量。定义如下:
struct DirLight{XMFLOAT4ambient;//环境光XMFLOAT4diffuse;//漫反射光XMFLOAT4specular;//高光XMFLOAT3dir;//光照方向floatunused;//用于与HLSL中"4D向量"对齐规则匹配};
注意最后一个float类型成员,该成员无任何爱得用途,它只是用于实现“4D向量对齐“。后面会解释。
5.2 点光源首先也是三种成分,其次是光源所在位置及其照射范围,最后是光强的衰减系数(A0、A1、A2)。定义如下:
struct PointLight{XMFLOAT4ambient;//环境光XMFLOAT4diffuse;//漫反射光XMFLOAT4specular;//高光XMFLOAT3pos;//光源位置floatrange;//光照范围XMFLOAT3att;//衰减系数floatunused;//用于与HLSL中"4D向量"对齐规则匹配};
同样,最后一个成员只用于对齐。
5.3 聚光灯首先是三种成分,其次同点光源一样,所在位置及其照射范围、衰减系数,聚光灯特有的还有其最大发散角及发散相关的系数。定义如下:
struct SpotLight{XMFLOAT4ambient;//环境光XMFLOAT4diffuse;//漫反射光XMFLOAT4specular;//高光XMFLOAT3dir;//光照方向floatrange;//光照范围XMFLOAT3pos;//光源位置floatspot;//聚光强度系数XMFLOAT3att;//衰减系数floattheta;//最大发散角度};
通过排列成员的次序,该结构正好能够满足”4D向量对齐“,因此不需要额外的成员。
除了C++程序中定义光源模型,在Effect程序中也需要进行完全匹配的定义。注意是”完全匹配“,这样在C++程序中,我们就可以将相应的变量直接赋给Effect程序中相应的光源变量。HLSL中三种光源定义如下:
//平行光struct DirLight{float4ambient;//环境光float4diffuse;//漫反射光float4specular;//高光float3dir;//方向floatunused;//“4D向量”对齐用};//点光源struct PointLight{float4ambient;//环境光float4diffuse;//漫反射光float4specular;//高光float3pos;//光源位置floatrange;//光源照射范围float3att;//光强衰减系数floatunused;//"4D向量"对齐用};//聚光灯struct SpotLight{float4ambient;//环境光float4diffuse;//漫反射光float4specular;//高光float3dir;//方向floatrange;//照射范围float3pos;//位置floatspot;//聚光强度系数float3att;//误差系数floattheta;//最大发散角度};5.4 材质
对于材质,同样是三种成分,此外还有一个表面光滑程度的系数,定义如下:
struct Material{XMFLOAT4ambient;XMFLOAT4diffuse;XMFLOAT4specular;//第4个元素为材质的镜面反射系数,即代表材质表面的光滑程度};
在该结构中,为了不使用额外的成员来满足对齐,我们把表面光滑程度的系数放到了全反射光部分的第4个成分当中。因为对于材质来说,全反射部分不需要相关的透明度信息。一般使用漫反射光的透明度作为该材质的透明度。 同样HLSL中定义如下:
struct Material{float4ambient;float4diffuse;float4specular;//specular中第4个元素代表材质的表面光滑程度};
定义好模型结构后,下面是最重要的光照计算了。这部分在HLSL中实现。
针对光源的三种成分,我们在计算时也同样针对各个成分进行计算。计算很简单,这里直接给出代码:
6.1 平行光void ComputeDirLight(Material mat,//材质DirLight dirLight,//平行光float3 normal,//顶点法线float3 toEye,//"顶点->眼"向量out float4 ambient,//计算结果:环境光部分out float4 diffuse,//计算结果:漫反射部分out float4 specular)//计算结果:高光部分{//结果首先清零ambient = float4(0.0f,0.0f,0.f,0.f);diffuse = float4(0.f,0.f,0.f,0.f);specular = float4(0.f,0.f,0.f,0.f);//环境光直接计算ambient = mat.ambient * dirLight.ambient;//计算漫反射系数//注意:计算前保证法线、光线方向归一化float diffFactor = -dot(normal,dirLight.dir);//如果系数小于0(即顶点背着光源),则不再进行计算[flatten]if(diffFactor > 0){//计算漫反射光diffuse = mat.diffuse * dirLight.diffuse * diffFactor;float3 refLight = reflect(dirLight.dir,normal);float specFactor = pow(max(dot(refLight,toEye),0.f),mat.specular.w);specular = mat.specular * dirLight.specular * specFactor;}}
void ComputePointLight(Material mat,//材质PointLight pLight,//点光源float3 normal,//法线float3 position,//顶点位置float3 toEye,//"顶点->眼"向量out float4 ambient,//计算结果:环境光部分out float4 diffuse,//计算结果:漫反射部分out float4 specular)//计算结果:高光部分{//结果首先清零ambient = float4(0.f,0.f,0.f,0.f);diffuse = float4(0.f,0.f,0.f,0.f);specular = float4(0.f,0.f,0.f,0.f);//计算光照方向:顶点->光源float3 dir = pLight.pos - position;//计算顶点到光源距离float dist = length(dir);//超过照射范围,则不再进行计算if(dist > pLight.range)return;//归一化光线方向dir /= dist;//计算光强的衰减float att = 1/(pLight.att.x + pLight.att.y*dist + pLight.att.z*dist*dist);//计算环境光ambient = mat.ambient * pLight.ambient * att;//计算漫反射系数float diffFactor = dot(dir,normal);//如果小于0,直接退出if(diffFactor > 0){//计算漫反射光diffuse = mat.diffuse * pLight.diffuse * diffFactor * att;float3 refLight = reflect(-dir,normal);//计算高光系数float specFactor = pow(max(dot(refLight,toEye),0.f),mat.specular.w);//计算高光specular = mat.specular * pLight.specular * specFactor * att;}}
void ComputeSpotLight(Material mat,//材质SpotLight L,//聚光灯float3 normal,//法线float3 position,//顶点位置float3 toEye,//"顶点->眼"向量out float4 ambient,//计算结果:环境光部分out float4 diffuse,//计算结果:漫反射部分out float4 specular)//计算结果:高光部分{//结果首先清零ambient = float4(0.f,0.f,0.f,0.f);diffuse = float4(0.f,0.f,0.f,0.f);specular = float4(0.f,0.f,0.f,0.f);//计算光照方向:顶点->光源float3 dir = L.pos - position;//计算顶点到光源距离float dist = length(dir);//如果距离大于光照范围,则不再进行计算if(dist > L.range)return;//归一化光线方向dir /= dist;//计算衰减系数float att = 1/(L.att.x + L.att.y*dist + L.att.z*dist*dist);//计算聚光衰减系数float tmp = -dot(dir,L.dir);if(tmp < cos(L.theta))return;float spotFactor = pow(max(tmp,0.f),L.spot);//计算环境光ambient = mat.ambient * L.ambient * att * spotFactor;//计算漫反射系数float diffFactor = dot(dir,normal);//如果小于0,直接退出if(diffFactor > 0){//计算漫反射光diffuse = mat.diffuse * L.diffuse * diffFactor * att * spotFactor;float3 refLight = reflect(-dir,normal);//计算高光系数float specFactor = pow(max(dot(refLight,toEye),0.f),mat.specular.w);//计算高光specular = mat.specular * L.specular * specFactor * att * spotFactor;}}
7. 程序示例
最后是该节的示例程序,场景与上节中一样,只是这次不再是线框模型,而是光照下的场景。截图如下:
源代码如下:
操作方法:鼠标左键按下拖动旋转场景,右键按下拖动调整镜头的远近。
光照计算示例程序