[翻译]XNA 3.0 Game Programming Recipes之thirty-six

PS:自己翻译的,转载请著明出处格
                                                 5-14 Add Per-Pixel Detail by Bump Mapping in Tangent Space
问题
                   虽然在前面的章节行之有效平面物体,它默认的法线是恒定的为所有象素在所有的三角形中,你会陷入麻烦中,如果你想要bump map 一个曲线或者有角的表面。
                   主要问题是,bump map包含偏离的法线在切线坐标内,意思是相对于默认的法线。
                   可视化的问题,让我们说你想去绘制一个圆柱,如图5-27所示。图型的左边显示默认法线在圆柱的顶点上。
                   现在想象下你想要bump map这个圆柱。作为一个例子,你会使用一个bump map,它包含(-1,0,1)法线为圆柱的所有象素。这符合一个法线向左偏离45度,相对于默认的法线。
                   正确的方法找到这种法线可能会同你的脚在这个圆柱上一致,在默认法线的初始点,沿着默认法线,旋转你自己45度向左。试着想象下这个为这顶点0,4,6的法线。你最终的方向将会是与圆柱上所有象素的方向不同,因为它依靠默认的法线在那个象素!如果你这样做为每个法线,你以法线在图5-27右边显示的那样告终。这偏离的法线是偏离了45向左,相对于默认的法线。
                   但是,如果你想使用这个法线去计算光照,你需要找到世界空间相当于这个(-1,0,1)法线。这是必须的,由于你必须只使用两个顶点成一体的计算,如果两个向量被定义在同样的空间,并且你会想指定光照的方向在世界空间里在你的XNA程序中。
解决方案
                   这三个颜色组成的bump map包含法线的坐标在切线空间中,这意味这相对于默认法线。它们表明默认的法线应该被偏移多少。这个本地坐标系统有一点不同在每个象素中,由于它依赖于默认法线,它能不同于一个曲线表面的每一个象素。这个本地坐标系统被称为tangent space.
提示:为得到一个概念这个切线空间看起来象什么,想象你自己站在圆柱的一个象素上如图5-27所显示,沿着默认的法线。现在想象下,你的Up方向是Z-轴,你的右方向是X-轴,你的前进方向是Y-轴。
                   象这样一个切线坐标系统为一个特定象素被显示通过三灰色的箭头在图象5-28上"a"。最重要的是注意,默认法线在一个象素中是切线空间的Z-轴(参看前面的章节)。

                   切线空间的X-和Y-轴必须垂直(使一个角度为90度)到这个Z-轴和其他的。Z-轴是垂直于物体(由于它是默认的法线),同时这X-和Y-轴接触,而不是相互交叉,该对象。X-和Y-轴被称为tangent和binormal.
                   三个颜色组成部分你从你的bump map中取样,包含偏移的法线,定义在这个切线空间。这作为黑箭头显示,这符合一个法线,它有一个X=0.3,Y=0,和z=0.8偏移从默认法线。图5-28的"a",你看见新的法线朝着X-轴偏移了一点。
                   最后,去计算出有多少象素点,你想采取数量积在这个法线它被定义在切线空间和光照方向之间,它被定义在世界(绝对)空间中在你的XNA工程中。为了得到一个数量积在两个顶点之间,虽然,两个向量必须被定义在同样的空间中。所以,无论你转变光照的方向从世界到切线空间或者你转换偏移法线从切线到世界空间。这节的开始部分,你会做这个稍后。这节的最后的阶段处理了反转方法。
它是如何工作的
                   为每个象素,你会从bump map中取样,获得偏移法线,相对于切线坐标系统。最后,你想知道这个偏移法线在世界空间坐标中,所以你能使这个数量积有光照的方向。
切线空间到世界空间的转换
                   这个法线你从bump map中取样被定义在切线空间中。一个圆形的塔,它是一个圆柱墙,将作为一个例子所示,如图5-28所示。每一个象素,这偏移的法线首先需要被转化到对象空间里,它是塔的空间。这将给你默认的法线在对象坐标里。
