问题
你想使用自定义的HLSL effect在场景中添加镜面高光。镜面高光是位于反光位置的高亮度区域,如图6-11所示。
解决方案
下面的讨论将帮助你判断哪个像素具有高光分量。
图6-11的左图显示了一条光线L,从光源指向三角形中的一个像素。在左图中还显示了 eye向量,从相机指向像素。如果L的反射向量与E相同,那么这个像素就有一个高光分量。
图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();