原文地址:http://digierr.spaces.live.com/blog/cns!2B7007E9EC2AE37B!612.entry。
本教程基于教程4-法线映射。在实现一个点光源前你无需知道法线映射,所以如果你只想知道如何实现一个点光源,可以暂时无需理解教程4的知识。
这个教程只解释什么是点光源以及如何实现它,你可以在本教程使用的算法之后添加法线映射或其他你需要的shader。
只要你理解了教程1,2,3,这个教程不是很难。
点光源(Point lights,在某些渲染工具中也叫做Omni light)的光线是从3D空间中的一个点向四周发出的。
不像前面教程中的单向光,点光源有一个作用范围,光线传播一段距离后会衰减为零,这叫做衰减因子(attenuation factor):
attenuation=1-((x/r )2+(y/r )2+(z/r )2)
公式中的r为衰减距离,公式也可表达为:
attenuation=1-(L/r )•(L/r )
如果用1减去V1=L/r和V2=L/r的点乘,就能获得衰减因子。
在点光源光照范围之外的物体只有环境光颜色,所以最后的光照方程如下:
I = A + Diffuse*Specular*attenuation
使用上述光照方程会遇到的一个问题是:在漫反射和镜面高光计算时会有一些显示错误,当光线向量和视线向量方向相反时像素也会被照亮,而且,如果光源距物体表面过近,也会有显示错误。
解决上述问题的一个方法是使用自阴影(Self-Shadowing),它可以避免不应该被照亮的像素获得光照。
当物体被遮挡或不应该获得光照时,自阴影因子为零或接近于零,如果像素被照亮,则这个因子大于零。
那么如何实现呢?只需计算表面法线和光照方向的点乘即可:
S = saturate( N.L );
但是这里的临界值过低,所以将它乘以4才可以获得正确的临界值。
现在我们可以用S乘以漫反射和镜面高光反射的颜色,这样当S为零时颜色也为零!我们的新光照方程如下:
I = A + S*(Diffuse*Specular*attenuation)
这个方程可以进行优化,方法是只有当S大于零时才计算漫反射和镜面高光反射,否则颜色就为零:if( S > 0) { calculate.. }.
当使用法线映射时,我们是在切线空间中进行计算的。这个算法让我们可以使用光线向量的Z分量,因为切线空间中的这个Z分量就等于N•L. 所以有以下方程:
S = 4 * LightDirection.z;
首先我们需要声明光照范围:
1 |
float LightRange; |
然后在顶点着色器中计算光照,将衰减值放在光源w分量,光线方向放在L向量中:
1 |
// calculate distance to light in world space |
2 |
float3 L = vecLightPos - PosWorld; |
3 |
|
4 |
// Transform light to tangent space |
5 |
Out.Light.xyz = normalize(mul(worldToTangentSpace, L)); // L, light |
6 |
|
7 |
// Add range to the light, attenuation |
8 |
Out.Light.w = saturate( 1 - dot(L / LightRange, L / LightRange)); |
剩下的事情就是在光照方程中使用衰减值和施加自阴影,这是在像素着色器中进行处理的:
1 |
float Shadow = saturate(4.0 * LightDir.z); |
2 |
.... |
3 |
.... |
4 |
return 0.2 * Color + Shadow*((Color * D * LightColor + S*LightColor) * (L.w)); |
如你所见,点光源与前面我们使用的单向光源区别不大,当使用光照时,自阴影值也可以用于前面的示例。
除了需要设置光照范围和光源位置代替前面教程中的光线方向,XNA代码没有其他变化:
1 |
effect.Parameters[ "vecLightPos" ].SetValue(vLightPosition); |
2 |
effect.Parameters[ "LightRange" ].SetValue(100.0f); |
3 |
effect.Parameters[ "LightColor" ].SetValue(vLightColor); |