提示:为得到一个概念塔内的空间看上去象什么,想象你自己的脚站在塔的初始的地方,例如在塔的内部中心,你的Up方向(沿着塔)是y-轴,你的右方向是x-轴,和你的后方向是z-轴。你想找到什么是偏移的法线,指定在你的塔的X,Y,Z坐标系统。
                   这个转换被表示通过一个箭头来自图5-28的"a"到"b"在图5-28。在图5-28的"a",法线(0.3,0,0.8)的坐标给出了本地切线坐标,同时在图象的右上,法线(0.2,0,0.85)的坐标被定义在对象坐标中(用两个系统轴核实这个)。
                   虽然这是一个有意义的3D方向,它是完全的可能的,你绘制这塔用一个世界矩阵包含一个旋转(例如,绘制比撒斜塔)。在这种情况下,你获得的法线应该摆脱这一旋转,获得绝对(世界)法线的方向。这是所显示的箭头在图5-28从"b"到"c"(注意这个图象它的对象坐标系统是真实的一个旋转的世界坐标系统版本)。最后,你获得法线向量在世界坐标中,准备与光照方向比较。
定义自定义顶点格式
                   作为任何转换。为了转换法线从切线空间到对象空间,你需要乘以它用正确的转换矩阵。你需要做这个在每个象素中。为了创建这个矩阵,你首先需要知道切线空间的每个顶点的x-,y-和z-轴(法线,切线,和binormal)。这可能与所有顶点不同,由于默认的法线,因此Z-轴将会不同。由于x-和y-轴必须垂直与z-轴,x-和y-轴将同样不同在每个顶点中。
                   由于三个轴必须互相垂直,一旦你知道它们中两个,你可以找到最后的一个通过已知的两个使用十字坐标。一个轴(z)是默认的法线,它能被找到使用5-7节所叙述的代码。切线向量能通过模型提供,或者你能定义它自己用简单的情况例如这个圆柱。
注意:在这种情况,你的顶点着色器将计算binormal向量通过使用一个十字坐标在法线和切线向量之间。因此,你的顶点需要保存唯一的法线和切线向量。你同样可以编写一个自定义的模型处理器,它计算binormal为每个顶点和保存它在顶点的内部。简单的使用MeshHelper.CalculateTangentFrames去做繁重的工作。作为一个好处,你的图形卡不需要计算这个向量为每一祯。
                   首先,你需要定义个自定义顶点格式,它能保存3D位置,纹理坐标,法线,和切线数据为每个顶点。参见5-12节自定义顶点格式的详细的描述。
 1 public struct VertPosTexNormTan
 2 {
 3     public Vector3 Position;
 4     public Vector3 TexCoords;
 5     public Vector3 Normal;
 6     public Vector3 Tangent;
 7     public Vector3 TexNormTan(Vector3 Position,Vector2 TexCoords,Vector3 Normal,Vector3 Tangent)
 8     {
 9            this.Position=Position; 
10            this.TexCoords=TexCoords;
11            this.Normal=Normal;
12            this.Tangent=Tangent;
13     }
14     public static readonly VertexElement[] VertexElement=
15     {
16          new VertexElement(0,0,VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Position,0),
17          new VertexElement(0,sizeof(float)*3,VertexElementFormat.Vector2,VertexElementMethod.Default,VertexElementUsage.TextureCoordinate,0),
18          new VertexElement(0,sizeof(float)*(3+2),VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Normal,0),
19          new VertexElement(0,sizeof(float)*(3+2+3),VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Tangent,0),
20     };
21      public static readonly int SizeInBytes=sizeof(float)*(3+2+3+3);
22 }
                     每个顶点需要存储一个Vector3为这个位置,一个Vector2为顶点坐标,超过两个Vector3s为法线和切线。这使总共有11个浮点被存储和转换到图形卡为每个顶点。binormals将会被计算在顶点着色器中。
