6.5 添加HLSL Vertex Shading
问题
使用你配置好的光照,BasicEffect可以很好地绘制场景。但是,如果你想定义一些更酷的效果,首先要实现的就是正确的光照。
本教程中,你将学习如何编写一个基本的HLSL effect实现逐顶点光照。
解决方案
传递每个顶点的3D位置和法线到effect中。显卡上的vertex shader需要对每个顶点做两件事。
首先,当绘制3D世界时,总是要使用世界矩阵,视矩阵和投影矩阵将3D位置转换为对应的2D屏幕坐标。
第二,通过叉乘光线方向和法线方向计算顶点的光照强度。
工作原理
首先需要在XNA项目中定义顶点。显然你需要将3D位置存储在每个顶点中。要在vertex shader 中计算正确的光照,你还需要为每个顶点提供法线,可参见教程6-1理解法线的概念。
你可以使用教程6-1中的相同代码,这个代码创建了包含一个3D位置和一个法线(还包含纹理坐标,只是这里你不使用它们)的六个顶点。
在XNA项目中创建一个新的. fx文件,添加以下代码。它包含了可以从XNA应用程序中改变的HLSL变量。
float4x4 xWorld; float4x4 xView; float4x4 xProjection; float xAmbient; float3 xLightDirection;
当将一个3D坐标转换到2D屏幕坐标时,总是需要视矩阵和投影矩阵(见教程2-1)。因为你还想在场景中移动物体,所以还需一个世界矩阵(见教程4-2)。因为这个教材处理的是光照,你需要定义光线的方向。Ambient变量让你可以设置光照的最小级别,这样,即使一个对象没有被光源直接照射,它仍是隐约可见的。
在进入vertex shader和pixel shader前,首先需要定义output结构。首先,vertex shader的output 就是pixel shader的input,必须保存每个顶点的2D屏幕坐标。第二,vertex shader 还计算了每个顶点的光照强度。
在vertex shader和pixel shader之间,这些值进行了插值,让每个像素获取了它们各自的插值。
pixel shader仅计算每个像素的最终颜色。
struct VSVertexToPixel { float4 Position : POSITION; float LightingFactor : TEXCOORD0; }; struct VSPixelToFrame { float4 Color : COLOR0; };
Vertex Shader
vertex shader将World,View和Projection矩阵组合成一个矩阵,用来将3D坐标转换为2D屏幕坐标。
给定光线方向和法线方向,vertex shader可以根据图6-7计算光照强度。光线和法线间的夹角越小,光照越强烈,夹角越大,光照越少。
你可以通过点乘这两个方向获得这个值。点乘返回一个0到1之间的值(如果两个向量的长度都是1)。
图6-7 光线方向和法线方向的点乘
但是,在计算两者的点乘时首先要将其中一个方向反向,否则这两个方向是相反的。例如,图6-7右图中你发现法线和光线方向是相反的,这会导致点乘的结果为负。
VSVertexToPixel VSVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0) { VSVertexToPixel Output = (VSVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); float3 normal = normalize(inNormal); Output.LightFactor = dot(rotNormal, -xLightDirection); return Output; }
点乘的结果是一个single值,基于两个法线的夹角和长度。在大多数情况中,你只需要光线基于两者的夹角。这意味着你需要确保3D空间中的所有法线和光线方向长度是一样的;否则,具有更长法线的顶点会获取更多的光照。
这可以通过让所有法线的长度为1做到,即需要归一化法线。
注意:归一化(normalizing)的意思不是对法线不做操作,而是让一个向量的长度变为1,可参见教程6-1。
使用世界矩阵时确保正确的光照
前面的代码在世界矩阵为单位矩阵时工作良好,即物体需要放置在(0,0,0)3D空间的初始位置(见教程5-2)。
但在大多数情况中,你想使用另外的世界矩阵,让你可以移动/旋转/缩放对象。
如图6-1所示,如果你旋转了物体,法线也会跟着一起旋转。这意味着法线需要通过世界矩阵中的旋转量进行变换。
世界矩阵中的缩放操作不会影响光照的计算。你总要在vertex shader中归一化法线,让向量的长度变为1。
但是,如果世界矩阵中包含平移,你就会遇到麻烦。这是因为法线是最大长度为1的向量。例如,当你使用一个包含超过两个单位的矩阵变换法线时,所有的法线都会指向那个方向。
如图6-8所示,一个物体使用一个包含平移一段距离的世界矩阵向右平移时,顶点的位置会移向右方。法线也会根据这个世界矩阵移向右方,但它们的方向应该是不变的。所以,在使用世界矩阵变换法线时,你需要将世界矩阵中的平移部分剥离出来。
图6-8 被世界矩阵中的平移影响的法线
矩阵是一个包含4 × 4个数字的表格。你应该只使用世界矩阵中的旋转部分变换法线,而不要用平移部分。你可以提取出矩阵的旋转部分,它位于左上的3 × 3的数字中。只需简单地将4 × 4世界矩阵变换为一个3 × 3矩阵,就可以只获取旋转信息,这正是你所需要的!使用这个矩阵旋转法线,代码如下所示:
float3 normal = normalize(inNormal); float3x3 rotMatrix = (float3x3)xWorld; float3 rotNormal = mul(normal, rotMatrix); Output.LightFactor = dot(rotNormal, -xLightDirection);
Pixel Shader
首先,三角形的三个顶点由vertex shader进行处理,计算光照值。然后,对三角形中的每个像素,这个光照值会在三个顶点间进行插值。这个插值过的光照值传递到pixel shader。
在这个简单地例子中,取蓝色为物体的基本颜色。要在三角形上添加明暗效果,要将这个基本颜色乘以LightFactor (在前面的vertex shader中计算)和环境光照(由XNA程序通过xAmbient变量设置)。环境光(ambient)因子确保所有物体不会是完全黑暗的,而LightFactor 根据光线方向施加对应的光照:
VSPixelToFrame VSPixelShader(VSVertexToPixel PSIn) : COLOR0 { VSPixelToFrame Output = (VSPixelToFrame)0; float4 baseColor = float4(0,0,1,1); Output.Color = baseColor*(PSIn.LightFactor+xAmbient); return Output; }
定义technique
最后,定义technique:
technique VertexShading { pass Pass0 { VertexShader = compile vs_2_0 VSVertexShader(); PixelShader = compile ps_2_0 VSPixelShader(); } }
XNA代码
在XNA项目中,导入HLSL文件并将它存储在一个Effect变量中,这和教程3-1中对纹理的操作是类似的。在本例中,HLSL文件名为vertexshading. fx:
effect = content.Load<Effect>("vertexshading");
当绘制物体时,首先需要设置effect的参数,这需要用到BasicEffect:
effect.CurrentTechnique = effect.Techniques["VertexShading"]; effect.Parameters["xWorld"].SetValue(Matrix.Identity); effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xLightDirection"].SetValue(new Vector3(1, 0, 0)); effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleList, vertices, 0, 2); pass.End(); } effect.End();
代码
XNA代码绘制对象的多个实例。因为使用了不同的世界矩阵,这些对象会绘制在不同位置。
最终结果和教程6-1是一样的,只是这次你使用了自己的HLSL effect:
effect.CurrentTechnique = effect.Techniques["VertexShading"]; effect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); effect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); effect.Parameters["xLightDirection"].SetValue(new Vector3(1, 0, 0)); effect.Parameters["xAmbient"].SetValue(0.0f); for (int i = 0; i < 9; i++) { Matrix world = Matrix.CreateTranslation(4, 0, 0) * Matrix.CreateRotationZ((float)i * MathHelper.PiOver2 / 8.0f); effect.Parameters["xWorld"].SetValue(world); effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = myVertexDeclaration; device.DrawUserPrimitives<VertexPositionNormalTexture> (PrimitiveType.TriangleList, vertices, 0, 2); pass.End(); } effect.End(); }
下面是.fx文件的完整内容:
float4x4 xWorld; float4x4 xView; float4x4 xProjection; float xAmbient; float3 xLightDirection; struct VSVertexToPixel { float4 Position : POSITION; float LightFactor : TEXCOORD0; }; struct VSPixelToFrame { float4 Color : COLOR0; } // Technique: VertexShading VSVertexToPixel VSVertexShader(float4 inPos: POSITION0, float3 inNormal: NORMAL0) { VSVertexToPixel Output = (VSVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); float3 normal = normalize(inNormal); float3x3 rotMatrix = (float3x3)xWorld; float3 rotNormal = mul(normal, rotMatrix); Output.LightFactor = dot(rotNormal, -xLightDirection); return Output; } VSPixelToFrame VSPixelShader(VSVertexToPixel PSIn) : COLOR0 { VSPixelToFrame Output = (VSPixelToFrame)0; float4 baseColor = float4(0,0,1,1); Output.Color = baseColor*(PSIn.LightFactor+xAmbient); return Output; } technique VertexShading { pass Pass0 { VertexShader = compile vs_2_0 VSVertexShader(); PixelShader = compile ps_2_0 VSPixelShader(); } }
发布时间:2009/9/25 上午8:21:59 阅读次数:5739