步入DirectX的圣殿(下)

  步入DirectX的圣殿(下) 收藏
声明
  本文来自《msdn开发精选》杂志2005年第4期“技术专题”栏目,本文版权归杂志编辑部所有,未经许可,禁止转载!

作者:Derek Pierson (3Leaf Development)

简介
  在本文中,我们将讨论某些更为高级的DirectX原理,如转换、矩阵、拣选和剪辑。

代码清理
  这些更改已经集成到本文的代码中。

我更新到了2005年6月发布的DirectX SDK版本。我的确遇到与添加到全局程序集缓存(GAC)中的DirectX程序集相关的问题,因此如果您遇到了任何生成问题,请将引用更新为:C:\Program Files\Microsoft DirectX 9.0 SDK (June 2005)\Developer Runtime\x86\DirectX for Managed Code。
在上一篇文章中,使用了一个#if DEBUG将PresentParameters的IsWindowed设置包括在其中。为了启用此选项,必须转到项目设置,然后在其中启用它:
右键单击BattleTank2005项目并选择“属性”。
选择“生成”选项卡。
在常规部分选中“定义DEBUG常量”选项。
将device.Clear方法中的颜色从DarkBlue更改为Black。
将以下代码添加到GameEngine构造函数的最后,以使窗体具有更为标准化的大小。
// force the window to a standard size
// the provides the correct aspect ratio of 1.33
this.Size = new Size ( 800, 600 );

将窗体的启动位置更改为CenterScreen。
右键单击GameEngine窗体,并选择“视图设计器”。
在“属性”中将“StartPosition”属性更改为“CenterScreen”
  现在我们可以实际在屏幕上进行绘制并开始创建游戏了。

绘制目标瞄准器十字准线
  在BattleTank 2005中,我们将坐在在坦克中,通过瞄准器的取景范围观察周围的环境。我们使用目标瞄准器十字准线帮助更方便地打击和摧毁敌人。在现实世界里,目标瞄准器十字准线固定在加农炮的光学部件中,所以始终可见,当通过取景器往外看时将与平视标志(HUD)位于同一位置。

  绘制我们游戏中的瞄准器十字准线时,有两种选择。

使用屏幕坐标将其直接绘制到屏幕中央。
使用世界坐标对其进行绘制,并确保视点位于合适位置,以使其保持可见且位于中央位置。
  此处需要就速度和可扩展性进行权衡。如果我们选择第一个选项,因为其已经使用屏幕坐标进行表示了,所以无需转换坐标,那么速度就要快一些。实际上,这么做是将瞄准十字线从我们创建的模型空间中移除了。不足在于,如果我们后来要更改游戏,允许玩家离开坦克,则在玩家离开坦克时,这个瞄准十字线仍然可见。第二个选项需要对坐标进行调整和转换,但为后面更改游戏提供了最大的灵活性。

  我将使用第二个选项,因为它的灵活性最大,在确定游戏速度很慢之前,我将不会浪费时间对游戏进行优化。编写游戏时,您将会面对与此类似的设计选择,必须对其间的得失有所了解。

  继续之前,让我们定义一个模型,以便更为方便地描述和(希望能)理解将在本文中讨论的术语。

模型
  想象面前有一个空房间。我将确定房间中X、Y、Z原点的位置。让我们假设房间前部的左下角为该环境中的原点。而这个房间则成了我的世界空间。(真正的世界空间是无限大的,但我们现在可以假定其为房间内的有限空间。)

  世界空间:无限的三维笛卡尔空间。您可以将物体放置在这个世界的任何位置以创建需要的环境。

  现在我将一把椅子放在房间中预定义的位置。椅子上的每个点都可以使用一个笛卡尔坐标集(X、Y、Z)以及关于每个点的可选信息进行准确地描述,其中的可选信息可以包括颜色(Color)和材质(Tu、Tv)。回想一下上一篇文章的内容,您将发现这使得每个点都成了一个顶点。因为存在很多独立的顶点,所以将它们存储在顶点数组中。当这个数组加载到内存中时,它就成立一个顶点缓冲区。

