玄铁剑

成功的途径:抄,创造,研究,发明...
posts - 128, comments - 42, trackbacks - 0, articles - 174

Silverlight 2.0 构建高级 3D 动画

Posted on 2008-04-21 21:27 玄铁剑 阅读(1277) 评论(2)  编辑 收藏 引用 所属分类: Silverlight
Silverlight
使用 Silverlight 2.0 构建高级 3D 动画
Declan Brennan

本文讨论:
  • XAML 基础
  • 在 XAML 中构建元素
  • 如何折叠多面体
  • 模拟 DirectX 算法
本文使用了以下技术:
Silverlight
如果由于横生波折,您与过去几个月内发布所有 SilverlightTM 方面的信息均失之交臂,我来为您补上这一课:Silverlight 是 Microsoft 出品的一款新的跨浏览器插件,它引进了 Microsoft® .NET Framework 的强大功能,用于履行之前保留给 Flash 或 Java 小程序的职责。Silverlight 有大量非常有用的现成功能。它支持小而精的 .NET Framework 3.5,此版本除了其他功能外,还包括 XML 和可扩展应用程序标记语言 (XAML)、泛型集合、Web 服务和 LINQ。Silverlight 还支持多种与 .NET 兼容的语言,但在这里我将仅使用 C#。
我认为掌握一项新技术的最好方法是从中寻找一些乐趣。因此,Silverlight 1.1 alpha 发布后,当我在都柏林的 IMT 会议上看到 Tim Sneath 令人激动的演示时,我就决定打造一个颇具培训色彩的应用程序,展示如何通过折叠一个平面来形成各种 3D 形状(即多面体)。默认情况下,Silverlight 并不支持 3D,因此需构建 DirectX® 算法库的模拟来操作 3D。
多面体是具有平面的三维对象。此 Silverlight 示例将研究称为柏拉图多面体的正多面体和称为阿基米德多面体的半正多面体。这些多面体的表面均是正多边形(即所有边的长度都相同),如等边三角形或正方形。它们也可以是球体外表面,即没有尖角。正如您可能从这些古希腊名字中猜出来的,这些对象长久以来都具有迷人的人文内涵。若感兴趣,可在 George Hart 的网站上找到有关它们的更多信息,地址为:georgehart.com/virtual-polyhedra/vp.html
可在图 1 中或在 www.picturespice.com/ps/Polyhedra/ClientBin/TestPage.html 上查看最终应用程序的演示。应用程序的基本功能是:允许通过将鼠标移动到某个形状(多面体)来选中它。然后,窗口右上角会显示有关所做选择的一些信息,并且您还会看到将平板折叠成所选多面体的动画。最后,如果单击“Cycle”(循环)按钮,程序会自动依次循环显示每个形状。
Figure 1 Silverlight Demonstration of Polyhedra (单击该图像获得较大视图)

