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

使用Silverlight-5和XNA实现3D基础

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (9投票s)

2011 年 11 月 24 日

CPOL

10分钟阅读

viewsIcon

61022

downloadIcon

1759

一个最小的 3D 程序,展示了如何在 Silverlight-5 中使用 XNA,并对核心概念进行了全面解释。

引言

本文介绍了最简单的 Silverlight/XNA 程序:一个基本的旋转三角形,但具有完整的 3D 照明效果。它是经典“Hello World”程序的 3D 等效版本。它包含大约 50 行代码,并且解释了每行代码背后的原理。

screenshot.png

背景

Silverlight-5 最大的新功能之一是使用 XNA 框架实现完全的 GPU 加速 3D 图形。

Silverlight 和 XNA 的结合将使一整类新的 3D 应用程序能够直接从网络运行,包括科学、工程、产品营销、商业和地理空间领域的应用程序。

XNA 框架为程序员提供了强大的能力,可以相当直接地控制图形硬件,并且可以实现一些令人印象深刻的结果。但是,尽管 XNA 大大简化了 3D 编程,特别是与 DirectX 或 OpenGL 相比,它仍然是一种相对低级的技术,没有 3D 编程经验的 Silverlight 程序员可能会面临陡峭的初始学习曲线。更糟糕的是,由于这是一种新技术,文档目前有些零散,并且可用的少数示例并不总是显示最简单的方法。

入门

您需要首先设置 Silverlight 5 开发环境,网上已经有大量关于此的信息,因此无需在此处重复。

设置好工具后,以下是构建第一个 XNA 应用程序的步骤:

  1. 启动 Visual Studio 并创建一个新的 Silverlight 5 应用程序。
  2. 将一个“DrawingSurface”控件拖放到主页面上。这就是所有 3D 魔法发生的地方。
  3. 如果在工具箱中看不到“DrawingSurface”控件,您可能需要右键单击并选择“选择项目...”来添加它。
  4. 为 Drawing Surface 添加一个“Draw”事件。
  5. 在事件处理程序中,添加两行代码。
    GraphicsDevice g = GraphicsDeviceManager.Current.GraphicsDevice;
    g.Clear(new Color(0.8f, 0.8f, 0.8f));
  6. 添加两个“using”指令。
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
  7. 删除不必要的“using”指令,特别是 System.Windows.Media,否则您将有两个冲突的 Color 定义,并且项目将无法编译。
  8. 将以下内容添加到 HTML 或 ASPX 测试页面的 <object> 元素中:
    <param name="EnableGPUAcceleration" value="true" />
  9. 按 F5,您应该会在浏览器中看到一个浅灰色矩形。

当然,这并非普通的 Silverlight 灰色矩形,它是一个 GPU 加速的 XNA 矩形!

到目前为止,这已经足够简单了,现在让我们添加一些 3D。

添加 3D

这是完整的代码

using System.Windows.Controls;
using System.Windows.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Silverlight5_3d
{
  public partial class MainPage : UserControl
  {
    private float aspectRatio = 1f;

    public MainPage()
    {
      InitializeComponent();
    }

    private void drawingSurface1_SizeChanged
    (object sender, System.Windows.SizeChangedEventArgs e)
    {
      aspectRatio = (float)(drawingSurface1.ActualWidth / drawingSurface1.ActualHeight);
    }

    private void drawingSurface1_Draw(object sender, DrawEventArgs e)
    {
      GraphicsDevice g = GraphicsDeviceManager.Current.GraphicsDevice;
      g.RasterizerState = RasterizerState.CullNone;
      g.Clear(new Color(0.8f, 0.8f, 0.8f, 1.0f));

      VertexPositionNormalTexture[] 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)};
      VertexBuffer vb = new VertexBuffer(g,VertexPositionNormalTexture.VertexDeclaration,
                    vertices.Length,BufferUsage.WriteOnly);
      vb.SetData(0, vertices, 0, vertices.Length, 0);
      g.SetVertexBuffer(vb);

      BasicEffect basicEffect = new BasicEffect(g);
      basicEffect.EnableDefaultLighting();
      basicEffect.LightingEnabled = true;

      basicEffect.Texture = new Texture2D(g, 1, 1, false, SurfaceFormat.Color);
      basicEffect.Texture.SetData<color>(new Color[1]{new Color(1f, 0, 0)});
      basicEffect.TextureEnabled = true;
      
      basicEffect.World = Matrix.CreateRotationY((float)e.TotalTime.TotalSeconds * 2);
      basicEffect.View = Matrix.CreateLookAt(new Vector3(0, 0, 5.0f), 
                    Vector3.Zero, Vector3.Up);
      basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView
                    (0.85f, aspectRatio, 0.01f, 1000.0f);

      basicEffect.CurrentTechnique.Passes[0].Apply();
      g.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
      e.InvalidateSurface();
    }
  }
}

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