顶点缓冲区
  顶点缓冲区非常适合供DirectX需要执行的复杂转换使用。DirectX提供了大量预定义的顶点类型,用以代表最常见的顶点格式。这些类型在CustomVertex类中被定义为结构。

  我们将使用PositionColored顶点绘制瞄准十字线。此类顶点提供X、Y、Z属性和Color属性,这正是我们所需要的。此类顶点还定义了坐标的世界空间而不是屏幕空间,这也是我们决定采用的。

  将以下方法添加的GameEngine类中,放置于OnPaint方法后。

private CustomVertex.PositionColored[] CreateCrossHairVertexArrayTop()
{
    CustomVertex.PositionColored[] crossHairs =
        new CustomVertex.PositionColored[7];
    float zval = 0f; // top of targeting crosshairs

    crossHairs[0].Position = new Vector3(-1f, 1f, zval);
    crossHairs[1].Position = new Vector3(-1f, 2f, zval);
    crossHairs[2].Position = new Vector3(0f, 2f, zval);
    crossHairs[3].Position = new Vector3(0f, 3f, zval);
    crossHairs[4].Position = new Vector3(0f, 2f, zval);
    crossHairs[5].Position = new Vector3(1f, 2f, zval);
    crossHairs[6].Position = new Vector3(1f, 1f, zval);

    crossHairs[0].Color = Color.Green.ToArgb();
    crossHairs[1].Color = Color.Green.ToArgb();
    crossHairs[2].Color = Color.Green.ToArgb();
    crossHairs[3].Color = Color.Green.ToArgb();
    crossHairs[4].Color = Color.Green.ToArgb();
    crossHairs[5].Color = Color.Green.ToArgb();
    crossHairs[6].Color = Color.Green.ToArgb();

    return crossHairs;
}

private CustomVertex.PositionColored[] CreateCrossHairVertexArrayBottom()
{
    // bottom of targeting crosshairs
    CustomVertex.PositionColored[] crossHairs =
        new CustomVertex.PositionColored[7];
    float zval = 0f; // bottom of targeting crosshairs

    crossHairs[0].Position = new Vector3(1f, -1f, zval);
    crossHairs[1].Position = new Vector3(1f, -2f, zval);
    crossHairs[2].Position = new Vector3(0f, -2f, zval);
    crossHairs[3].Position = new Vector3(0f, -3f, zval);
    crossHairs[4].Position = new Vector3(0f, -2f, zval);
    crossHairs[5].Position = new Vector3(-1f, -2f, zval);
    crossHairs[6].Position = new Vector3(-1f, -1f, zval);

    crossHairs[0].Color = Color.Green.ToArgb();
    crossHairs[1].Color = Color.Green.ToArgb();
    crossHairs[2].Color = Color.Green.ToArgb();
    crossHairs[3].Color = Color.Green.ToArgb();
    crossHairs[4].Color = Color.Green.ToArgb();
    crossHairs[5].Color = Color.Green.ToArgb();
    crossHairs[6].Color = Color.Green.ToArgb();

    return crossHairs;
}

  请记住,每个顶点的Position属性的坐标均采用世界空间坐标定义。稍后我们会将其转换为屏幕坐标。另外,还要注意,必须调用ToArgb方法将Color转换为Direct所要求的32位整数格式。

  继续之前,我们需要让设备知道我们所选择的顶点类型。通过将Device类的VertexFormat属性设置为我们使用的顶点的Format属性,就可以完成这一工作。此属性确定设备将要使用的固定功能管线。现在不必关心这个的具体用途;只需要知道我们将要使用位置与着色 管线即可。

  在OnPaint方法中,紧跟device.Clear方法后添加以下代码。

device.VertexFormat = CustomVertex.PositionColored.Format;

  定义了十字准线后,我们必须告知设备将顶点缓冲区中描述的对象实际呈现到屏幕上。可以使用设备类的DrawUserPrimitives方法完成这项工作。那么什么是基元(Primitives)呢?

绘制基元
  绘制基元是定义单个三维物体的顶点的集合。DirectX中有六种基元可在PrimitiveType枚举中列出。