定义每个顶点的法线和切线
                     在这个例子中,你需要定义一些三角形去创建一个塔(一个圆柱装的墙),法线数据将会被生成,使用5-7节的代码。
                     如前所述,切线方向需要垂直法线,应该与塔接触,而不是相交。在这个情况下,你已经定义一个垂直的塔,这样你知道Up方向是没有相交到塔,但是它垂直于所有的塔的法线,是一种理想的切线方向。        
                     这个代码生成圆柱的顶点。每一个生成的顶点,3D位置被计算,(0,1,0)Up方向被保存作为切线方向。
 1 private void InitVertices()
 2 {
 3     List<VertPosTexNormTan> verticesList=new List<VertPosTexNormTan>();
 4     int detail=20;
 5     float radius=2;
 6     float height=8;
 7     for(int i=0;i<detail+1;i++)
 8     {  
 9         float angle=MathHelper.Pi*2.0f/(float)detail*(float)i;
10         Vector3 baseVector=Vector3.Transform(Vector3.Forward,Matrix.CreateRotationY(angle));
11         Vector3 posLow=baseVector*radiu;
12         posLow.Y=-height/2.0f
13         Vector3 posHigh=posLow;
14         posHigh.Y+=height;
15         Vector2 texCoordLow=new Vector2(angle/(MathHelper.Pi*2.0f),1);
16         Vector2 texCoordHigh=new Vector2(angle/(MathHelper.Pi*2.0f),0);
17         verticesList.Add(new VertPosNormTan(posLow,texCoordLow,Vector3.Zero,new Vector3(0,1,0)));
18         verticesList.Add(new VertPosNormTan(posHigh,texCoordHigh,Vector3.Zero,new Vector3(0,1,0)));
19     }
20     vertices=verticesList.ToArray();
21 }
                  下一步,一些索引的产生,绘制三角形基于顶点。所有这些代码在5-7节有说明:
1 vertices=InitVertices();
2 indices=InitIndices(vertices);
3 vertices=GenerateNormalsForTriangleList(vertices,indices);
                  随着你的顶点和索引设置,你已经准备移动拟订.fx文件。
XNA-to-HLSL变量
                  正如所有3D着色器,你需要传递世界,视景,和投影矩阵。由于凹凸贴图没有光照就没有用,你可以设置光照的方向。最后,xTexStretch变量允许你定义多少次砖块纹理应该被缩小在它放在圆柱之上的时候。
                  你需要一个普通的纹理去从砖块颜色中取样,你同样需要bump map包含偏移的法线定义在切线空间坐标为所有的象素。
                  一如往常,你的顶点着色器应该转化每个顶点的3D位置到2D屏幕坐标,这个顶点坐标应该被传递到象素着色器中。为了允许你的象素着色器转换一个法线从切线空间到世界空间,顶点着色器将计算一个Tangent-to=World矩阵,它同样应该被传递到象素着色器中。
 1 float4*4 xWorld;
 2 float4*4 xView;
 3 float4*4 xProjection;
 4 float3 xLightDirection;
 5 float xTexStretch;
 6 Texture xTexture;
 7 sampler TextureSampler=sampler_state{texture=<xTexture>;magfilter=LINEAR;minfilter=LINEAR;mipfilter=LINEAR;AddressU=wrap;AddressV=wrap;};
 8 Texture xBumpMap;
 9 sampler BumpMapSampler=sampler_state{texture=<xBumpMap>;magfilter=LINEAR;minfilter=LINEAR;mipfilter=LINEAR;AddressU=wrap;AddressV=wrap;};
