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