zoukankan      html  css  js  c++  java
  • 在场景中添加光线——添加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)。

    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

  • 相关阅读:
    利用SEH进行代码混淆
    HDU5294 Tricks Device(最大流+SPFA) 2015 Multi-University Training Contest 1
    输入字符串反序输出
    微信公众平台开发(104) 自定义菜单扫一扫、发图片、发地理位置
    不同编码页面之间表单的提交方法
    PHP登陆Session验证
    微信公众平台开发培训
    微信公众平台开发(98) UnionID
    微信电商再侵袭,腾讯要革淘宝的命
    微信企业号
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120098.html
Copyright © 2011-2022 走看看