10 struct BMVertexToPixel
11 {
12      float4 Position:POSITION;
13      float2 TexCoord:TEXCOORD0;
14      float3*3 TTW:TEXCOORD1;
15 };
16 struct BMPixelToFrame
17 {
18     float4 Color:COLOR0;
19 };
注意:这个代码传递一个3*3矩阵,使用一个单一的本质的TEXCOORD1.这将会被编译,但是在背景TEXCOORD2和TEXCOORD3同样被使用,所以你不能使用这些了。
顶点着色器
                   开始顶点着色器,这是通常的;它转换3D位置到3D屏幕坐标,传递纹理坐标到象素着色器:
1 BMVertexToPixel BMVertexShader(float4 inPos:POSITION0,float3 inNormal:NORMAL0,float2 inTexCoord:TEXCOORD0,float3 inTangent:TANGENT0)
2 {
3      BMVertexToPixel Output=(BMVertexToPixel)0;
4      float4*4 preViewProjection=mul(xView,xProjection);
5      float4*4 preWorldViewProjection=mul(xWorld,preViewProjection);
6      Output.Position=mul(inPos,preWorldViewProjection);
7      Output.TexCoord=inTexCoord;
8      return Output;
9 }
                   接下来,您需要添加一些代码到您的顶点着色器构建Tangent-to-World矩阵。如前所述,这一转变发生在两个阶段进行。首先,您法线的坐标变换相切的对象从空间和未来的对象世界空间。
                   首先定义tangentToObject矩阵。去定义一个转化矩阵,你需要知道基础坐标系向量,它们是法线,切线,和切线空间的双切线。
                   你的顶点着色器接收到法线和每个顶点的切线。如前面所述,你能计算binormal通过使它们的十字坐标,由于它是顶点垂直于法线和切线:
1 float Binormal=normalize(cross(inTangent,inNormal));
                   与你的binormal计算,你已经准备建造你的Tangent-to-Object矩阵,由于转换矩阵的行没有任何更多的三顶点定义新的坐标系统。一个理想坐标系统的顶点同样被规格化,这样你按照下面:
1 float3*3 tangentToObject;
2 tangentToObject[0]=normalize(Binormal);
3 tangentToObject[1]=normalize(inTangent);
4 tangentToObject[2]=normalize(inNormal);
提示:规格化这个矩阵的行,由于三行顶点互相垂直,这个矩阵的反转是和矩阵的颠倒顺序是一样的。你可以找到矩阵的颠倒顺序通过镜像原理在轴,它运行从左上角到右下角的原理, 这是比计算逆矩阵很容易做的。
                   所以你现在应该做的是结合Tangent-to-Object转换用这Object-to-World转换通过乘以它们的矩阵,这样你获得Tangent-to-World矩阵。Object-to-World矩阵无非是世界矩阵(例如,包含塔的旋转),所以你结束这段代码:
1 float3*3 tangentToWorld=mul(tangentToObject,xWorld);
2 Output.TTW=tangentToWorld;
                   你传递这个矩阵到你的象素着色器。现在你的象素着色器能很容易转换成任何顶点从切线空间到世界空间,通过乘以这顶点用这个矩阵!
象素着色器
                   与所有的准备工作在顶点着色器里,支持Pixel Shader看上去身容易:
 1 BMPixelToFrame BMPixelShader(BMVertexToPixel PSIn):COLOR0
 2 
 3       BMPixelToFrame Output=(BMPixelToFrame)0;
 4       float3 bumpColor=tex2D(BumpMapSampler,PSIn.TexCoord*xTexStretch);
 5       float3 normalT=(bumpColor-0.5f)*2.0f;
 6       float3 normalW=mul(normalT,PSIn.TTW);
 7       float lightFactor=dot(-normalize(normalW),normalize(xLightDirection));
 8       float4 texColor=tex2D(TextureSampler,PSIn.TexCoord*xTexStretch);
 9       Output.Color=lightFactor*texColor;
