65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 XNA 为 Windows 8 应用商店应用和 Windows Phone 应用进行 3D 基础知识

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (7投票s)

2013年2月27日

CPOL

9分钟阅读

viewsIcon

33023

downloadIcon

651

如何使用 XNA 为 Windows 8 应用商店应用和 Windows Phone 应用编程硬件加速的 3D 图形。

介绍 

XNA 是一个 .NET 托管框架,可简化 3D 图形编程。它将低级 DirectX API 封装在一组 .NET 类中,隐藏了许多复杂性,同时不会严重影响运行时性能或限制 DirectX 的功能。XNA 的编程比 DirectX 容易得多,但它仍然是一个低级、功能强大的 API。

XNA 最初由微软于 2006 年推出,旨在使独立游戏开发者能够使用 C# 和 .NET 为 Xbox 和 Windows PC 创建高性能游戏。后来的版本也为 Silverlight 5 和 Windows Phone 7 提供了支持。

微软目前不提供适用于 Windows 8 应用商店应用的 XNA 版本,但有一个名为 MonoGame 的出色的开源替代方案。

本文介绍了 XNA 在三个平台上的支持。

  • Windows Phone 7 
  • Windows 8 应用商店应用
  • Windows Phone 8

然后,对于其中两个平台(Phone 7 和 Windows 8 应用商店应用),它会呈现最简单的 XNA 程序:一个基本的旋转三角形,尽管具有完整的 3D 阴影效果。这是经典“Hello World”程序的 3D 等效版本。它包含大约 50 行代码,并解释了每一行的原因。

 

Windows Phone 7

微软完全支持 XNA 用于 Windows Phone 7 应用,并且由于 Phone 7 应用也运行在 Windows Phone 8 上,因此您自动获得了对该平台的兼容性。当然,在 Phone 7 应用中,您无法访问 Windows Phone 8 的新功能。

Visual Studio 为 Phone 7 XNA 应用提供了两个项目模板:

  • Windows Phone Game(在 XNA Game Studio 部分)
  • Windows Phone XAML and XNA App(在 Windows Phone 部分)

如果您的 3D 应用程序或游戏不需要任何 Phone 8 特定功能,那么将其开发为 Phone 7 应用是一个不错的选择。它可以为您提供最广泛的市场覆盖,并且仅需一个代码库。

Windows 8 应用商店应用

微软不为 Windows 应用商店应用提供 XNA。您必须改用开源的 MonoGame 框架。好消息是 MonoGame 易于设置和使用,并且与微软的 XNA 框架提供了非常好的兼容性。如果您有现有的 XNA 代码,那么将其移植到 MonoGame 可能会非常容易。

MonoGame 的另一个巨大优势是它也可用于 iPhone 和 Android,这为使用 C# 和 .NET 编写高性能、跨平台 3D 应用程序或游戏带来了令人兴奋的前景。

Windows Phone 8

微软不为 Windows Phone 8 提供 XNA 支持,但 MonoGame 再次填补了这一空白。如果您需要创建 Phone 8 特定 3D 应用程序或游戏,例如,如果您想利用新的 Phone 8 功能,那么 MonoGame 是最佳选择。

本文未详细介绍此选项,但可以肯定的是,您的 Windows 8 应用商店应用的 MonoGame 代码在 Windows Phone 8 上工作时几乎不需要任何更改。

3D Hello World 程序 

本文不介绍如何设置 XNA 或 MonoGame 的开发环境,网上已经有很多相关信息。 

相反,我们将直接深入代码。

这是 Windows 8 应用商店应用的完整源代码。 

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Win8StoreApp_Xna_Triangle
{
  public class Game1 : Game
  {
    private float angleRadians;

    public Game1()
    {
      new GraphicsDeviceManager(this);
    }

    protected override void Update(GameTime gameTime)
    {
      angleRadians = (float)gameTime.TotalGameTime.TotalSeconds * 4;
    }

    protected override void Draw(GameTime gameTime)
    {
      GraphicsDevice gd = this.GraphicsDevice;
      gd.Clear(Color.White);
      gd.RasterizerState = RasterizerState.CullNone;

      var vertices = new VertexPositionNormalTexture[]{
        new VertexPositionNormalTexture(new Vector3(-1, -1, 0),Vector3.Forward,Vector2.Zero),
        new VertexPositionNormalTexture(new Vector3(0, 1, 0),Vector3.Forward,Vector2.Zero),
        new VertexPositionNormalTexture(new Vector3(1, -1, 0),Vector3.Forward,Vector2.Zero)};
      var vb = new VertexBuffer(gd, VertexPositionNormalTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly);
      vb.SetData(0, vertices, 0, vertices.Length, 0);
      gd.SetVertexBuffer(vb);

      var be = new BasicEffect(gd);
      be.EnableDefaultLighting();

      var blueTexture = new Texture2D(gd, 1, 1, false, SurfaceFormat.Color);
      blueTexture.SetData<Color>(new Color[] { Color.Blue });
      be.Texture = blueTexture;
      be.TextureEnabled = true;

      float aspectRatio = gd.Viewport.AspectRatio;
      be.World = Matrix.CreateRotationY(angleRadians);
      be.View = Matrix.CreateLookAt(new Vector3(0, 0, 5.0f), Vector3.Zero, Vector3.Up);
      be.Projection = Matrix.CreatePerspectiveFieldOfView(0.85f, aspectRatio, 0.01f, 1000.0f);

      be.CurrentTechnique.Passes[0].Apply();
      gd.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
    }
  }
}

