粗糙的物体表面向各个方向等强度地反射光, 这种等同地向各个方向散射的现象称为光的漫反射(diffuse reflection)。
漫反射光照符合Lambert余弦定律,编程中通常采用它的简化形式,即:漫反射光的光强与入射光反方向L和顶点表面法向量N夹角的余弦成正比,例如当入射光反方向L垂直于顶点时,即与顶点法线N夹角为0度时,余弦值为1,此时有最大光强;若入射光反方向L平行于顶点所在平面,即与法线N夹角为90度,余弦值为0,没有光照。
如下图所示:
而余弦值可以通过对光线向量和法线向量进行点乘得到,即:
cosθ=N•L
我将处理过程放在了Light.inc中的LambertianComponent函数中,这样可以重用这个方法。代码如下:
01 |
//----------------------------------------------------------------------------------------------- |
02 |
// 函数: Lambertian因子 |
03 |
// |
04 |
// 作者: E. Riba (Riki) 07.08.2009 |
05 |
// |
06 |
// 描述: 基本的lambertian光照方程 |
07 |
// |
08 |
//----------------------------------------------------------------------------------------------- |
09 |
float3 LambertianComponent( in float3 directionToLight, in float3 worldNormal, in float3 lightColor) |
10 |
{ |
11 |
float3 NdotL = saturate(dot(directionToLight, worldNormal)); |
12 |
return NdotL * lightColor; |
13 |
} |
你也可以参考XNA Shader编程教程2-漫反射光照。
要模拟光滑表面,还需要添加镜面高光反射的颜色。1975年,Phong Bui Tuong提出一个计算镜面反射光强的经验模型,称为Phone光照模型,在XNA Shader编程教程3-镜面反射光照和6.9 添加HLSL镜面高光中使用的就是Phone光照模型。 镜面反射的光强与反射光线和视线反方向的夹角相关,其中的强度因子的数学表达为:
(V•R)n
n 是高光指数,V表示从顶点到相机的观察方向,R代表反射光方向,V•R即cosθ,如下图所示:
即V越靠近R,镜面反射强度越大。
高光指数n反映了物体表面的光泽程度。n越大,反射光越集中,当偏离反射方向时,光线衰减的越厉害,只有当视线方向与反射光线方向非常接近时才能看到镜面反射的高光现象,此时,镜面反射光将会在反射方向附近形成亮且小的光斑; n 越小,表示物体越粗糙,反射光分散,强度弱,下图就是在我的引擎中n取不同大小的效果,左图n取10,右图取50。
反射光的方向R可以通过入射光反方向L(从顶点指向光源)和法向量N求出,公式为:
R=2(N•L)N-L
以上公式可用下图表示:
但因为HLSL已经内置了计算反射向量的指令reflect,所以通常不用上面的公式,而是使用以下公式计算反射向量:
R=reflect(-L,N)
注意:公式中要求代入的是入射光方向,所以应使用-L。
对应引擎中的代码位于Light.inc中:
01 |
//----------------------------------------------------------------------------------------------- |
02 |
// 函数: Specular分量 |
03 |
// |
04 |
// 作者: E. Riba (Riki) 08.08.2009 |
05 |
// |
06 |
// 描述: 基于Phong公式的镜面高光公式 |
07 |
// |
08 |
//----------------------------------------------------------------------------------------------- |
09 |
float3 SpecularComponent( in float3 directionToLight, in float3 worldNormal, in float3 worldPosition, in float3 lightColor, in float3 specularColor, in float specularPower) |
10 |
{ |
11 |
float3 specular = 0; |
12 |
if (length(specularColor)>0) |
13 |
{ |
14 |
// 计算反射光方向 |
15 |
float3 reflectionVector = normalize(reflect(-directionToLight, worldNormal)); |
16 |
|
17 |
// 计算像素指向相机的向量,即视线反方向 |
18 |
float3 directionToCamera = normalize(gCameraPos - worldPosition); |
19 |
|
20 |
// 计算视线反方向和反射光方向的夹角 |
21 |
float VdotR = saturate(dot(directionToCamera,reflectionVector)); |
22 |
|
23 |
// 计算镜面高光颜色 |
24 |
specular = lightColor * specularColor *pow(VdotR,specularPower); |
25 |
specular = saturate(specular); |
26 |
} |
27 |
return specular; |
28 |
} |
注意:计算机图形学上还有一个名称叫做Phone着色模型,不要和镜面反射的Phone光照模型混淆,Phone着色模型实际上就是逐像素着色,与它对应的是逐顶点着色,又称为Gourand(高洛德)着色,比逐顶点更低级的着色是Flat着色,可参见XNA Shader编程教程3.2-逐顶点和逐像素光照。
另一种模型叫做Blinn-Phong光照模型,是由 Jim Blinn于 1977年对传统 phong光照模型基础上进行修改提出的。和传统phong光照模型相比,Blinn-phong光照模型的渲染效果有时比 Phong 高光更柔和、更平滑,此外它在速度上相当快,因此成为许多 CG软件中的默认光照渲染方法。它也集成在了大多数图形芯片中,用以产生实时快速的渲染。在OpenGL和Direct3D渲染管线中,Blinn-Phong就是默认的渲染模型。
在§6.1Shader教程中用的就是Blinn-Phong光照模型。
Blinn-phong光照模型中,用N•H的值取代了V•R。Blinn-phong光照模型的光强因子为:
(N•H)n
其中n 是高光指数,N 是像素的法向量,H 是“光入射反方向L和视线反方向V 的角平分线向量”,通常也称之为半角向量,计算半角向量只需简单的V+L即可,是一件简单、耗时不多的工作。如下图所示,N•H就是图中的cosθ:
即H 越靠近N,光照越强。
通常情况下,使用 Blinn-phong光照模型渲染的效果和 phong模型渲染的效果没有太大的区别,有些艺术工作者认为phong光照模型比 blinn-phong更加真实,实际上也是如此。Blinn-phong渲染效果要更加柔和一些,而Blinn-phong光照模型省去了计算反射光线方向的乘法运算,速度更快。下图是我的引擎中使用Blinn-phong光照模型和phong光照模型的对比,镜面高光强度n都是50,左图是Blinn-phong光照模型,右图是phong光照模型,我也觉得右图phong光照模型漂亮点,大概是场景不是很复杂的缘故,帧频两者差不多,理论上应该Blinn-phong光照模型快点:
对应引擎中的代码位于Light.inc中:
01 |
//----------------------------------------------------------------------------------------------- |
02 |
// 函数: BlinnPhongSpecular |
03 |
// |
04 |
// 作者: E. Riba (Riki) 12.08.2009 |
05 |
// |
06 |
// 描述: 基于Bilnn Phong公式的镜面高光公式 |
07 |
// |
08 |
//----------------------------------------------------------------------------------------------- |
09 |
float3 BlinnPhongSpecular( in float3 directionToLight, in float3 worldNormal, in float3 worldPosition, in float3 lightColor, in float3 specularColor, in float3 specularPower) |
10 |
{ |
11 |
float3 specular = 0; |
12 |
if (length(specularColor)>0) |
13 |
{ |
14 |
//计算像素指向相机的向量,即视线反方向 |
15 |
float3 viewer = normalize(gCameraPos - worldPosition); |
16 |
|
17 |
// 计算入射光反方向和视线反方向之间的角平分线方向 |
18 |
float3 half_vector = normalize(directionToLight + viewer); |
19 |
|
20 |
// 计算角平分线与法线方向的夹角 |
21 |
float NdotH = saturate(dot( worldNormal, half_vector)); |
22 |
|
23 |
// 计算镜面高光颜色 |
24 |
specular = lightColor * specularColor * pow( NdotH, specularPower) ; |
25 |
specular = saturate(specular); |
26 |
} |
27 |
return specular; |
28 |
} |
以下点光源的示意图片来自于DirectX SDK:
点光源没有方向只有位置,但是在真实情况中,光照强度有衰减,物理上光强衰减值与离开光源的距离平方成反比。在DirectX中使用的是如下公式:
Atten = 1/(att0i + att1i * d + att2i * d2)
其中att0i ,att1i和att2i分别为随距离衰减的常数因子,随距离衰减的线性因子和随距离衰减的二次因子,它们定义在了DirectX中的光源结构D3DLIGHT9中,而d表示像素与光源的距离。
例如att0i取0,att1i取1,att2i取1,则在距离光源5个单位的地方光照强度衰减为初始的0.2倍。如果你想实现DirectX中的衰减方程,可以在StunEngine引擎的Light类中添加三个变量att0i ,att1i和att2i,对应effect中一个float3类型的变量Attenuation的三个分量,要计算然后在effect文件中添加以下代码:
1 |
float att = dot(float3(1.0f,lDistance, lDistance* lDistance) , float Attenuation); |
att结果就是att0i + att1i * d + att2i * d2,这是因为点乘dot的数学本质就是对应分量乘积的和。
而在我的引擎中使用的是一个简化形式:
Atten =((range – d)/range )falloff
其中range为光照作用范围,超出此距离光照强度为0,d为像素离开光源的距离,如下图所示,falloff为衰减指数。例如,falloff为0,则Atten总为1,即不衰减;Range为10, falloff为1,则d为5(即离开最大距离一半)处的像素获得的光照为初始值的一半,这种情况就是线性衰减;若falloff为2,则只有初始值的0.25倍,衰减得比线性快,可以想象如果falloff指数小于1,则衰减得比线性慢。
对应引擎中的代码位于Light.inc中:
01 |
//----------------------------------------------------------------------------------------------- |
02 |
// 函数: Attenuation |
03 |
// |
04 |
// 作者: E. Riba (Riki) 12.08.2009 |
05 |
// |
06 |
// 描述: 计算随距离增大而减小的衰减值,例如lRange为10,falloff为1,则距离光源为5的顶点的光照只有一半 |
07 |
// |
08 |
//----------------------------------------------------------------------------------------------- |
09 |
float Attenuation( float lDistance, float lRange, float falloff) |
10 |
{ |
11 |
float att = pow(saturate((lRange - lDistance) / lRange), falloff); |
12 |
// 以下为简化计算方程 |
13 |
//float att = 1 - saturate(lDistance*falloff / lRange); |
14 |
return att; |
15 |
} |
在代码中你还可以看到我注释了更加简单的计算方程:
1 |
float att = 1 - saturate(lDistance*falloff / lRange); |
例如lDistance为10,falloff取1,lDistance为5的像素的光照强度为初始值的一半;如果falloff取2,则光照强度为0,由此可知此简化方程相对于上面的方程衰减得更快。使用这个方程,可以不进行指数运算,速度会快一点。
以下聚光灯的示意图片来自于DirectX SDK:
上图圆锥表示聚光灯光照范围,这个锥体由两个区域组成:内部区域InnerCone,物理概念通常称为本影,对应内光锥顶角Theta;外部区域OuterCone,物理概念通常称为半影,对应外光锥角度Phi。 聚光灯光强除了有距离衰减外,还需要处理角度衰减,即:在本影内,光照是恒定的,但过渡到半影中后急剧衰减。
为了简化角度比较,使用两个半角:θ=Theta/2,φ=Phi/2,在我的引擎中theta和phi两个参数其实指的就是这两个半角,我觉得这样做比较符合使用习惯。
如下图所示:
图中α为光源指向像素的向量和聚光灯方向向量的夹角,这个夹角的余弦值可通过点乘这两个向量获得。聚光灯光照角度衰减因子的公式如下:
1.若α>φ,即位于半影之外,根本照不到,此时角度衰减因子为0。
2.若α<θ,即位于本影内,则只有距离衰减,角度衰减因子为1。
3.若θ<α<φ,即位于半影内,则角度衰减因子为:
((cosα-cosφ)/(cosθ-cosφ))falloff
例如内光锥角度θ为30度,外光锥角度φ为60度,falloff取1,那么位于α=30度方向处像素的角度衰减因子为1,即不衰减;α=60度时角度衰减因子为0,没有光照;α=45度时角度衰减因子为0.566。以上公式来自于DirectX SDK,可参见Light Types (Direct3D 9)。
具体代码位于Light.inc的CalculateSingleLight函数中:
01 |
//如果是聚光灯光源,则首先判断顶点是否在光照范围之内 |
02 |
else if ( lightType == SPOT_LIGHT) |
03 |
{ |
04 |
//如果在光照范围之内 |
05 |
if (lightDist < range) |
06 |
{ |
07 |
//首先计算距离衰减 |
08 |
float fOuterAtten = Attenuation(lightDist, range, falloff); |
09 |
|
10 |
// 计算当前光源指向顶点方向和聚光灯光线方向的夹角Alpha的余弦值 |
11 |
float cosAlpha = saturate(dot(-directionToLight, normalize(light.direction))); |
12 |
|
13 |
// 计算聚光灯内外光锥间的衰减因子 |
14 |
float fSpotAtten = 0.0f; |
15 |
//如果夹角Alpha小于内光锥顶角,即cosAlpha大于cosTheta,则不衰减,衰减因子为1 |
16 |
if ( cosAlpha > cosTheta) |
17 |
fSpotAtten = 1.0f; |
18 |
//如果夹角Alpha大于内光锥顶角小于外光锥顶角,则需要根据falloff指数计算衰减因子 |
19 |
else if ( cosAlpha > cosPhi) |
20 |
fSpotAtten = pow((cosAlpha - cosPhi)/(cosTheta - cosPhi), falloff); |
21 |
//将距离衰减因子乘以光锥间衰减因子得到最终的衰减值 |
22 |
attenuation = fOuterAtten * fSpotAtten; |
23 |
} |
24 |
else |
25 |
{ |
26 |
attenuation = 0; |
27 |
} |
28 |
} |
要让计算更简单,可以将公式((cosα-cosφ)/(cosθ-cosφ))falloff简化成cosαfalloff,即不将锥体划分成本影和半影,从中心点就开始角度衰减,6.8 使用HLSL定义聚光灯就是这样做的。
雾化效果是模拟自然界的雾,近处物体可以被看到,远处物体被雾遮挡,雾化还可以提高渲染速度。
在DirectX和XNA中雾化主要有三种,对应FogMode枚举:线性雾化FogMode.Linear,指数雾化FogMode. Exponent和指数平方雾化FogMode. ExponentSquared。其中线性雾化的公式为:
f=(FogEnd -d)/(FogEnd-FogStart)
式中f为雾化强度,FogStart为雾化的起始距离,FogEnd为雾化结束距离,d为像素离开相机的距离。例如:FogStart为50,FogEnd为100,d为75时的雾化强度为0.5。本引擎只实现了线性雾化,具体代码位于standard.inc中的LinearFog函数中:
01 |
//----------------------------------------------------------------------------------------------- |
02 |
// 函数: LinearFog |
03 |
// |
04 |
// 作者: E. Riba (Riki) 19.02.2009 |
05 |
// |
06 |
// 描述: 计算线性雾化颜色 |
07 |
// |
08 |
// Shared参数: gFogStart,gFogEnd,gFogColor |
09 |
// |
10 |
//----------------------------------------------------------------------------------------------- |
11 |
float3 LinearFog(float3 color, float3 worldPosition) |
12 |
{ |
13 |
float d = length(worldPosition - gCameraPos); |
14 |
float l = saturate((d - gFogStart) / (gFogEnd - gFogStart)); |
15 |
color.rgb = lerp(color, gFogColor.rgb, l); |
16 |
return color; |
17 |
} |
程序截图如下: