问题

使用你配置好的光照,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)。

image

图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所示,一个物体使用一个包含平移一段距离的世界矩阵向右平移时,顶点的位置会移向右方。法线也会根据这个世界矩阵移向右方,但它们的方向应该是不变的。所以,在使用世界矩阵变换法线时,你需要将世界矩阵中的平移部分剥离出来。

image

图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(); 
    }
}

image