转自http://blog.csdn.net/soilwork/archive/2007/01/14/1482661.aspx
The Complete Effect and HLSL Guide(十二)
我们已经有了个一个漫反射点光源着色器,现在应该考虑光照方程中的高光部分了。和漫反射相比,镜面高光最大的区别就是光照亮度不但与光线到表面的角度有关,还和观察者的角度有关。
为了计算高光,我们需要一个称为中间(half)矢量的值。这个矢量其实是观察矢量和灯光矢量的中间值。为了计算观察矢量需要把观察点变换到模型空间。我们假设观察点的位置位于( 0,0,10)。把它变换到模型空间之后,加上顶点位置,就是最终的观察矢量:
EyeVector = -normal(mul ( inv_view_matrix, float4(0,0,10,1) + inPos);
把EyeVector与光源矢量混合到一起,然后进行标准化,就是中间矢量。由于EyeVector和LightDir都是标准矢量,所以中间矢量相当于二者的均值(A + B)/ 2。
HalfVect = normalize ( LightDir – EyeVetor);
(译注:更直观的做法是在世界坐标下计算EyeVector,然后用LightDir + EyeVetor计算HalfVect)
有了中间矢量,把它和表面法线的点积进行m次幂运算,就能算出光源基于角度的光线强度。幂运算时的指数相当于物体的镜面指数,值越大,高光区域就越小也越明亮。下面的代码假设镜面指数为32。
struct VS_OUTPUT
{
float4 Pos: POSITION;
float2 texCoord: TEXCOORD0;
float2 Color: COLOR;
};
float4 Light_PointSpecular( float3 VertPos, float3 VertNorm,float3 LightPos,
float4 LightColor, float4 Lightattenuation, float3 EyeDir)
{
//Determine the Distance from the light to the vertex and the direction
float3 LightDir = LightPos - VertPos;
float Dis = length(lightDir);
LightDir = LightDir / Dist;
//Computer half vector
float3 HalfVect = normalize( LightDir = EyeDir);
//Compute distance based attenuation. this is defined as:
//Attenuation = 1/(LA.x + LA.y * Dist + LA * Dist * Dist)
float DistAttn = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist +
LightAttenuation.z * Dist * Dist));
float SpecularAttn = pow(saturate ( dot(VertNorm,HalfVect)),32);
//Compute finfal lighting
return LightColor * DistAttn * SpecularAttn;
}
VS_OUTPUT vs_main( float4 inPos:POSITION,float3 inNormal: NORMAL ,float2 inTxr:TEXCOORD0)
{
VS_OUTPUT Out;
//compute the projected positon and send out the texture coordinates
Out.Pos = mul(View_proj_matrix,inPos);
Out.TexCoord = inTxr;
//Output the ambient Color
float4 Color = Light_Ambient;
//Determine the eye vector
float3 EyeVector = -normlaize(mul(inv_view_matrix,float4(0,0,10,1)) + inPos);
//computer the light contribution
Color = Light_PointSpecular(inPos,inNormal,Light1_Position,Light1_Color,Lihgt_Attenuation,
EyeVector);
//Output Final Color
Out.Color = Color;
return Out
}
当考虑光照时,大部分人都认为逐顶点的光照足够好了。对于镶嵌度较高的模型来说是这样,但对某些复杂或的精度模型来说却不一定。出现这种效果的原因是顶点间颜色插值的方式。
当逐顶点照亮对象时,将为每个顶点计算一次光照颜色,然后在通过顶点在多边形所覆盖的区域对像素颜色进行线形插值。现实中,光照值取决于光线角度,表面法线,和观察点(对于镜面高光来说)。与逐像素对所有光照元素进行单独插值,再计算光照相比,直接对顶点颜色进行插值所得的结果通常不够精确,特别是对面积较大的多边形来说。
(左图为per-pixel lighting,右图为per-vertex)
上图显示了逐像素和逐顶点光照的差别。当处理高精度多边形模型时,由于每个多边形所覆盖的区域很小,因此插值之后每个像素的误差也很小,所以逐顶点光照可以工作的很好。而当处理低模时,这种误差就变的很大了。
使用逐像素光照的另一个好处是可以在渲染时添加并不存在的表面细节。通过bump map或normal map,可以在像素级别让原本平坦的表面表现出近似的凹凸效果。
再回过头来看看漫反射光线,你需要决定哪些光照元素可以插值之后传送到像素着色器,哪些元素必须逐像素计算。从头到尾,决定漫反射光照的就是表面法线和光线矢量的点积。
这个点积定义了光照的强度,但是对它进行插值的结果通常不正确。因此这一步计算应该移动到像素着色器中,进行逐像素计算。
表面法线和光矢量是计算点积的要素。通常矢量间的插值是正确的。因此可以在顶点着色器计算他们的值,并传递到像素着色器中,然后进行最终的点积计算。
注意
虽然对法线的插值是正确的,但是插值之后的向量将不再是标准向量。为了对此进行校正,需要在像素着色器中对法线重新进行标准化,可以使用内建的normalize函数。
为了把光照计算移动到像素着色器中,需要先添加两个变量到顶点着色器的输出结构中。可以使用TEXCOORD1和TEXCOORD2语义把这些值传递到像素着色器。代码如下:
struct VS_OUTPUT
{
float4 Pos : POSITION;
float2 TexCoord: TEXCOORD0;
float3 Normal : TEXCOORD1;
float3 LightDir: TEXCOORD2;
}
接下来修改顶点着色器代码。目前我们只考虑单光源光照的情况,所以先删除原先的光照函数把其中的代码复制到vs_main函数中。记住,我们后面回讨论多光源的情况。
你可能已经注意到我们还没有讨论基于距离的衰减值应该在哪里计算。虽然对距离的插值结果不是完全正确,但由于光线的变化有足够容差范围,距离上的微小差别并不会给结果带来明显变化,所以不需要逐像素计算。
衰减值只是一个标量,为了把它传递给像素着色器,而又不浪费一个额外的寄存器来传值,可以把LightDir改为一个float4的矢量,把衰减值作为这个矢量的w分量。修改后的顶点着色器如下:
VS_OUTPUT vs_main(float4 inPos:POSITION,float3 inNormal: NORMAL ,
float2 inTxr: TEXCOORD0)
{
VS_OUTPUT Out;
Out.Pos = mul( view_proj_matrix, inPos);
Out.TexCoord = inTxr;
Out.Normal = inNormal;
//Computer and move the light direction to the pixel shader
float4 LightDir;
LightDir.xyz = Light1_Position - inPosition;
float Dist = length(LightDir.xyz);
LightDir.xyz = LightDir.xyz / Dist;
LightDir.w = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist +
LightAttenuation.y * Dist*Dist));
//Output the light direction
Out.LightDir = lightDir;
return Out;
}
在像素着色器中,只需要接收参数,计算点击和颜色就可以了。为了方便,把光照计算单独放到一个名为Light_PointDiffuse的函数中。本质上来说,这里计算光照的方法和在顶点着色器中是一样的。
float4 Light_PointDiffuse(float4 LightDir, float3 Normal ,Float4 LightColor)
{
//compute dot product of N and L
float AngleAttn = saturate(dot( Normal ,LightDir.xyz));
//compute final lighting
return LightColor * LightDir.w * AngleAttn;
}
float4 ps_main(float3 inNormal:TEXCOORD1,
float4 inLightDir : TEXCOORD2) : COLOR
{
//Compter the lighting contribution for this single light
return Light_PointDiffuse(inLightDir,inNormal,Light_Color);
}
很简单,对吧!接下来编写镜面高光像素着色器。整个步骤基本上和我们刚才编写漫反射像素着色器的方法差不多。这一个shader的核心是法线和中间矢量的点积,需要把它们移动到像素着色器中。这意味着需要在顶点着色器中计算光照矢量,中间矢量。先修改顶点着色器的输出结构。
struct VS_OUTPUT
{
float4 pos: POSITION;
float2 TexCoord: TEXCOORD0;
float3 Normal : TEXCOORD1;
float4 LightDir: TEXCOORD2;
float3 HalfVect: TEXCOORD3;
}
在顶点着色器中,需要计算以下值:表面法线,光照矢量,中间矢量和基于距离的衰减值。只需要把先前编写的镜面高光着色器代码稍作修改就行了。
VS_OUTPUT vs_main(float4 inPos:POSITION,float3 inNormal: NORMAL ,
float2 inTxr: TEXCOORD0)
{
VS_OUT Out;
Out.Pos = mul(view_proj_matrix, inPos);
Out.TexCoord = inTxr;
float4 LightDir;
LightDir.xyz = Light1_Position - inPos;
flaot Dist = length(LightDir.xyz);
LightDir.xyz = LightDir.xyz / Dist;
LightDir.w = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist + LightAttenuation.y * Dist*Dist));
//computer eye vector and the half vector
float3 EyeVector = -normalize(mul(inv_view_matrix,float4(0,0,10,1))+inPos);
Out.HalfVect = normalize(LightDir - EyeVector);
//Output normal and light direction
Out.Normal = inNormal;
Out.LightDir = LightDir;
return Out;
}
对像素着色器来说,同样把计算高光的代码作为一个单独的函数:
float4 Light_PointSpecular(float3 Normal ,flaot3 HalfVect,float4 LightDir,float4 LightColor)
{
float SpecularAttn = pow(saturate(dot( Normal ,HalfVect)),32);
return LightColor * LightDir.w * SpecularAttn;
}
float4 ps_main(float3 inNormal:TEXCOORD1,float4 LightDir:TEXCOORD2,
float3 HalfVect:TEXCOORD3):COLOR
{
//simply route teh vertex color to the output
return Light_PointSpecular(inNormal,HalfVect,LightDir,Light1_Color);
}
~~~~~~~~未完待续~~~~~~~~~~~~~~~~