Windows Phone 7 版本完全相同,只是多了一个命名空间。

using Microsoft.Xna.Framework.Input; 

... 以及几行用于处理硬件 Back 按钮的代码: 

protected override void Update(GameTime gameTime)
{
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
    this.Exit();
  angleRadians = (float)gameTime.TotalGameTime.TotalSeconds * 4;
}

代码行数不多,但内容很多,所以我们来分解一下。

绘制图元 

XNA 是一个相当底层的框架,直接与图形硬件交互,所以它唯一知道如何绘制的就是三角形和线条。在我们的程序中,执行绘制的代码是:

gd.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

这指示 GPU 使用预加载的顶点缓冲区绘制三角形。

顶点缓冲区

XNA 提供了几种内置的顶点选项,您也可以定义自己的自定义类型。所有顶点都使用 Vector3 结构定义 3D 位置,并且可以具有颜色、纹理坐标和光照“法线”等附加属性。我们希望我们的程序具有逼真的 3D 光照,因此我们使用 VertexPositionNormalTexture 结构。

我们的程序只有一个三角形,所以我们创建了一个包含三个顶点的数组。我们定义的 3D 位置坐标使三角形位于 XY 平面上,并且我们定义的“法线”与此垂直,即沿着 Z 轴“向前”。我们定义纹理坐标为 (0,0),因为我们将使用一个像素的纯色纹理位图。

一旦我们有了顶点数组,我们就将其包装在 VertexBuffer 类中并将其发送到 GPU。

var vertices = new VertexPositionNormalTexture[]{
  new VertexPositionNormalTexture(new Vector3(-1, -1, 0),Vector3.Forward,Vector2.Zero),
  new VertexPositionNormalTexture(new Vector3(0, 1, 0),Vector3.Forward,Vector2.Zero),
  new VertexPositionNormalTexture(new Vector3(1, -1, 0),Vector3.Forward,Vector2.Zero)};
var vb = new VertexBuffer(gd, VertexPositionNormalTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly);
vb.SetData(0, vertices, 0, vertices.Length, 0);
gd.SetVertexBuffer(vb); 

GraphicsDevice 类 

GraphicsDevice 类是 XNA 的核心,它用于控制 GPU。它只有一个实例,您可以通过 Game 对象的属性获取该实例。 

GraphicsDevice 类具有绘制图元和设置顶点缓冲区(如上所述)的方法。它还有许多其他属性和方法,用于控制 3D 渲染过程的详细信息。对于我们的简单程序,我们将所有设置保留为默认值,除了“RasterizerState”,我们将其更改为“CullNone”。默认情况下,它不会绘制三角形的背面,因为它们通常位于 3D 对象内部且不可见,但我们的 3D 模型是一个单一的三角形,我们希望能够看到它的两面。

GraphicsDevice gd = this.GraphicsDevice;
gd.Clear(Color.White);
gd.RasterizerState = RasterizerState.CullNone; 

效果

XNA 提供了五种不同的“Effect”类供选择,它们封装了与 GraphicsDevice 通信的许多低级细节。我们将使用“BasicEffect”类,它实际上非常强大——它内置了非常出色的 3D 光照设置。

var be = new BasicEffect(gd);
be.EnableDefaultLighting(); 

纹理

在 XNA 中,3D 场景由叠加纹理的三角形网格组成。纹理只是 2D 位图——XNA 类是 Texture2D。它们通过 Effect 类发送到 GraphicsDevice,每个三角形顶点指定其在纹理中的 2D 坐标。按照惯例,这些纹理坐标称为 U 和 V。