10       return Output;
11 }
                   你开始通过从bump map取样在位置相应到象素的纹理坐标。这个颜色包含三个有用的颜色组成部分,相应到偏移法线的坐标,relative-to-tangent空间。
                   作为提到的"Pixel Shader"节在前面的章节中,一个颜色的组成部分范围在0到1,同时一个法线的坐标范围从-1到1。这样,首先减去0.5从这个值,使它们从[-0.5,0.5]范围开始,然后乘以2,使它们到[-1,1]的范围。这个结果被存储在normalT变量中。
                  这个normalT变量包含偏移法线,定义在切线空间坐标。一个normalT值的(0,0,1)表明,偏移法线相当于默认的法线(z-轴在切线空间)。normalT(0,0.7,0.7)的值,例如,表明这法线应该指向45度在默认法线和切线之间(y-轴在切线空间)。
                  这个转换从切线空间到世界空间是通过乘以normalT向量用Tangent-to-World矩阵实现的。这normalW值被获得,包含法线在世界的坐标。
注意:你可以立即转化法线从切线空间到世界空间,因为Tangent-to-World矩阵是由Tangent-to-Object和Object-to-World矩阵组成。
                  最后,你知道偏移法线在世界空间坐标,所以你能使点乘积在这个向量和光照方向之间,它同样是在世界空间得到。同往常一样,如6-1所描述的那样,这个点乘积给你照明系数和应乘的颜色。由于每个象素有一个不同偏移法线,每个象素将会有一点不同。
代码
            参看上面的代码。
反转方法
                  前面的代码显示你如何创建tangentToWorld矩阵,这样每个象素能转换它的法线顶点从切线空间到世界空间。这是必须的在它能有光照方向之前,它同样定义在世界空间中。
                  你同样可以做这个事情用别的方法:你能转换你的光照方向从世界矩阵到切线空间,在切线空间内用法线给它打点。这成了非常有趣的由于每个象素光照方向是相同的,它能使它能够转换光照方向在顶点着色器中,并且传送结果到象素着色器中。这样,你的象素着色器能立即执行标量乘积,不在有计算任何种类的转换!
注意:使用这个方法,你需要转换光照的向量只有三次为每个三角形,而不是转换法线为三角形的每个象素。
                  而不是传递tangentToWorld矩阵到你的象素着色器,你会传递光照方向,转换到切线空间,到你的象素着色器:
struct BMVertexToPixel
{
     float4 Position:POSITION;
     float2 TexCoord:TEXCOORD0;
     float3 LightDirT:TEXCOORD1;
};
                  在你的顶点着色器,你已经准备计算tangentToWorld矩阵。这次,无论如何,你想转变光照方向从世界到切线空间,这样你想反转tangentToWorld矩阵。
                  计算反转矩阵是一个非常复杂的工作。运气的是,tangentToWorld矩阵被创建从三个垂直的,规格化向量(binormal,切线,和法线)。
一般来说,你传递向量作为第一个参数,矩阵作为第二个参数到mul方法中:
1 float3 vectorW=mul(vertorT,tangentToWorld);
                  在这个特殊的情况下,三个垂直规格化的向量,你能转换一个向量通过反转一个矩阵,用交换它们的顺序在乘积操作:
1 float3 vectorT=mul(tangentToWorld,vectorW);
注意:这是由于一个矩阵反转的构造从三个正常化和垂直向量相当于矩阵的颠倒顺序。
                  这正是你想要做的为你的光照方向:你想去转换它从世界空间到切线空间:
1 Output.LightDirT=mul(tangentToWorld,xLightDirection);
                  你计算这个光照方向,表示在您的切线空间,传送它到你的象素着色器。由于法线从你的bump map里取样,并且你的光照方向在同一个空间,你可以立即计算它们的标量乘积在你的象素着色器:
1 float3 bumpColor=tex2D(BumpMapSampler,PSIn.TexCoord*xTexStretch);
2 float3 normalT=(bumpColor-0.5f)*2.0f;
3 float lightFactor=dot(-normalize(normalT),normalize(PSIn.LightDirT));

posted on 2009-08-10 09:48  一盘散沙  阅读(373)  评论(0编辑  收藏  举报

导航