绘制图元

XNA 是一个相当低级的框架,直接与图形硬件协同工作,因此它唯一知道如何绘制的东西是三角形和线条。在我们的程序中,执行绘制的代码是:

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

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

顶点缓冲区

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

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

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

VertexPositionNormalTexture[] 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)};
VertexBuffer vb = new VertexBuffer
  (g,VertexPositionNormalTexture.VertexDeclaration,
    vertices.Length,BufferUsage.WriteOnly);
vb.SetData(0, vertices, 0, vertices.Length, 0);
g.SetVertexBuffer(vb);

GraphicsDevice 类

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

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

GraphicsDevice g = GraphicsDeviceManager.Current.GraphicsDevice;
g.RasterizerState = RasterizerState.CullNone;
g.Clear(new Color(0.8f, 0.8f, 0.8f, 1.0f));

效果

XNA 提供了五种不同的“Effect”类选择,它们封装了与 GraphicsDevice 通信的许多低级细节。我们将使用“BasicEffect”类,它实际上相当强大——它标准地包含了一个非常好的 3D 照明设置。

BasicEffect basicEffect = new BasicEffect(g);
basicEffect.EnableDefaultLighting();
basicEffect.LightingEnabled = true;

为了实现终极的低级控制,可以通过直接使用 GraphicsDevice 对象,而不使用内置的 Effect 类来编程 XNA。您甚至可以编程自己的自定义像素着色器和顶点着色器。但是,内置的 Effect 类,特别是 BasicEffect,使事情变得容易得多。

纹理

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

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

basicEffect.Texture = new Texture2D(g, 1, 1, false, SurfaceFormat.Color);
basicEffect.Texture.SetData<color>(new Color[1]{new Color(1f, 0, 0)});
basicEffect.TextureEnabled = true;
</color>

矩阵和 3D 变换

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

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

basicEffect.World = Matrix.CreateRotationY((float)e.TotalTime.TotalSeconds * 2);
basicEffect.View = Matrix.CreateLookAt(new Vector3(0, 0, 5.0f), Vector3.Zero, Vector3.Up);
basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView
                (0.85f, aspectRatio, 0.01f, 1000.0f);

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

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

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

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

效果通道和技术

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

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

通道用于多阶段渲染。

BasicEffect 类提供了一种技术,默认情况下会选中该技术,并且该技术只有一个通道。在发出任何绘制命令之前,有必要“应用”一个通道。我们的程序通过这行代码来实现这一点:

basicEffect.CurrentTechnique.Passes[0].Apply();

但您经常会看到以下形式的 XNA 代码:

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

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

Resize 事件和新的 Silverlight 5 线程模型

当我们创建投影变换时,我们需要提供 DrawingSurface 的宽高比 (宽度/高度)。

为了使我们的“Hello World”程序保持简单,我们可能会尝试在 Draw 事件处理程序中执行此操作。但这将不起作用:

Float aspectRatio = (float)(drawingSurface1.ActualWidth / drawingSurface1.ActualHeight);
basicEffect.Projection = 
    Matrix.CreatePerspectiveFieldOfView(0.85f, aspectRatio, 0.01f, 1000.0f);

它会抛出异常:

System.UnauthorizedAccessException: Invalid cross-thread access

原因在于 Silverlight 5 的新线程模型。以前,Silverlight 在其单一 UI 线程上执行所有操作,尽管您确实可以选择手动创建额外的线程。

Silverlight 5 现在在另一个称为“Composition”线程的线程上执行其低级图形工作。对于基于 XAML 的 Silverlight,您无需担心此更改,因为您的代码在 XAML 对象树上运行,并且不涉及其低级渲染。

然而,使用 Silverlight-XNA,您正在更低的层次上工作,并且 DrawingSurface 控件的 Draw 事件在合成线程上执行。如果您尝试从合成线程访问 XAML 控件,您将收到一个异常。

在我们的程序中,我们通过局部变量“aspectRatio”来避免这种情况,我们在 Resize 事件处理程序中更新它,然后在 Draw 事件处理程序中使用它。我们没有对这个变量进行任何锁定,假设一个 32 位浮点数是原子性的。

对于在合成线程和 UI 线程之间共享更复杂数据结构的生产代码,您需要非常小心地正确锁定。

Invalidate

程序中的最后一行代码是对以下内容的调用:

e.InvalidateSurface();

将此调用放在 Draw 事件处理程序的末尾会导致 Draw 事件立即再次触发。对于不断变化的 3D 场景(例如我们旋转的三角形),这提供了最流畅的操作。

如果您的场景是静态的,那么您无需不断重绘它,只需在有变化时调用 InvalidateSurface() 即可。

优化绘制方法

我们的程序旨在尽可能短小精悍,以说明 Silverlight-XNA 的要点。

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

结论

至此,我们完成了对一个非常简单的 Silverlight-XNA 程序的介绍。

尽管它只涵盖了 XNA 中可用的一小部分,但其中包含许多关键概念,您实际上可以使用此处展示的类和方法构建一个相当复杂的 3D 程序。

附录

本文撰写以来,微软已经发布了 Silverlight 5。

发布的版本比测试版具有更严格的安全限制,用户现在必须明确授予网站使用加速 3D 图形的权限。

您需要将以下代码添加到主页构造函数中以提示用户执行此操作:

public MainPage()
{
  InitializeComponent();
  if (GraphicsDeviceManager.Current.RenderMode != RenderMode.Hardware)
  {
    string message;
    switch (GraphicsDeviceManager.Current.RenderModeReason)
    {
      case RenderModeReason.Not3DCapable:
        message = "You graphics hardware is not capable of displaying this page ";
        break;
      case RenderModeReason.GPUAccelerationDisabled:
        message = "Hardware graphics acceleration has not been enabled 
            on this web page.\n\n" +
          "Please notify the web site owner.";
        break;
      case RenderModeReason.TemporarilyUnavailable:
        message = "Your graphics hardware is temporarily unavailable.\n\n"+
          "Try reloading the web page or restarting your browser.";
        break;
      case RenderModeReason.SecurityBlocked:
        message =
          "You need to configure your system to allow this web site 
        to display 3D graphics:\n\n" +
          "  1. Right-Click the page\n" +
          "  2. Select 'Silverlight'\n" +
          "     (The 'Microsoft Silverlight Configuration' dialog will be displayed)\n" +
          "  3. Select the 'Permissions' tab\n" +
          "  4. Find this site in the list and change its 3D Graphics 
        permission from 'Deny' to 'Allow'\n" +
          "  5. Click 'OK'\n" +
          "  6. Reload the page";
        break;
      default:
        message = "Unknown error";
        break;
    }
    MessageBox.Show(message,"3D Content Blocked", MessageBoxButton.OK);
  }
}

不幸的是,微软没有为提供一个简单的用户体验而提供良好的支持。

© . All rights reserved.