使用 XAML
与许多 Silverlight 应用程序一样,多边形大量使用内容定义语言 XAML,它等价于 HTML,但更加灵活。同样地,尽管可仅使用 HTML 文档对象模型 (DOM) 来创建 HTML 页面,但它并非一个用于生成内容的明智方法,因为编码往往非常耗时,并且所生成页面的初始化速度也非常慢。最好尽可能在页面中使用 HTML 标记,然后在需要灵活性的地方使用 JavaScript 和 DOM 加以扩展。
使用 XAML 时也适合采用上述原则。组合内容的最快捷方法是尽可能多地使用 XAML 标记,并在必要时使用与 .NET 兼容的语言(如 C#)和 Silverlight Media API 来加以扩展。XAML 可以是手工编码、通过设计软件包(如 Expression BlendTM)生成、在开发期间由运行的程序产生,甚至在服务器上动态产生。理解这一点需要观念的改变。当 C# 程序员最后在非常适合使用 XAML 的实际环境中对功能进行编码时,则非常容易。
在此仅是粗略地体会一下 XAML,其他内容则超出了本文的讨论范围。不过,Charles Petzold 在《Applications= Code+Markup》一书中非常详细地介绍了 XAML。
以下是我们在学习新语言时都会遇到的“Hello World”示例的 XAML 等价代码:
<UserControl x:Class="Polyhedra.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock>Hello World</TextBlock>
</Grid>
</UserControl>
根元素是 UserControl。它包含 Grid,而 Grid 又包含具有“Hello World”文本的 TextBlock 元素。
先简单了解一下 UserControl 属性。简单说来,它为 XAML 定义代码隐藏类的等同项。在解析和加载 XAML 的同时实例化该类。可在构造函数中执行多种实例化,但如果需要更加复杂的操作,通常必须要运行事件处理程序。它是 Silverlight 的一个重要特征。可为多种 XAML 对象附加事件处理程序,并在所选择的与 .NET 兼容的语言中执行,该语言与 JavaScript 不同,对其编译后会开放所有可能性。
在 HTML 中,元素通常在各个 DIV 中分组,以便安排它们在页面上的位置。与此类似,在 XAML 中,形状在 Canvas(或 Canvas 类型的其他元素,如 Grid)中分组。就像 DIV 在 HTML 通常为嵌套格式一样,Canvas 在 XAML 中也可为嵌套格式。HTML 中的大多数元素都是矩形。然而,XAML 支持所有形状,包括 TextBlock、Rectangle、Polygon、Ellipse 以及非常灵活的 Path(它可实现用户定义的形状)。HTML 中的元素是通过 ID 属性来标识的,而 XAML 中用于标识元素的等价属性是 x:Name,其中 x 是 XAML 命名空间的别名。
除生成静态页面外,XAML 还具备许多其他功能。其中最强大的功能之一是使用 Storyboard 来动画模拟对 XAML 所指定初始 UI 的更改。(在 Internet Explorer® 5.0 中,向 HTML 添加了类似的功能,称为 HTML+Time。)其中一个示例为动画模拟对某个对象的颜色、可见性或透明度的更改。如果与转换结合使用,Storyboard 还可旋转、缩放或移动对象。
Storyboard 使用少量甚至无需任何传统代码即可生成所有类型的动画效果。如果与触发器结合使用,从理论上讲,当对象上发生某个事件(如 MouseEnter)时可自动启动各种动画。唉,可是到 2008 年 3 月发布的 Silverlight 2.0 Beta 为止,触发器可处理的事件只有 Loaded。对于其他事件,则需要少量事件处理程序形式的探测代码。我希望这一状况能够很快得到改善。
Silverlight 中的 Storyboard 具有一个不错的特征,就是它们基于时间而非基于帧。通过使用与不同时间和事件关联的多个独立 storyboard,可简化复杂行为的实现。毕竟,真实世界并非基于帧—帧是电影胶片流传下来的产物。如果使用独立的行为来实现独立的对象,则问题就简单的多啦。

一些 XAML 示例
多面体应用程序中间的折叠动画是使用 C# 代码来生成的。而剩余部分基本是在 XAML 中定义的。其中包括循环示例多面体、以多种方式突出显示当前选中多面体的方法以及“Cycle”(循环)按钮(通过一个旋转箭头动画来指示已激活)。
让我们来看看从 Page.xaml 摘录的两段代码以了解这些动画的实现方式,首先来看“Cycle”(循环)按钮(如图 2 所示)。其中有个名为 Cycle 的 Path。数据属性指定一系列操作,其中 M 代表 move(移动),A 代表 arc(弧形运动)且 L 代表 line(直线运动)。由于它只是一个相当简单的符号,因此我仅在一张纸上画出我希望的形状并手动设计所需操作。在大多数情况下,最好使用工具(如 Expression Design)来完成操作。
该 Path 还有一个名为 CycleRotate 的 RotateTransform。该转换最初不会执行任何操作,因为它的角度已设置为 0。但是,当名为 CycleLatched 的 Storyboard 被激活后,它会持续以很小的增量更改角度,从而使箭头开始旋转。
图 3 显示了如何定义其中的一个多面体示例。在该 XAML 的底部,可看到此示例包含用于定义四面体的四个 Polygon。它们位于自身的 Canvas(即 Model0)之内,并且周围有一个名为 Ring0(最初并不可见)的圆环或椭圆。共定义了两个 Storyboard 动画,一个用于 MouseEnter,另一个用于 MouseLeave。MouseEnter 动画会立即显示圆环,增大 Model0 Canvas 的大小,并使其在 0.7 秒时间内变得更加不透明。MouseLeave 动画则完全反转这些更改。
由于这些 Storyboard 可独立运行,因此每件事都可按您的预期发生,无论用户移动鼠标的速度是快还是慢。通常情况下,一个示例的 MouseLeave 动画会与新选择示例的 MouseEnter 动画同时发生。但是,不必担心,因为 Silverlight 会自动处理多个活动 Storyboard 的并行运行。
下面的话有点跑题,您可能想知道是如何计算四面体示例的每个 Polygon 的边角的,并且,应用程序中其他示例的 Polygon 数量明显要多得多。我是懒惰主义的坚决拥护者,只要计算机能更快完成的,我决不会自己来做。因此,我只在生产过程中使用了一个修改后的多面体。该版本依次执行每个示例中心动画的一个帧—该帧具有完全闭合的形状。我将每个结果 Polygon 组放到沿圆周等距分布的一组 Canvas 中,并将它们一起放到一个文件中以备主程序使用。
要将对象放到圆周上,需以相同的增量 2*PI/NumSamples 增加角度,然后使用坐标(x= Radius*Cos(Angle), y=Radius*Sin(Angle))来指定每个示例的中心点。并且,由于定位对象的 Canvas 使用的是 Left 和 Top 属性,因此需将中心点的位置分别偏移半个宽度和半个高度。

使用 XAML 的技巧
正如您所看到的,可使用 XAML 来获得非常丰富的 UI,并且几乎不需要其他代码。即使是大型文件(如 Page.xaml),其初始化速度也快得令人惊讶。在此先与大家分享一些技巧,这是根据我使用 Silverlight 2.0 最新版本(2008 年 3 月 Beta 版)的经验所总结出的。
我先前提到,当前的触发器仅可针对 Loaded 事件(通过 Begin 方法)自动启动 Storyboard。对于其他事件,需要类似如下的少量事件处理程序形式的探测代码:
public void MouseEnterHandler(
object o, EventArgs e) {
this.MouseEnterStoryBoard.Begin();
}
如果对 Storyboard 采用了命名约定(如在对象名称后紧跟事件名称),则可大大缩减需编码的事件处理程序数量。例如,在多面体中将以下方法用作圆周中所有示例的共享事件处理程序:
public void MouseEnterHandler(
object o, EventArgs e) {
this.triggerStoryboard(o,"MouseEnter");
}
private bool triggerStoryboard(
object o, string eventType) {
Canvas el = o as Canvas;
string name= el.GetValue(NameProperty) as String;
Storyboard sb = el.FindName(name + eventType) as Storyboard;
if (sb != null)
sb.Begin();
return (sb != null);
}
通过使用 Silverlight 2.0 Beta,主初始化(即 InitComponents)已从 Loaded 事件处理程序变成了代码隐藏对象的构造函数。此方法更为妥当,但切记构造函数并不是万能的。例如,无法在 Storyboard 上调用 Begin 或 Pause,因此仍需通过事件处理程序来实现。
正如我从 Andy Beaulieu 的“行星般爆炸的 Silverlight 岩石!”示例 (www.andybeaulieu.com/Home/tabid/67/EntryID/73/default.aspx) 中所发现的,要制做基于代码的动画,一个不错的方法是使用设置为一小段时间的一个 Storyboard,并使用一个 Completed 事件处理程序制做一帧动画,然后重新启动 Storyboard:
public Page() { // Constructor for "code-behind"
// Required to initialize variables
InitializeComponent();
this.animationTimer.Completed +=
new EventHandler(animationTimer_Completed);
}
void animationTimer_Completed(object sender, EventArgs e) {
[ Do a frame of animation ]
this.animationTimer.Begin();
}
Silverlight 2.0 Alpha 九月更新版更改了对 Storyboard 的要求,因此动画现在必须有一个目标,即使并不使用它:
<Canvas.Resources>
<Storyboard x:Name="animationTimer">
<DoubleAnimation Duration="00:00:00.01"
Storyboard.TargetName="bogusTimerTarget"
Storyboard.TargetProperty="Width" />
</Storyboard>
</Canvas.Resources>
<Canvas Name="bogusTimerTarget">
</Canvas>
切勿尝试在同一 HTML 页面上使用多个单独的 Silverlight 控件。我实现的第一个多面体针对圆周中的每个示例使用了一个单独的控件,它实际占用了大量内存。在某些情况下,它可能意味着将内容从 HTML 移到 XAML 以减少所使用的控件数量。
XAML 的好处之一是它省去了 UI 中的大量常规工作,从而使您可集中精力完成创造性的问题域代码。在此示例中,问题域为将平板折叠成 3D 形状,即下一节讨论的内容。

如何折叠多面体
很早以前,Niklaus Wirth 撰写了一本名为《Algorithms+Data Structures=Programs》的著作,描述的是面向对象之前的内容,我猜想之前提到的 Charles Petzold 的那本书是它的改编本。即使在这么多年之后,它仍是我读过的最具影响力的书籍之一,虽然之后在语言和模式方面发生了许多变化,但它阐述的核心内容仍旧有效。该书的基本理念是开发方法应先确定模拟问题效果最好的数据结构,然后确定可处理或修改这些数据结构的算法。在解决非标准程序时,它是我经常采用的方法。
我试遍了各种方法,最终才确定要在多面体中使用的一种。我想知道开始实际折叠动画时,所需的信息能精简到何种程度。结果证明,由于所有边的长度都相同,因此只要知道多面体中相连的面,几乎就万事大吉。由此可推出图形数据结构。在完成 XAML 输出之前,需使用一组算法来通过两个单独的树数据结构处理图形。稍后会对此加以介绍。
尽管 Windows® Presentation Foundation (WPF) 可支持采用 XAML 的 3D,但 Silverlight 仅默认支持 2D,因为如果机器中的图形处理单元 (GPU) 硬件不会产生负面影响,这样就能轻松实现跨浏览器的兼容性。当然,如果深究的话,计算机屏幕上的 3D 是一个假象。无论使用何种操作,最终显示在显示器上的都是一组普通的 2D 多边形。如果要在代码中执行 3D 操作来计算这些多边形的坐标,可使用非硬件加速的 3D 呈现。假如多边形的数量不多,性能尚可接受。(从主观上讲,Alpha 到 Beta 的帧性能得到了改进。)
选定形状后,会构建名为 Net(与 .NET 没有任何关系)的一个二维展开平板。此过程共分为两个阶段(如图 4 所示)。首先在内存中构建一个图形,一个 GraphNode 对应形状的一个面(请参阅 Graph.cs)。通过从嵌入应用程序集的一个 .shp 资源输入一组连接信息(指出哪些面相互连接)来构建该图形。例如,以下是 cube.shp 的内容:
Figure 4 Building the Shape, from Graph to XAML (单击该图像获得较大视图)
1:3,4,5,2
2:1,5,6,3
3:2,6,4,1
4:3,6,5,1
5:1,4,6,2
6:2,5,4,3
然后,可将图形中的任意 GraphNode 选作开始构建 Net 的首个节点(请参阅代码下载中的 Net.cs 获取详细信息)。然后布置一个二维 FlatFace,其边数与 GraphNode 的相邻点数量相同。接着,选择任意相邻的 GraphNode 来重复此过程,布置下一个 FlatFace 以便其与当前的 FlatFace 共享一个边。每个 GraphNode 仅访问一次,继续此过程直至访问完所有的 GraphNode,生成树结构的 FlatFace。
在 3D 动画的每个帧中,通过 Net 构建一个三维多面体(可能部分闭合)(请参阅本文后面部分的 Polyhedron.cs 了解此操作的详细信息)。这需要将 FlatFace 树复制到 Face 树中,并用 3D 点替换 2D 角点。(图 5 显示了两个坐标系统。由于我希望从水平面开始,因此 Y 应从零开始。所以,通过设置 X=X、Y=0 和 Z=Y 来从 2D 映射到 3D。)
Figure 5 Transforming Points from 2D to 3D (单击该图像获得较大视图)
最后,使用递归来访问 Face 树中的每个接合点以执行折叠。折叠量是形状完全闭合时各个面之间的角度(称为两面角)。从理论上来说,可仅使用 .shp 文件中的连接信息直接计算两面角。
对于特殊情况(如任意三个面共角或相同类型的任意数量面共角),它极其简单。但它处理常见情况要困难一些,因此我决定将这些角存到一个单独的 .dihedrals 资源中。例如,以下是 cube.dihedrals 的一段摘录:
1,3:1.5707965056551
1,4:1.57079661280793
1,5:1.57079614793469
1,2:1.57079604078186
2,1:1.57079604078186
2,5:1.57079628466395
2,6:1.57079661280793
2,3:1.57079636892585
3,2:1.57079636892585
3,6:1.57079614793469
3,4:1.57079628466395
...
这些数字中的分钟差异(在本例中均为 PI/2)仅是用于计算它们的人为方法。
最后是一系列转换,将多面体视图投影到显示器上,形成一组多边形。您可能已注意到,在折叠动画中,多面体还沿着垂直轴旋转。为达到这一效果,我将枢轴点移动到原点,旋转,然后再移动到视点。接着执行透视转换以确保较远的对象(在 Z 轴上)显得更小。(我将在下一节中更详细地介绍如何执行转换。)
最后,会生成一组 XAML 多边形,与 3D 形状在显示器表面的投影相对应。通过使用 3D 多边形中心的 Z 坐标设置每个 Polygon 的 XAML ZIndex 属性(将浮点数压缩成整数),可强制 Silverlight 以从后到前的顺序绘制这些多边形。这是一个过于简单化的 3D 实现方法,仅适用于规整的多边形(如没有交叉),就象我举的特例一样。更智能形式的 3D 包括其他机制(如深度缓冲区)。由于需要访问 GPU,因此超出了 Silverlight .NET 的范围。
最后,XAML 允许 Polygon 部分透明(使用 Opacity 属性),从而让多面体产生部分透明的艺术效果。这就是对它所做的全部工作。

模拟 DirectX 算法
我将简单介绍一下用于实现动画的算法。如需更多详细信息,Web 上有许多不错的网站可供参考,如 wikipedia.com、euclideanspace.com、mathworld.wolfram.com、gamasutra.com 和 gamedev.net。
我已在之前的 XAML 示例中介绍了一些 2D 转换(如 RotationTransform 和 ScaleTransform)。Silverlight 通过这些转换帮您完成了所有工作。如果在 WPF 环境中使用 XAML,则还需用到 3D 转换,但它们在 Silverlight 中并不可用。WPF 依靠 DirectX 来完成基础工作。这是因为 DirectX 有一组类,非常适合执行 3D 转换所需的算法。为在 Silverlight 中做一些相同的转换,我针对许多这样的类生成了 DirectX 算法模拟,以便在 Silverlight 中使用它们。
如果有过在 Direct3D® 中编码的经验,Microsoft 对 Vector2、Vector3、Matrix 以及类似项的实现会令您倍感熟悉(请参阅图 6)。如果您没有这方面的经验,我会为您大致做个讲解,并提供几个示例。
Figure 6 Emulated DirectX Math Classes (单击该图像获得较大视图)
在 2D 中定位一个点需要两个坐标:X 和 Y。在 3D 中则需要三个坐标:X、Y 和 Z。尽管严格说来点并非矢量,但 2D 点可存储在 Vector2 对象中,而 3D 点可存储在 Vector3 对象中。(矢量是具有量值和方向的实际实体,可通过始于原点且在某一点结束的箭头来表示。从某种意义上说,可将所有矢量都看作始于原点。)
可通过一组面来确定三维形状。而面则是通过代表角的一组点来确定的。所以通常都是应用一个或多个操作(如旋转、平移/移动或缩放)来将这些点转换成另一组点。几乎所有操作都可用 4x4 数字网格(称为矩阵)来表示。(通常,实际矩阵可具有任意数量的行和列,但我们仅对用于操控 3D 齐次坐标点的特殊矩阵感兴趣。)这些操作称为线性转换,因为它们不会(过度)扭曲所处理对象的形状。
有一种方法非常有用,它可把两个这样的矩阵合并成具有相同效果的一个矩阵。此操作称为矩阵乘法并且可重复多次。即意味着可将整组转换放到具有相同效果的一个矩阵中。它比单独应用每个转换效率要高得多。无需担心如何执行矩阵乘法或它是如何实现其转换魔法的。只把它当成工具使用即可。
要将源点转换成目标点,还需要一个在矩阵和矢量对象之间定义的乘法操作。为避免混淆(与矩阵-矩阵乘法),在 Vector3 中使用 TransformCoordinate 方法来执行它。
可使用一些小代码段来更加清楚地表达这一效果。首先,使用 polyhedron.cs 中的以下代码来沿其公用边折叠两个面:
Vector3 axis = axisTo - axisFrom;
Matrix foldTransform =
Matrix.Translation(-axisFrom) *
Matrix.RotationAxis(axis, proportion * _dihedralAngle) *
Matrix.Translation(axisFrom);
Vector3[] p= Vector3.TransformCoordinate(
face.Points,foldTransform);
在此处,公共边是从 axisFrom 到 axisTo 的直线。旋转通常是绕着穿过原点的一条线来定义的,因此,在旋转之前,需将此边的一个点移到原点,之后再把它移回去。为此,我将以下三个线性转换放到一起以构建用于转换面坐标的矩阵:移到原点,旋转,然后重新移回。
这里还有一个小窍门:代表旋转轴的矢量也必须始于原点,因此我减去 axisFrom,以将 (axisFrom,axisTo) 移到 (原点, axisTo-AxisFrom)。
如果算法还没有装满您的大脑,让我们再来看一个示例(基于 projector.cs),可使用它来将多面体的多边形转换成您想从屏幕视点看到的任意形状:
Matrix projection =
Matrix.Translation(-pivot) *
Matrix.RotationY(yaw) *
Matrix.RotationX(angle) *
Matrix.Translation(viewPoint) *
Geometry.Perspective(5);
Vector3[] p= Vector3.TransformCoordinate(face.Points,foldTransform);
请记住,在折叠时形状是在水平面上旋转的。为此,将枢轴上的一个点移到原点,然后绕垂直 (Y) 轴以偏航角旋转。(如果在实际或模拟环境中体验过航空器,则可能会遇到其他两种独立旋转:俯仰和横滚。)
此次不是移回起始点,而是视点发生移动。最后,执行透视转换以收缩在 Z 轴上较远的对象。这要完成许多操作,但都可以合并到单个 4x4 矩阵中(这一点令人印象非常深刻)。

了解更多内容
尽管在多面体中并不会使用,但模拟库中仍提供了四元数。它们是将一组 3D 旋转合并成单个旋转的不错方法。它们还非常适合于从一个方向顺利转到另一方向。它们是 William Rowan Hamilton(来自我的家乡爱尔兰都柏林)在经过一条街时发现的,他立即将方程式刻在布鲁穆桥上以防忘记(请转到 www.maths.tcd.ie/pub/HistMath/People/Hamilton/Quaternions.html 查看详细信息)。我们文章中的内容包罗万象吧,您一定认为我说得有些夸张了。
四元数还可避免出现万向节锁。它在阿波罗登月之旅的陀螺仪导航中起着非常重要的作用,并且在有关阿波罗 13 号任务的影片中重点介绍过它。
还有许多其他内容我并未涉及到(如矢量点积和叉积),但希望您能从我所讲的内容中了解可使用 DirectX 算法模拟实现的功能。我最初开发此模拟是用于在无法安装 DirectX 且运行 ASP.NET 的托管服务器上执行转换。此类环境中还有可提供此功能的其他应用程序。
多面体应让您初步了解到 Silverlight 是如何提供一个非常有用且可扩展的工具,从而使 Web UI 可真正吸引用户的注意力并且通过快捷且难忘的方式来获取信息。进行构建时,您可以用各种有创意的方法来利用现有的 .NET 经验,不必担心在客户端上所执行的代码行数,这一点与 JavaScript 不同。

Declan Brennan 资历相当老,他见证了首个微处理器的诞生并且头脑中有着一些根深蒂固的印象。他非常庆幸自己能生活在一个“只有想不到的,没有做不到的”的年代。可通过 declan.brennan.name 了解 Declan 的更多信息
只有注册用户登录后才能发表评论。