Clayman的专栏

It's all about XNA & GPU Programming

用户操作
[即时聊天] [发私信] [加为好友]
claymanID:soilwork
192942次访问,排名382好友0人,关注者13
soilwork的文章
原创 85 篇
翻译 15 篇
转载 0 篇
评论 330 篇
clayman的公告
嘿嘿 ^o^....
最近评论
ffffk:研究到这的都是高手
jym5596337:我也不知道我怎么就走到了你的路上来了呵呵...
太晕了,我们专业就学的C#... 那我就凑合着用它学习MDX喽,但到了2008你的这个时期,感觉形式有点尴尬,以前的人说 在中国搞软件是 前有微软,后有盗版。
现在是 前有XNA后有C++ ... MDX 学习资料太太太难找了。。太太太少了。 师兄给介绍下你学历路途中的资料目录咯。。。我好找来学习咯。。 感谢哦感谢。。
jym5596337:好象没有继续哇 ...
shapin:ATI的网站有个支持HLSL语法高亮的vs插件,可以支持其他版本,只要修改相应的那个注册表就行
flip:To linxv :
貼圖座標有用投影嗎?
文章分类
收藏
    相册
    blogs
    David Weller
    nVidia Developer blog
    Rico Mariani
    Shawn Hargreaves
    XNA Team blog
    XNA资源
    XNA Creators Club
    ZBuffer
    Ziggyware XNA Resources
    中国XNA开发网
    存档
    软件项目交易
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创 XNA Kick Start (四) 收藏

    新一篇: i'm just busy, not dead...+_+ | 旧一篇: 卡通渲染--Dot3 Cel Shading

    本文版权归我所有,仅供个人学习使用,请勿转载,勿用于任何商业用途。
    由于本人水平有限,难免出错,欢迎大家和我交流。
    作者:clayman
    Blog:
    http://blog.csdn.net/soilwork
    clayman_joe@yahoo.com.cn 

    三.绘制三角形

     

    创建几何体信息

     

           讲了那么多,还没绘制过任何图形呢,三角形是3D图形中最基本的图元之一,就从绘制三角形开始吧。

           首先,需要一些数据来描述三角形。XNA中使用顶点来描述物体的几何信息。顶点中通常包含顶点位置,颜色,纹理坐标,法线,切线等信息。XNA中有很多现成的结构来储存常见顶点类型,比如:VertexPositionColor, VertexPositionNormalTexture,VertexPositionTexture等结构。你也可以定义新的顶点类型。 目前,简单的VertexPositionColor类型就能满足需要,正如它的名字所示,这种顶点格式包含顶点的位置和颜色信息。位置是包含三个分量的矢量,定义顶点在3D空间中的坐标。XNA有自己的颜色系统,所以不需要使用System.Draw中的Color类。以下是定义三角形的代码,把它放到一个名为InitTrangile()的函数中:

     

         private void InitTrangile()

         {

             verts = new VertexPositionColor[3];

             verts[0].Position = new Vector3(0, 0.5f, 1.0f);

             verts[0].Color = xna.Color.Red;

             verts[2].Position = new Vector3(-0.5f, -0.5f, 1.0f);

             verts[2].Color = xna.Color.Green;

             verts[1].Position = new Vector3(0.5f, -0.5f, 1.0f);

             verts[1].Color = xna.Color.Blue;

         }

          

           接下来,需要创建一个称为VertexDeclaration的对象。从名字就能看出,这个对象是用来描述顶点类型的。注意,无论绘制什么图形时,都必须先使用这个对象告诉GraphicsDevice所处理的顶点是什么类型。对于XNA内置的顶点类型来说,创建相应的VertexDeclaration对象也非常简单。首先在Form1中添加VertexDeclaration成员decl,然后在InitTrangile()中初始化变量:

     

             decl = new VertexDeclaration(this.graphicsDevice, VertexPositionColor.VertexElements);

     

    自定义顶点类型:

         XNA中的预定义顶点类型不能满足要求时,可以手动创建结构来保存顶点数据。假设我们希望顶点中包含位置,纹理坐标,法线和切线信息,把这个结构称为VertexPosTexNorTan,它只是一个简单的数据集合:

        struct VertexPosTexNorTan

        {               

            public Vector3 Position;

            public Vector3 Normal;

            public Vector2 UV;     

            public Vector3 Tangent;

    }

        对于创建VertexDeclaration来说就稍微麻烦一点,它的构造函数需要一个VertexElement类型的数组,需要先构造出这个数组:

         VertexElement[] element = new VertexElement[]

        {

            new VertexElement(0,0,VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Position,0),

            new VertexElement(0,12,VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Normal,0),

            new VertexElement(0,24,VertexElementFormat.Vector2,VertexElementMethod.Default,VertexElementUsage.TextureCoordinate,0),

            new VertexElement(0,32,VertexElementFormat.Vector3,VertexElementMethod.Default,VertexElementUsage.Tangent,0),         

     };

         VertexElement里每个元素对应VertexPosTexNorTan中的一个成员。第一个参数表示关联到顶点分量的数据流索引。大多数情况下一条数据流就可以了,索引0表示第一条流。第二个参数是成员在顶点结构中的偏移值。Positon是结构中的第一个元素,因此偏移为0PositionVector3类型的变量,所以对第二个成员Normal来说,它在结构中的偏移值为12。这里的偏移值都以Byte为单位。下一个参数是VertexElementFormat类型的枚举,用来表示每个成员的数据类型。接下来的VertexElementMethod枚举表示在图形流水线中的图元镶嵌光栅化阶段如何对当前变量进行插值,一般使用默认值就可以。VertexElementUsage是一个比较重要的参数,指定当前成员的用途。比如,定义Position变量在程序中表示顶点的位置。最后一个变量也和VertexElementUsage枚举有关。有时,一个顶点可能会有一个以上的同用途元素,比如,有两组纹理坐标,为了区分两组不同的纹理坐标,必须指定一个索引值。这里,并没有重复用途的元素,所用使用0表示它是该用途的第一组数据就可以。

            有了VertexElement数据,创建VertexPosTexNorTan顶点相应的VertexDeclaration就很简单了:

    decl = new VertexDeclaration(graphicsDevice, element);

     

     

    编写HLSL代码

     

        有了顶点信息,接下来要做的就是渲染这个三角形。显然,渲染任务是由GPU来完成的,所以接下来的代码将使用HLSL来编写。为项目添加一个文本文件,把文件名改为simpleTriangle.fx,在文件中键入以下代码:

         void transform(inout float4 pos :POSITION,

              inout half4 color : COLOR0)

         {

              pos = pos;

              color = color;

         }

     

         void coloring(inout half4 color :COLOR)

         {

              color = color;

         }

     

         technique render

         {

              pass P0

              {

                  vertexShader = compile vs_2_0 transform();

                  pixelShader = compile ps_2_0 coloring();

              }

         }

     

        这可以说是最简单的shader了,来看看这些代码是什么意思(关于HLSL的详细语法,已经超出了本文讨论范围,可以参考我的其他文章)。首先,关键字technique定义了一个完整的渲染过程。一个fx文件中可能有一个或者多个technique,渲染时,必须指定使用哪一个technique。目前只有一个名为rendertechniquePass关键字定义了一个渲染遍。一个完整的渲染过程中,可以包含多个渲染遍。注意,使用的pass越多,所要的绘图代价也越大。P0中又定义了vertesShaderpixelShader

        让我们打个比方来解释这些概念吧。假如我们需要绘制一副画,那么整个绘制过程就是一个technique。绘画时,我们可能先用铅笔打草稿,然后上色,最后完善,其中每个步骤对应为一个pass。每个步骤中具体如何绘制物体,就由vertesShaderpixelShader来定义。你可能对P0中的两行代码还有些迷惑,让我用为你当一次翻译吧,第一行代码表示:使用vs_2_0配置条件,把tranform()函数编译为vertexShader。后一行代码与此类似。在绘图时,编译好的vertesShaderpixelShader程序将由XNA分别加载到GPU硬件的vertesShaderpixelShader中运行(有些像绕口令-_-b)。

        接下来看被编译为vertesShadertransform函数。它把顶点的位置和颜色作为参数,同样再把位置和颜色作为返回值。因为是最简单的shader,所以我们并没有为顶点和颜色进行任何计算,就原样输出了。对于聪明的你来说,相信已经能看懂后面的coloring函数了。

        接下来,我要加强你对图形流水线的认识。上面的代码中,你看到对transformcoloring的方法调用吗?看到为他们传递参数吗?显然没有,那么程序是如何运行的呢。当顶点数据提交到GPU时,顶点结构中的数据将被分别输送到不同的寄存器中,比如把顶点位置放到一个寄存器,颜色信息放到另一个寄存器。GPU对每个顶点调用vertexShader进行处理。那么vertexShader如何知道去那个寄存器找相应的值作为输入参数呢?前面在自定义顶点中曾讲过,VertexElement保存了顶点数据中每个值的用法VertexElementUsage和相应的索引。此外,再次看tranform函数的声明部分:

     

           inout float4 pos :POSITION

     

         这里的语法有些奇怪,不是吗?冒号前面,我们声明了一个float4类型的pos变量,它既是输入参数,也是函数返回值,而冒号以及POSITION则定义这个变量用来表示位置信息。发现它和VertexElementUsage的作用有些相似了没。shader正是通过这个标记来找到相应的寄存器,获得正确输入。

           VertexShader处理完成之后,到了流水线的图元装配(镶嵌)和光栅化阶段。这个过程是不可编程的,因此不需要编写代码。但它究竟完成哪些任务呢?目前为止,我们只有三角形的三个顶点,因此这里必须计算出三角形究竟将覆盖平面中的哪些像素,并通过插值计算,得出每个像素可能包含的属性值,比如纹理坐标,颜色,法线等等。处理完成过后,每个值又放到相应的积存器中。

           接下来,GPU为每个像素调用PixelSader,同样用前面描述的方法来寻找输入值,并且完成处理过程。

           哦,相当复杂的一个过程,不是吗?不知道我讲述的是否清楚,如果你现在没有完全理解,也不要担心,后面还会继续对此进行解释。

     

    初识Content Pipeline

     

           HLSL代码已经编写好了,现在需要把它加载到程序中。第一章中曾说过.fx文件,纹理,模型这些都属于游戏资源,自然需要使用Content Pipeline来处理它们。由于我们建的是普通winForm应用程序,现在解决方案中还不包含Conten Pipeline。好了,现在关闭整个项目,用记事本打开WindowsApplication1.csproj文件,可以看到这是一个类似xml的文件,把以下代码添加到<PropertyGroup>元素之内:

     

    <ProjectTypeGuids>{9F340DF3-2AED-4330-AC16-78AC2D9B4738};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>

    <XnaFrameworkVersion>v1.0</XnaFrameworkVersion>

    <XnaPlatform>Windows</XnaPlatform>

    <XNAGlobalContentPipelineAssemblies>Microsoft.Xna.Framework.Content.Pipeline.EffectImporter.dll;Microsoft.Xna.Framework.Content.Pipeline.FBXImporter.dll;Mi       crosoft.Xna.Framework.Content.Pipeline.TextureImporter.dll;Microsoft.Xna.Framework.Content.Pipeline.XImporter.dll</XNAGlobalContentPipelineAssemblies>

    <XNAProjectContentPipelineAssemblies>

    </XNAProjectContentPipelineAssemblies>

     

           再次打开项目,在solution explorer窗口中双击properties,可以看到项目属性标签中已经多了content pipeline标签,content pipeline已经加载到解决方案中了。选中effect.fx文件,在properties窗口中可以看到这个文件已经不再是简单添加到项目中的文件,而是经过content pipeline处理并导入项目中的。前一章说过,content pipeline分为预处理和运行时处理两个阶段,目前为止,我们完成了预处理阶段,要在运行时使用content pipeline,就必须通过ContenManager对象了。查看文档,可以知道ContentManager的构造函数需要一个IServiceProvider类型的接口作为参数。更确切的说,ContentManager需要IServiceProvider提供一个IGraphicsDeviceService接口来初始化对象。IServiceProvider是定义在System名称空间下的一个接口,XNA中,GameServiceContainer对象继承了这个接口,所以下面我们将使用GameServiceContainer而不是IServiceProvider

           开始编码,首先,让Form1继承IGraphicsDeviceService接口:

     

    partial class Form1:IGraphicsDeviceService

     

           这里,只需要实现接口的GraphicsDevice属性:

     

         public event EventHandler  DeviceCreated;

         public event EventHandler  DeviceDisposing;

         public event EventHandler  DeviceReset;

         public event EventHandler  DeviceResetting;

         public GraphicsDevice  GraphicsDevice { get { return graphicsDevice;} }

     

           接下来在Form1中添加GameServiceContainerContentManager成员:

     

         private ContentManager content;

         private GameServiceContainer service;

     

           Initiaize()方法中添加对象初始化代码:

     

         public void Initiaize()

         {

            service = new GameServiceContainer();

            service.AddService(typeof(IGraphicsDeviceService),this);

            content = new ContentManager(service);

          }

     

           使用ContentManager对象就能把任何所需要的资源加载到程序中了。

     

    使用Effect绘制图形

     

           讲了那么多,终于到了最终的绘图部分。我们已经有了三角形数据,渲染三角形的HLSL代码,如何把它们联系起来,并且进行绘图呢?前面说过,Shader将由XNA调用编译并加载到GPU中,而实际上,这些任务都是由Effect类来完成的,这是一个功能非常强大的类。在Form1中添加Effect成员:

     

         private Effect effect;

     

           你可能正在迷惑,刚才花了那么多代码创建ContentManager对象,却没有使用它。现在就是它派上用场的时候了。Effect类虽然提供了众多构造函数,但最常见的方法是直接通过一个.fx文件创建Effect对象,在Initiaize()中添加如下代码:

     

         effect = content.Load<Effect>("simpleTriangle");

         effect.CurrentTechnique = effect.Techniques["render"];

     

           第一行代码是用content pipeline加载资源的标准方法。注意,这里所加载的是经过Content Pipeline预处理过的文件,因此不需要带任何后缀名,但要保证文件名路径的正确。第二行代码则是为effect指定一个technique用于渲染,前面说过,一个.fx文件中可能有多个technique

        现在可以绘图了:

     

         protected override void OnPaint(PaintEventArgs e)

        {

            graphicsDevice.Clear(ClearOptions.Target, Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue, 1.0f, 0);

            graphicsDevice.VertexDeclaration = decl;

            effect.Begin();

            foreach (EffectPass pass in effect.CurrentTechnique.Passes)

            {

                pass.Begin();

                graphicsDevice.DrawUserPrimitives<VertexPositionColor>(PrimitiveType.TriangleList, verts, 0, 1);

                pass.End();

             }

             effect.End();

             graphicsDevice.Present();

          }

        

         首先使用decl变量告诉graphicsDevice所要绘制的顶点类型。使用effect绘图时,必须前调用Begin()方法,它将激活当前的technique,与此像对应,必须在绘图结束之后调用End()方法。之后,迭代当前technique中的所有pass进行绘图,同样必须为每个pass调用Begin()End()方法。注意,对于有Managed DirectX经验的人来说,可能发现graphicsDevice缺少了BeginSceneEndScene方法,XNA中已经删除了这两个方法,自然也不用调用它们。

        DrawUserPrimitives是真正发出绘图命令的地方,它把顶点提交给GPU,然后GPU开始在流水线上处理传入的顶点。DrawUserPrimitives的第一个参数是所要绘制的图元类型,它是PrimitiveType类型的枚举。在后面的章节将详细讨论它。接下来是包含顶点数据的数组。第三个参数是所要绘制的顶点在数组中的偏移值。比如顶点数组中包含10个顶点,而我们只希望绘制最后的三个顶点,那么就必须用这个参数来控制。最后一个参数则是所要绘制的图形数量。

        现在运行程序,可以看到如下画面(注意我稍微修改了程序结构,详见源代码):

       

     

           哦,我们所绘制的第一个图形程序看起来还不错。注意到没有,对于顶点以外的像素,硬件进行了插值,渲染出了一个多彩的三角形。如果你把simpleTriangle.fx文件中Coloring()函数中的代码改为:

     

        Color = half4(1.0,1.0,1.0,1.0);

          

           那么将看到一个