Line List:主要用于在屏幕上添加平视标志(HUD)信息。其基元数等于点的数量除以二。要此类型正常工作,点数必须为偶数。
Line Strip:此类型与Line List的用法一样,但呈现的是单条连续的直线。其基元数等于顶点数减去1。
Point List:主要用于呈现颗粒图像中的各个点,如爆炸和夜空中的星星。其基元数等于顶点缓冲区中的点的数量。
Triangle Fan:绘制椭圆对象时,这个特别有用。
Triangle List:这是最常用的基元。其基元数为顶点的数量除以三。
Triangle Strip:呈现直角物体时,这个最为有用。
  在本例中,因为十字准线是一个HUD,所以我们选择LineStrip类型对其进行绘制。在OnPaint方法中,紧跟device.Clear方法后添加以下代码。

device.DrawUserPrimitives(PrimitiveType.LineStrip, 6,
    CreateCrossHairVertexArrayTop());
device.DrawUserPrimitives(PrimitiveType.LineStrip, 6,
    CreateCrossHairVertexArrayBottom());

  DrawUserPrimitives方法要求传入PrimitiveType、要呈现的基元数和对象的顶点数据源。由于我们使用LineStrip基元,每个顶点缓冲区中的七个点将创建六根直线。

  到此时为止,我们还没有在屏幕上绘制任何东西,只是进行了设备清除;因此我们需要告知设备我们计划进行的操作。可以使用Device类的BeginScene方法完成这项工作。

BEGINSCENE/ENDSCENE
  正如我在第一篇文章中提到的,电影和DirectX游戏之间有很多的相关性。我们遇到的第一个是帧。现在我们将添加场景,稍后还将添加摄像机。

  我们使用设备的BeginScene和EndScene方法定义场景的开始点和结束点。BeginScene通过锁定后台缓冲区使设备准备好,以进行后续的操作。EndScene方法告知设备已经完成绘制,并取消后台缓冲区的锁定。必须始终在调用BeginScene之后调用EndScene,否则后台缓冲区将保持锁定状态。BeginScene和EndScene方法与Present方法紧密协作,一起管理后台缓冲区;如果其中一个方法失败,其他两个方法也会失败。

  在GameEngine类的OnPaint方法中添加对BeginScene和EndScene的调用。对BeginScene的调用需要紧跟在Clear方法调用之后。

// Tell DirectX we are about to draw something
device.Clear(ClearFlags.Target, Color.Black, 1.0f, 0);
device.BeginScene();

  而EndScene方法需要在紧靠Present方法调用之前调用。

// Tell DirectX that we are done drawing
device.EndScene();

// Flip the back buffer to the front
device.Present();

  现在几乎已经快完成了。剩下的最后一步就是将每个三维对象的世界坐标转换为屏幕坐标。

  在上一篇文章中,我们讨论了许多您需要了解的DirectX术语。新术语我们已经了解得差不多了,不过在成功将3D世界呈现到屏幕上之前,我们需要了解一下最后一组定义。

灯光、摄像机、操作
  在模型中,我们已经使用笛卡尔坐标和颜色只描述了椅子上的每个点,并将这些点存储在了顶点缓冲区中。但DirectX仍然不能呈现这把椅子。为什么呢?缺少的信息是我们在房间中的位置和视角方向。我们还需要确定如何处理投影。只有获得了这些信息,才能将3D世界转换为屏幕上显示的2D图像。

  在DirectX中,我们(观察者)的位置称为摄像机位置,由视图矩阵 进行定义。从现在开始,需要将视角 和摄像机 两个术语看作是同义词。投影是应用到物体上的直线距离,与摄像机的镜头设置相一致。

  在创建出色的3D游戏时,摄像机是非常强大的工具。可以将摄像机绑定到移动的对象上以获得实际在其中的感觉,或者稍微偏后一点以获得追逐视角。还可以在您的世界中设置多个静止不动的摄像机,通过在摄像机之间切换查看场景中的动作。

  将世界坐标转换为屏幕坐标过程中涉及到的所有计算都是使用矩阵 执行的。这些计算称为转换。几乎DirectX中执行的所有密集型操作都是使用矩阵进行坐标转换的。

  转换:转换操作将根据指定的视角、投影类型和世界转换更改三维物体的坐标。这些转换均通过使用一个4x4的矩阵集完成。

  矩阵:由数字组成的方形表格,在转换过程中非常有用。要了解使用矩阵进行转换的准确细节,可能需要很长时间。了解其如何工作非常重要,但不用自己进行任何手动计算,因此我们将跳过具体的细节。Matrix类包含很多进行矩阵操作的最常用方法。第一步是将摄像机放入我们的三维世界中,并设定其方向。

