问题

你想使用自定义的HLSL effect在场景中添加镜面高光。镜面高光是位于反光位置的高亮度区域,如图6-11所示。

解决方案

下面的讨论将帮助你判断哪个像素具有高光分量。

图6-11的左图显示了一条光线L,从光源指向三角形中的一个像素。在左图中还显示了 eye向量,从相机指向像素。如果L的反射向量与E相同,那么这个像素就有一个高光分量。

image

图6-11 使用靠近eye向量的光线方向检测像素

你可以通过求L关于像素法线的镜像获取L的反射向量。如果镜像方向与eye向量夹角很小则这两个方向几乎是相同的。你可以通过点乘这两个向量检测两者的夹角(可参见教程 6-8)。

如果角度为0,则这两个方向是相同的,你需要添加一个高光分量,这时点乘的结果为1。如果两个方向不同,则点乘结果小于1。

注意:两个向量A和B的点乘结果等于(A的长度)*(B的长度)*(两者夹角的余弦)。如果A和B都已经进行了归一化,点乘结果会变为(两者夹角的余弦)。如果A和B的夹角为0,则余弦值为1。如果两者垂直,夹角为90度,余弦值为0,如图6-11的右图所示。如果两个向量方向相反,夹角为180度,余弦值为-1。 当反射的方向与eye向量的方向夹角小于90度时,点乘结果为正。

你还不能立即使用这个值判断高光,因为这样做会在所有反射向量与eye向量的夹角小于90度的像素上添加高光,而你想在夹角小于10度时才添加高光。

这可以通过对点乘结果进行一个高次幂实现。例如,将点乘结果进行12次方的操作,会使角度小于10度的情况下这个值才会大于0,如图6-11右下图所示。

每个像素的运算结果是一个single值,表示高光强度。

工作原理

和以往一样,你需要首先设置World,View和Projection矩阵将3D位置转换到2D屏幕位置。因为这个教程用的是一个点光源,你还需指定它的位置。要计算eye向量,你需要知道相机的位置。你还需能够设置光照强度控制高光大小。因为光照强度可能大于1,因此需要缩小光照强度避免饱和(saturation)。

注意:在大多数情况中,你需要缩小光源的强度。在多光源的情况中大多数像素的光照会饱和,浪费光照effect。

float4x4 xWorld; 
float4x4 xView;
float4x4 xProjection; 
float3 xLightPosition; 
float3 xCameraPos; 
float xAmbient; 
float xSpecularPower; 
float xLightStrength; 

struct SLVertexToPixel 
{
    float4 Position : POSITION; 
    float3 Normal : TEXCOORD0; 
    float3 LightDirection : TEXCOORD1; 
    float3 EyeDirection : TEXCOORD2; 
}; 

struct SLPixelToFrame 
{
    float4 Color : COLOR0; 
};

vertex shader还计算了EyeDirection并进行插值。pixel shader仍然只输出每个像素的颜色。

Vertex Shader

vertex shader与前面的教程没有太大的不同。唯一一个新的东西就是eye向量在vertex shader中进行计算。从一个点指向另一个点的向量可以通过将终点减去起点实现。

SLVertexToPixel SLVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0)
{
    SLVertexToPixel Output = (SLVertexToPixel)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; 
    Output.EyeDirection = final3DPos - xCameraPos; 
    
    float3x3 rotMatrix = (float3x3)xWorld; 
    float3 rotNormal = mul(inNormal, rotMatrix); 
    Output.Normal = rotNormal; 
    
    return Output; 
}
Pixel Shader

pixel shader更加有趣。基本颜色是蓝色的,无需关注太多。在pixel shader中归一化每个方向,因为它的长度可能不是1 (见教程6-3)。

与以往一样,你计算了光照,将它乘以xLightStrength缩小一点(xLightStrength小于1)。

SLPixelToFrame SLPixelShader(SLVertexToPixel PSIn) : COLOR0 
{
    SLPixelToFrame Output = (SLPixelToFrame)0; 
    
    float4 baseColor = float4(0,0,1,1); 
    float3 normal = normalize(PSIn.Normal); 
    float3 lightDirection = normalize(PSIn.LightDirection); 
    float shading = dot(normal, -lightDirection); 
    shading *= xLightStrength; 
    
    float3 reflection = -reflect(lightDirection, normal); 
    float3 eyeDirection = normalize(PSIn.EyeDirection); 
    float specular = dot(reflection, eyeDirection); 
    specular = pow(specular, xSpecularPower); 
    specular *= xLightStrength; 
    
    Output.Color = baseColor*(shading+xAmbient)+specular; 
    
    return Output; 
}

然后,使用reflect 函数计算光线方向的镜像。因为光线方向是指向像素的,它的反射方向将指向眼睛,反射方向与eye向量相反,所以需要取负值。

Specular的值可以通过点乘eye向量和反射方向获取,将这个值进行高次幂计算,使这两个向量的夹角小于10度的像素高光值才会大于0。这个值需要通过乘以xLightStrength 变得小一点。

最后,ambient,shading和specular分量组合在一起获得像素最后的颜色。

注意:specular分量在最终颜色中添加白色。如果光线有不同的颜色,你需要将specular值乘以光线的颜色。

定义Technique

下面是technique定义:

technique SpecularLighting 
{
    pass Pass0
    {
        VertexShader = compile vs_2_0 SLVertexShader(); 
        PixelShader = compile ps_2_0 SLPixelShader(); 
    }
}
代码

因为所有HLSL代码前面已经写过了,下面只是XNA代码:

effect.CurrentTechnique = effect.Techniques["SpecularLighting"]; 
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["xLightStrength"].SetValue(0.5f); 
effect.Parameters["xLightPosition"].SetValue(new Vector3(5.0f, 2.0f, -15.0f)); 
effect.Parameters["xCameraPos"].SetValue(fpsCam.Position); 
effect.Parameters["xSpecularPower"].SetValue(128.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