在许多 XNA 应用程序(尤其是游戏)中,纹理是在离线创建然后运行时加载的。对于我们简单的程序,我们以编程方式创建了一个单像素的纯色纹理。

var blueTexture = new Texture2D(gd, 1, 1, false, SurfaceFormat.Color);
blueTexture.SetData<Color>(new Color[] { Color.Blue });
be.Texture = blueTexture;
be.TextureEnabled = true; 

矩阵和 3D 变换 

这是 3D 编程的核心。我们的顶点缓冲区中的顶点定义了三角形在我们 3D 虚拟世界中三个角的位置,但在显示任何内容之前,需要将这些 3D 坐标转换为 2D 屏幕坐标。

这个过程分为三个阶段,每个阶段都使用 Matrix 类定义的 3D 变换。Matrix 类实际上代表一个 4x4 的浮点数矩阵,但其主要功能是通过平移、旋转和缩放的基本操作来变换由 Vector3 类定义的 3D 坐标。

float aspectRatio = gd.Viewport.AspectRatio;
be.World = Matrix.CreateRotationY(angleRadians);
be.View = Matrix.CreateLookAt(new Vector3(0, 0, 5.0f), Vector3.Zero, Vector3.Up);
be.Projection = Matrix.CreatePerspectiveFieldOfView(0.85f, aspectRatio, 0.01f, 1000.0f); 

第一步是从模型坐标转换为世界坐标。这称为世界变换。在我们的程序中,我们只有一个模型,即一个单一的三角形,世界变换用于定义它在我们 3D 虚拟世界中的位置和方向。如果我们的 3D 场景中有多个模型,或者同一个模型的多个实例,它们将各自拥有自己的世界变换。我们只有一个模型,但它会实时旋转。因此,我们使用世界变换来表示基于经过时间计算的旋转。

第二步是从以我们的虚拟世界“原点”为中心的世界坐标转换为以我们的视点或相机位置为中心的视图坐标。这称为视图变换。Matrix 类有一个方便的方法(CreateLookAt),可以根据相机位置和相机所看点的中心点来创建视图变换。

第三步是将我们虚拟世界中的 3D 坐标转换为 2D 屏幕坐标。为了保持一致性,此步骤也使用 Matrix 类完成,因此输出是 3D 坐标,但仅使用 X 和 Y 值来创建屏幕上的 2D 渲染图像。此步骤称为投影变换,XNA 提供了两种选择:正交和透视。使用正交选项时,离相机更远的物体不会显得更小。透视选项是更“自然”的视图,其中远处的物体显得更小。Matrix 类还提供了一个方便的方法(CreatePerspectiveFieldOfView),可以根据相机属性(尤其是镜头的角度(即广角或长焦),以 Y 轴的弧度表示)来创建投影变换。

一旦设置了我们的三个变换矩阵,BasicEffect 类就会负责设置 GraphicsDevice 以将我们的 3D 场景渲染到 2D 屏幕上。

Effect Passes 和 Techniques 

所有 XNA 的内置 Effect 类都继承自 Effect 基类,该基类封装了“Techniques”(技术)和“Passes”(通道)的概念。

Techniques 提供替代的渲染效果,可以在绘制时选择,也许取决于正在绘制的对象类型。

Passes 用于多阶段渲染。

BasicEffect 类提供一个单一的技术,该技术是默认选择的,并且只有一个通道。在发出任何绘制命令之前,必须“Apply”(应用)一个通道。我们的程序通过以下代码行来执行此操作:

be.CurrentTechnique.Passes[0].Apply();
gd.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); 

但您经常会看到这种形式的 XNA 代码:

foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
  pass.Apply();
  // Drawing commands
} 

我们使用更简单的形式,因为我们知道 BasicEffect 只有一个 Pass。如果我们使用的是具有多个通道的效果,或者我们想将代码通用化以使其适用于任何 Effect 类,我们将使用 foreach 循环。

优化 Draw 方法 

我们的程序旨在尽可能短小精悍,以阐明 XNA 的基本原理。

在实际程序中,您需要通过将一次性设置代码分离出来来优化关键的 Draw 处理程序。Draw 处理程序每帧执行一次,例如每秒 60 次,因此您需要使其尽可能轻量。特别是,您应该避免在该函数中实例化任何对象。

结论 

这完成了我们对一个非常简单的 XNA 程序的概述。

虽然它只涵盖了 XNA 中可用功能的一小部分,但其中包含了许多关键概念,您可以使用此处介绍的类和方法来构建相当复杂的 3D 程序。

© . All rights reserved.