视角转换
  视图矩阵定义摄像机的位置和方向(通过指定目标确定)。视图矩阵还包括一个用于确定在我们的世界中哪个方向是向上的值。这几乎通常都是Y轴。可以定义自己的矩阵,也可以使用内置的Matrix.LookAtLH和Matrix.LookAtRH方法。由于我们使用左手坐标系统,我们自然会使用LookAtLH方法(LH表示左手)。由于没有显式定义视角,DirectX会使用默认视角。

  在GameEngine类的OnPaint方法中,紧跟device.Clear方法调用后添加以下代码。

device.Transform.View = Matrix.LookAtLH(
    new Vector3(0, 0, 5f),
    new Vector3(0, 0, 0),
    new Vector3(0, 1, 0));

  在Battle Tank 2005中,我们将把摄像机放置在原点(0,0)稍微朝向Z轴的位置。那么,我们将摄像机放置在原点,并通过为Y提供一个值以表示Y轴方向为向上。

  放置了摄像机并确定其方向后,我们需要定义要使用的投影。

投影转换
  在DirectX中,有可以选择两种类型的投影:

透视投影
正交投影
  透视投影是最常用的投影,是我们看世界的正常方式。在此投影中,物体离我们越远,显得越小,到一定距离的时候会发生变形(就像路看起来像在地平线上聚成了一点一样)。

  另一方面,正交投影会忽略距离(Z值),无论物体距离摄像机有多远,其大小都不会变化。

  在BattleTank 2005中,我们将要使用透视投影。投影将确定在视图截锥中转换物体顶点的方式。视图截锥定义了一个三维空间,在其中的物体对于摄像机可见。可以将这个可视化为一个去掉了顶部的金字塔。金字塔的底部是远平面,而顶部为近平面,视场角是顶点处的一个角。为了执行此转换,我们需要知道四方面的信息。

视场角(FoV):通常为45度或Math.PI / 4。减小FoV值就像对场景进行放大,而增加FoV值则像进行缩小。使用小于45度的值就像通过鱼眼镜头观看场景。
画面比率:这与电视或显示器的画面比率一样,通常计算为视口宽度/高度。视口实际就是我们在其上呈现游戏的窗体。计算机屏幕的标准画面比率为1.33(640x480或1280x1024)。
近剪切面:将不会呈现比此平面更为接近摄像机的物体。
远剪切面:将不会呈现比此平面更远的物体。
  弧度:在DirectX中,大部分角都以弧度表示,而不是用度数表示。若要将度数转换为弧度,直接将度数值乘以p/180即可,其中n是角的度数值。DirectXDX库在Geometry类中包含用以执行该转换的Helper函数。

  通过将这些值和一些数学公式一起使用,DirectX可将每个对象的顶点从世界坐标转换为屏幕坐标。就某种程度而言,此转换就是我们在手动绘制三维空间图形时在大脑中进行的转换。我们会将距离远的物体画得小一些,尽管实际物体的大小并没有真正发生,但我们会将较远的物体进行一些扭曲。

  在GameEngine类的OnPaint方法中,将以下代码添加到紧跟前一步骤添加的device.Transform.View方法调用之后。

device.Transform.Projection = Matrix.PerspectiveFovLH(
    (float)Math.PI / 4, this.Width / this.Height, 1.0f, 100.0f);

  对于BattleTank 2005,我们将使用FoV的传统设置,采用45度的角度值。接下来,我们将设置画面比率基,然后将视图截锥设置为在坐标空间中的1到100之间。剪切面的值定义可视区域的大小,能够表示任何希望表示的物体。在BattleTank 2005中一个单位值等于10米,因此我们的游戏场地大小为1公里。

  定义视图截锥时,必须使用一些常识。我们可以简单地声明每个单位值等于一公里,使得可见区域的深度为100公里。不过,如果我们将坦克设置为正常大小,几公里外就不可能看到了,因此,如果玩家看不到它们又何必浪费资源将其呈现在屏幕上呢?

  视图矩阵和投影矩阵描述摄像机和摄像机镜头,而世界转换矩阵则负责将模型空间坐标转换为世界空间坐标。这些世界空间坐标然后将在视图和投影转换中转换为屏幕坐标。

