问题

如教程6-3所示,要获得最好的光照效果应该使用逐像素光照,特别是对那些由大三角形构成的曲线的情况中。你想使用自己的effect添加逐像素光照。

解决方案

前两个教程中,你在每个顶点中计算明暗值(shading value,也可以翻译成着色值)。三角形三个顶点的明暗值会进行线性以获取每个像素的明暗值。

在逐像素光照中,你想对三个顶点的法线进行插值以获取每个像素的法线,这样就可以基于每个像素的法线计算光照因子。但是,当从一个顶点到另一个顶点进行法线插值时结果是有缺陷的。如图6-9中的左图所示,图中的水平直线表示一个顶点包含法线的三角形。当你在像素上对这左右两个顶点的法线进行插值时,插值的法线总会沿着虚线。导致在三角形中间的法线法线是正确的,其他位置的法线会小于实际值,如图所示。

image

图6-9 从顶点到像素的线性插值是错误的

解决方法是在pixel shader中处理这个插过值的法线。因为它的方向是正确的,你可以归一化这个向量。这是因为每个像素的缩放因子是不同的,所以这一步需要在pixel shader中进行。

注意:要理解为什么将法线长度变为1,可参见教程6-1中的“归一化法线”一节。

当你想让光照强度只取决于法线和入射光夹角时,更小的法线导致更小的光照强度。

光照方向也会遇到同样的问题,如图6-9右图所示。插过值的光线方向的长度沿着虚线曲线,这会导致向量比实际的小。你仍需要归一化这个插过值的光线方向。

工作原理

和以往一样,你的项目必须从至少包含3D位置和法线的顶点中与显卡进行交互。你想让XNA代码可以设置World,View,Projection矩阵,光源的3D位置和环境光:

float4x4 xWorld; float4x4 xView;
float4x4 xProjection; 
float3 xLightPosition; 
float xAmbient; 

struct PPSVertexToPixel 
{
    float4 Position: POSITION; 
    float3 Normal: TEXCOORD0; 
    float3 LightDirection: TEXCOORD1; 
}; 

struct PPSPixelToFrame 
{
    float4 Color: COLOR0; 
};

如前所述,vertex shader会输出法线,这个法线已经进行了插值,光线方向也进行了插值。pixel shader只需计算每个像素最后的颜色。

注意:在单向光的简单例子中,光源的方向是XNA-to-HLSL变量,对顶点和像素来说都是不变的。所以vertex shader无需计算这个值。

Vertex Shader

vertex shader从顶点中接受法线,根据世界矩阵中的旋转值旋转这个法线(见教程6-5),并将它传递到pixel shader。 在vertex shader中还通过将顶点位置减去点光源的位置计算了光线方向(见教程6-5)。根据当前世界矩阵获取顶点的最终3D位置。

PPSVertexToPixel PPSVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0) 
{
    PPSVertexToPixel Output = (PPSVertexToPixel)0; 
    
    float4x4 preViewProjection = mul(xView, xProjection); 
    float4x4 preWorldViewProjection = mul(xWorld, preViewProjection);
    
    Output.Position = mul(inPos, preWorldViewProjection);
    float3 final3DPos = mul(inPos, xWorld); 
    Output.LightDirection = final3DPos - xLightPosition; 
    float3x3 rotMatrix = (float3x3)xWorld; 
    float3 rotNormal = mul(inNormal, rotMatrix);
    Output.Normal = rotNormal;
    
    return Output; 
}
Pixel Shader

法线和光线方向在三个顶点间进行插值,作用在三角形的所有像素上。如前所述,因为被插值的向量的长度会比实际的小,所以会发生错误。你可以通过归一化操作解决这个问题。将两个方向归一化之后,就可以点乘两者获取光照因子:

PPSPixelToFrame PPSPixelShader(PPSVertexToPixel PSIn) : COLOR0 
{
    PPSPixelToFrame Output = (PPSPixelToFrame)0; 
    float4 baseColor = float4(0,0,1,1); 
    float3 normal = normalize(PSIn.Normal); 
    float3 lightDirection = normalize(PSIn.LightDirection); 
    float lightFactor = dot(normal, -lightDirection);
   
    Output.Color = baseColor*(lightFactor+xAmbient); 
    
    return Output; 
}
定义Technique

这个technique需要Shader 2.0–compatible的显卡:

technique PerPixelShading
{
    pass Pass0 
    {
        VertexShader = compile vs_2_0 PPSVertexShader(); 
        PixelShader = compile ps_2_0 PPSPixelShader(); 
    }
}
代码

前面已经写过. fx文件中的HLSL代码了,所以下面只是绘制三角形的XNA代码:

effect.CurrentTechnique = effect.Techniques["PerPixelShading"]; 
effect.Parameters["xWorld"].SetValue(Matrix.Identity); 
effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); 
effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); 
effect.Parameters["xAmbient"].SetValue(0.0f); 
effect.Parameters["xLightPosition"].SetValue(new Vector3(6.0f, 1.0f, -5.0f));

effect.Begin(); 
foreach (EffectPass pass in effect.CurrentTechnique.Passes) 
{
    pass.Begin(); 
    device.VertexDeclaration = myVertexDeclaration; 
    device.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleStrip, vertices, 0, 6); 
    pass.End(); 
}
effect.End();

你可以试着改变光源的位置查看效果。本例中的光源前后移动。

image