问题
如教程6-3所示,要获得最好的光照效果应该使用逐像素光照,特别是对那些由大三角形构成的曲线的情况中。你想使用自己的effect添加逐像素光照。
解决方案
前两个教程中,你在每个顶点中计算明暗值(shading value,也可以翻译成着色值)。三角形三个顶点的明暗值会进行线性以获取每个像素的明暗值。
在逐像素光照中,你想对三个顶点的法线进行插值以获取每个像素的法线,这样就可以基于每个像素的法线计算光照因子。但是,当从一个顶点到另一个顶点进行法线插值时结果是有缺陷的。如图6-9中的左图所示,图中的水平直线表示一个顶点包含法线的三角形。当你在像素上对这左右两个顶点的法线进行插值时,插值的法线总会沿着虚线。导致在三角形中间的法线法线是正确的,其他位置的法线会小于实际值,如图所示。
图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();
你可以试着改变光源的位置查看效果。本例中的光源前后移动。