世界转换
  最后一个转换是世界转换。这会对我们从模型空间转换到世界空间的对象进行转换。

  在世界转换中可以对每个对象进行移动、旋转和缩放。设置转换后,这个转换将应用到所有绘制的对象,直到指定了新的转换为止。

  模型空间:我们通过参照模型定义每个顶点,从而定义每个对象。

  在BattleTank 2005中,此时我们将不使用世界转换,但我已在本文的代码中包含了一个转换,使用了一个测试立方体供您做试验。我建议您可以试试更改所有三个转换的值,看一看屏幕上显示的结果如何。这是最简单的理解各种设置功能的方法。有关如何进行此操作的详细信息,请参见“试验”部分。

灯光
  只要使用世界坐标绘制基元,DirectX就会使用照明情况确定每个象素的颜色。但是,由于我们尚未定义光源,此时可以直接将灯光关掉。如果没有将照明关掉,DirectX将假设场景中没有灯光,从而将每个象素呈现为黑色。

  在GameEngine类的OnPaint方法中,将以下代码添加到紧跟前一步骤添加的device.Transform.Projection方法调用之后。

// turn off the light source
device.RenderState.Lighting = false;

  处理控制照明之外,RenderState还允许对拣选行为进行控制。

拣选
  拣选是将视图截锥之外的整个物体(与剪辑不一样,剪辑只移除部分)从屏幕上移除,从而减少需要呈现的物体的总数。整体目标当然是速度;通过将不重要的物体从屏幕上移除,可以更快地进行呈现。

  剪辑:剪辑会丢弃落入视图截锥之外的任何单个物体的部分。剪辑由DirectX自动进行管理,不需要进一步的干预。

  绘制三维物体时,DirectX不会呈现那些不面对摄像机的对象表面的组成基元(三角形)。这称为背面拣选。

  DirectX将通过使用顶点的顺序(绕向)确定物体的哪一面对着摄像机。如果选择顺时针方向或逆时针方向,则组成相反面的顶点位于物体的背面,将被拣选掉。默认模式为逆时针拣选,因此,需要确保顶点的按顺时针顺序进行定义。

  通过将Cull枚举设置之一赋予device.RenderState.CullMode属性,可以设置RenderState中的拣选选项。

  在GameEngine类的OnPaint方法中,将以下代码添加到紧跟前一步骤添加的device.RenderState.Lighting代码之后。

// Turn off backface culling
device.RenderState.CullMode = Cull.None;

  在本文所附带的代码中,为了方便您对我们刚刚讨论过的各种设置进行试验,我添加了几个额外的项。

结束语
  在本专题中,我们讨论了很多基础问题。到目前为止,您应该已经知道如何创建设备并将其与Windows窗体挂钩以及如何使用OnPaint和Invalidate方法创建游戏循环。您应该能够使用顶点缓冲区和DrawUserPrimitives方法创建和绘制自己的三维物体、设置摄像机以及将模型转换为世界空间。

  如果此时有不清楚的地方,您应查看一下DirectX SDK或转到网站上列出的资源之一进行深入了解。一旦完全了解了这些原则和原理,剩下的游戏开发过程就简单多了。

  现在,您就可以快乐地进行编码了!


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dotnet_editor/archive/2005/08/18/457880.aspx

 

补充例如:

int NumberItems=12;

                case 0: // Points
                    device.DrawPrimitives(PrimitiveType.PointList, 0, NumberItems);
                    if (needRecreate)
                    {
                        // After the primitives have been drawn, recreate a new set
                        OnVertexBufferCreate(vb, null);
                        needRecreate = false;
                    }
                    break;
                case 1: // LineList
                    device.DrawPrimitives(PrimitiveType.LineList, 0, NumberItems / 2);
                    needRecreate = true;
                    break;
                case 2: // LineStrip
                    device.DrawPrimitives(PrimitiveType.LineStrip, 0, NumberItems - 1);
                    break;
                case 3: // TriangleList
                    device.DrawPrimitives(PrimitiveType.TriangleList, 0, NumberItems / 3);
                    break;
                case 4: // TriangleStrip
                    device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, NumberItems - 2);
                    break;
                case 5: // TriangleFan
                    device.DrawPrimitives(PrimitiveType.TriangleFan, 0, NumberItems - 2);
                    break;

你可能感兴趣的:(DI)