logo资料库

WPF 3D XAML入门.pdf

第1页 / 共12页
第2页 / 共12页
第3页 / 共12页
第4页 / 共12页
第5页 / 共12页
第6页 / 共12页
第7页 / 共12页
第8页 / 共12页
资料共12页,剩余部分请下载后查看
WPF 在组成 Microsoft Windows Presentation Foundation 的类中, System.Windows.Media.Media3D 命名空间中的那些类很突出。这些类的用途是使主流 Windows® 应用程序能够显示三维图形。与 Windows Presentation Foundation 2D 图形一样, 通常可以用可扩展应用程序标记语言 (XAML) 非常方便地访问 3D 图形,但二者的相似性非常 少。3D 图形编程涉及非常不同的概念和约定。其中,3D 和 2D 相同的部分是画笔的区域:您 始终要用 2D 画笔来覆盖 3D 可视区的表面。 图 1 显示了 Hello3D,这是一个 3D 版的传统“Hello, World”程序。如果您运行的是 Windows Vista™ 或者是安装了 Microsoft® .NET Framework 3.0 运行库的 Windows XP,则 只需使用 Internet Explorer® 即可启动产生该图形的 XAML 代码,从而可以看到图像(参见 图 2)。 图 1 Hello3D 图像 3D 视区 在 3D 图形编程中,没有线条、Bezier 样条曲线、矩形或椭圆。每个 3D 物体都是三维坐 标空间中的三角形的集合。三角形是 3D 编程的基本单位,这是因为每个单独的三角形总是能 定义一个平面,而三角形集合可以模仿立体物体,甚至可以模拟曲面。随着您深入了解 3D 编 程,您将会用三角形看待生活中的所有事物。 正如 Hello3D.xaml 所示,3D 视图由 Viewport3D 元素组成。3D 场景需要一个或多个 GeometryModel3D 类型的物体、一个或多个光源、以及一个用于控制 3D 物体如何投射到 2D 表面从而控制观看者如何看到图像的摄像机。 GeometryModel3D 元素有三个重要属性:Geometry、Material 和 BackMaterial。Geometry 属性被设置为 MeshGeometry3D 元素,用于根据坐标点和三角形描述可视物体。Material 和 BackMaterial 属性说明物体的前面和背面如何着色。在 Hello3D.xaml 中,这两个属性被设置 为 DiffuseMaterial 类型的对象。Material 属性是 VisualBrush,由包含文字“Hello, World”的 TextBlock 组成。BackMaterial 属性只是红色画笔。(如果要看到物体的背面,请将摄像机 Position 属性更改为“0 0 -5”,并将 LookDirection 更改为“0 0 1”。)
解析网格几何体 在本专栏中,我将着重介绍此 Viewport3D 聚合的一个特别重要的部分 — MeshGeometry3D 类,该类用于定义 3D 物体的实际几何表示形式。该类有四个重要属性: Positions、TriangleIndices、TextureCoordinates 和 Normals。Positions 属性是 Point3D 对 象的集合,这些对象通过 X、Y 和 Z 坐标定义位置。在此坐标系统中,X 坐标向右增加,Y 坐 标沿屏幕向上增加,Z 坐标向屏幕外增加。(这称为右手坐标系统;如果用右手食指指向 X 值 增加方向,并用中指指向 Y 值增加方向,则拇指指向 Z 值增加方向。Windows Presentation Foundation 3D 系统还实现了右手旋转定则:如果右手拇指指向任何轴的增加值方向,则其他 手指的曲线显示围绕该轴的正向旋转轴的方向。) Hello3D.xaml 示例中的 Positions 属性有六个以逗号分隔的 Point3D 对象: Positions="-2 1 -1, 0 2 0, 2 1 -1, -2 -1 -1, 0 -2 0, 2 -1 -1" 前面三个从左到右是物体顶部的坐标,最后三个是对应的底部坐标。注意,中心的 Z 坐标 是 0,但在左右边缘是 -1,因此中心是左右边缘的前景。 Positions 属性表示物体的所有顶点。这些顶点在定义物体时肯定是有作用的,但它们不能 描述所有信息。任何一组三个顶点都可以组合成一个三角形,这就是 TriangleIndices 集合所说 明的内容。TriangleIndices 是三个一组排列的整数集合。每一组的三个整数都定义了一个三角 形。整数值是 Positions 集合中的序数。例如,第一个三联数是 0 3 1,它是指 3D 点 (-2, 1, -1)、 (-2, -1,-1) 和 (0, 2, 0)。这是位于物体左上角的三角形。 TriangleIndices="0 3 1, 1 3 4, 1 5 2, 1 4 5" TriangleIndices 集合实际上驱动物体的呈现。TriangleIndices 未引用的任何 Positions 元 素都会忽略(如果没有 TriangleIndices,则 Positions 集合中的每个 Point3D 三联数都将解释 为一个三角形)。 每个三角形都应有正面和背面。查看三角形的正面时,三联数以反时针方向表示顶点。如果 将第一个三联数更改为 0 1 3,将看到左上三角形以红色着色,这是因为查看的是三角形的背面, 而不是它的正面。 如果 3D 物体以实线画笔着色,则 Positions 和 TriangleIndices 属性就已足够。对于其他 类型的画笔(渐变画笔或并列画笔),还需要 TextureCoordinates。画笔是像热塑包装一样覆 盖 3D 物体的 2D 表面。TextureCoordinates 集合表示 3D 物体的顶点和 2D 画笔的坐标之 间的对应关系。此集合包含与 Positions 中的每个 3D 点对应的一个 2D 点。这些 2D 点是以 Y 轴向下为增加方向的相对坐标(0 和 1 之间)。点 (0, 0) 表示画笔的左上角,(1, 1) 是右下 角。在 Hello3D.xaml 中,六个 2D 点表示 VisualBrush 的坐标,这些坐标被定义为 GeometryModel3D 元素的 Material 属性: TextureCoordinates="0 0, 0.5 0, 1 0, 0 1, 0.5 1, 1 1" 实际上,3D 物体的每个三角形均由画笔的三角形覆盖,这些三角形可能需要拉伸或收缩以 适合具体大小。 Normals 属性是按与 Positions 集合的一对一对应关系得到的向量的集合。每个顶点均被 视为面向特定方向,该方向以该顶点的 Normals 向量表示。每个三角形内的每个点基于在其三 个顶点上的向量的内插值,以不同方式反射光线。如果不提供 Normals 集合,则会基于在网格 规范中共享的每个顶点上会合的三角形的 Normals 的平均值计算一个该集合。 算法网格几何体
System.Windows.Media Media3D 命名空间中的类没有提供超越 MeshGeometry3D 的任 何更高级别的接口。(看完本文后,您可能会理解为什么。)对于诸如金字塔体和立方体这样具 有平滑侧面的简单物体来说,可以手工编写 MeshGeometry3D 对象。对于更复杂的图元(特 别是那些有曲面的图元)来说,您可能会选择使用符合 .NET 的语言通过算法生成顶点和系数。 的确,如果您要求更进一步,则可能想到建立一个由从 MeshGeometry3D 派生的网格几何体 组成的完整库。 但您会立即发现这样行不通。MeshGeometry3D 是密封的;它无法被继承。此外, MeshGeometry3D 本身派生于抽象类 Geometry3D,并且无法从 Geometry3D 派生,因为它 有独立的内部构造函数。 如果不考虑从 MeshGeometry3D 派生类,则替代的途径是直接定义一个能够生成 MeshGeometry3D 对象的类,并将该对象作为属性公开。然后,可以在 XAML 文件中将此类 定义为资源,并通过绑定引用 MeshGeometry3D。 图 3 显示了一个用 C# 编写的名为 SimpleCylinderGenerator 的类。(在这里,我将仅限 于圆柱体。)在此专栏的可下载代码中,SimpleCylinderGenerator 支持名为 Petzold.MeshGeometries 的 DLL。 SimpleCylinderGenerator 类有两个属性:使用 Slices 属性可以定义用多少三角形模拟柱 状体的曲面。(我从 Direct3D 类库中的静态 Mesh.Cylinder 方法的文档中选用术语“slices”。) 沿柱状体长度方向分布的三角形的个数实际上是 Slices 属性的两倍。柱状体顶部和底部各自还 需要很多等于 Slices 的三角形。 MeshGeometry 属性基于 Slices 属性创建 MeshGeometry3D 对象。对算法进行硬编码, 以创建以一个单位的半径沿正向 Y 轴扩展一个单位的柱状体。注意,可以在随后调整此物体的 大小,并使用转换将它移动到任何所需位置。 SimpleCylinderDemo 程序显示了如何使用此 SimpleCylinderGenerator 类。此程序的主体 是 SimpleCylinderDemo.xaml 文件,如图 4 所示。根元素包含定义 SimpleCylinderGenerator 类并通过前缀 pmg(表示“Petzold 网格几何体”)与它关联的命名空间和 DLL 的 XML 命名空 间声明。 Resources 部分包括键名称为“cylinder”并且 Slices 值为 36 的 SimpleCylinderGenerator 类型的对象。GeometryModel3D 元素将它的 Geometry 属性赋给此 资源的绑定及其 MeshGeometry 属性。转换将更改柱状体的大小。在 DiffuseMaterial 上添加 了一些有向光线和 SpecularMaterial 对象后,结果如图 5 所示。通过使用应用于该物体的任意 转换,可以使此物体运动。
图 5 柱状体 显然,SimpleCylinderGenerator 技术是有效的,但我感到它有很多缺点和不足。找出这些 问题并解决它们将使您不仅能从几个方面特别地洞察网格几何体,而且还能洞察 Windows Presentation Foundation 编程的某些常规方面。 适应使用者 当我刚开始为诸如柱状体等物体定义网格几何体时,我首先努力实现对称性, SimpleCylinderGenerator 反映了我的偏好:所有三角形都是等腰三角形。但对于沿柱状体纵向 分布的三角形来说,实际上这一点就成了问题。 通常,当 Slices 属性很小时,有利的一点在于网格几何算法会产生某些有用的结果。请尝 试将 SimpleCylinderGenerator 的 Slices 属性设置为 4,并在图 6 的左侧显示物体。(为了 说明目的,边缘已增强。)这很有趣,但不如右侧的物体有用。右侧的物体要求纵向三角形不是 等腰三角形,而是直角三角形,以便每一对直角三角形形成一个矩形。
图 6 对柱状体进行切片 SimpleCylinderGenerator 不生成 TextureCoordinates 属性,如果将只用 SolidColorBrush 覆盖柱状体,则需要该属性。另一方面,如果想使用任何类型的 GradientBrush 或任何类型的 TileBrush,则需要一个与 Positions 属性一起智能地生成的 TextureCoordinates 属性。 用 2D 画笔覆盖柱状体时,您可能希望画笔环绕柱状体,以便画笔的左右边缘在我将其称 之为“接缝”的线条上会合。此接缝应当沿柱状体的纵向延伸。之所以使用直角三角形而不是等腰 三角形来定义网格,还有另一个原因。 SimpleCylinderGenerator 还反映了我的另一个偏好,经济性:Positions 集合中的所有 Point3D 对象都是唯一的。在生成 TriangleIndices 集合的 for 循环中,使用了模运算符,以便 某些随后的三角形使用 Positions 集合中前期存在的点。对我来说,似乎为了“闭合”物体必须要 这样做,但这完全是错误的。同样,如果想用画笔覆盖柱状体,需要在接缝处有重复的 Point3D 对象,以便 3D 空间中的相同位置同时映射到画笔的左侧和右侧。 通过使柱状体有一个单位的长度和半径,SimpleCylinderGenerator 采取了在定义 Positions 集合时可以想像的最容易的途径。然后,它依赖于类的使用者(可能是您或另一个程 序员)来实现转换,以正确调整柱状体大小和位置。通过使用转换操作,可以相当容易地将柱状 体的底部转换到另一个位置,但如果要将柱状体的顶部也放到特定位置呢?您将需要为该操作计 算旋转转换。 请让使用者休息一会儿!如果让柱状体生成程序具有属性 Point1 和 Point2 以表示柱状体 两端的中心坐标位置,将会更有意义。可能还要考虑 Radius1 和 Radius2 属性,以便单独设 置两端的半径。具有单独的半径属性会带来额外的好处:如果一个半径是 0,则算法将生成一 个锥体。定义网格几何算法时,“奖励”图元始终是受欢迎的。 迄今为止,对 SimpleGeometryGenerator 的改进已经形成一个很长的列表,但我要使该列 表更长。 与 XAML 集成
若要使用 SimpleCylinderGenerator,必须将该类定义为资源,然后用绑定访问它。如果有 一个用 XAML 实例化并直接集成到 Viewport3D 标记中的类,则会是更好的选择。但如何实现? 它无法从 MeshGeometry3D 或 Geometry3D 派生。 幸运的是,有另一个方式,但它需要熟悉往往使人糊涂的 Media3D 命名空间和某些在类名 称中涉及的术语。首先谈一谈可视效果和模型之间的差异。 可视效果是可以在屏幕上呈现自身的项目。在 Media3D 命名空间中,这是抽象的 Visual3D 类。Visual3D 对象是 Viewport3D 的子对象。 相比之下,模型是对可视效果可以显示的项目的描述。在 3D 编程中,模型包括 3D 物体 自身,还包括照射它们的光线。多个可视效果可以共享相同模型。例如,在 Media3D 命名空间 中,模型由抽象 Model3D 类表示。从 Model3D 派生的类包括 GeometryModel3D、Light(它 是抽象的)和 Model3DGroup。 将可视效果与模型联系在一起的类是 ModelVisual3D。ModelVisual3D 派生自 Visual3D, 因此它一定是可视效果,但它有 Model3D 类型的 Content 属性。模型是可视效果的内容,这 一点很像文本或位图是按钮的内容。 令人吃惊的是,ModelVisual3D 是在整个 Media3D 命名空间中唯一既不密封也不抽象的类, 这意味着它可以用于继承而不会有明显的问题。 假设您编写了一个从 ModelVisual3D 派生的 Cylinder 类。然后,可以指定诸如 Viewport3D 的子项这样的元素: ... 这似乎很方便,但它并不像此标记隐含的那样简单。如果 Cylinder 类派生自 ModelVisual3D,那么它继承了类型 Model3D(GeometryModel3D 从中派生而来)的 Content 属性。Cylinder 必须创建一个类型为 GeometryModel3D 的对象,以便设置为它继承的 Content 属性。GeometryModel3D 定义了 Geometry 属性,因此 Cylinder 类也会创建 MeshGeometry3D 对象以便设置为此 Geometry 属性。 迄今为止,都还不坏,但还有问题。GeometryModel3D 还有 Material 和 BackMaterial 属 性。这是材料如何与网格几何体关联的问题。若要使此方案正确工作,Cylinder 类必须重新定 义 Material 和 BackMaterial 属性,因此标记将类似于如下所示: ... ... ...
现在,大问题来了:我已经提到过,Cylinder 类有名为 Point1、Point2、Radius1 和 Radius2 的属性。您是否想让这些属性潜在地成为数据绑定的目标?我想您希望如此。您是否希望它们可 以启用?我确信您希望如此。在这两种情况下,这些属性都必须被定义为依赖性属性。您可能听 说过,Windows Presentation Foundation 中的所有东西都是可启用的(但只有当它是依赖性属 性时)。 依赖性属性 依赖性属性已作为 Windows Presentation Foundation 的最重要的创新之一出现。在 Windows Presentation Foundation 中,可以按多种方式设置属性。可以在代码或 XAML 中直 接在对象上设置属性。此外,可以通过样式和数据绑定来设置属性。某些属性可以通过父子关系 继承,并且属性可以启用。如果根本不设置属性,则它会拥有默认值。依赖性属性试图使所有这 样的多样性能够以可预测的优先级正确工作。 依赖性属性需要您的类中有一些涉及向系统注册属性的非常大的开销,但完成之后,可以几 乎不必考虑如何设置该属性。重要的是您的类会在属性已经更改时获得通知,因而它可以对这些 更改做出反应。 本文附带的可下载代码中的 Cylinder 类定义了九个依赖性属性。我没有直接从 ModelVisual3D 派生 Cylinder,而是创建了一个可能是其他类(例如 Sphere、Tube 和 BunnyRabbit)的父类的中间类,名为 ModelVisualBase。ModelVisualBase 定义了 Geometry、 Material 和 BackMaterial 依赖性属性,它们向在 GeometryModel3D 中定义的相同属性添加 所有者。 作为如何在代码中实现依赖性属性的示例,让我们看一看 Cylinder 的 Radius1 属性。注 册依赖性属性的过程涉及定义一个 DependencyProperty 类型的公用静态只读字段,名为 Radius1Property,它是属性名加上单词 Property: public static readonly DependencyProperty Radius1Property = DependencyProperty.Register( "Radius1", typeof(double), typeof(Cylinder), new PropertyMetadata(1.0, PositionsChanged), ValidateNonNegative); 注意,PropertyMetadata 构造函数的参数(用于表示默认值)、Cylinder 类中当值发生更 改时要调用的方法以及用于验证值的方法。 还包括引用此静态只读字段的传统属性定义(又称为“CLR 属性”): public double Radius1 { get { return (double)GetValue(Radius1Property); } set { SetValue(Radius1Property, value); } } GetValue 和 SetValue 方法是从 DependencyObject 继承的。任何实现依赖性属性的类 都必须派生自 DependencyObject。 请务必意识到对 Radius1 属性的更改不总是通过 CLR 属性进行的。例如,当启用 Radius1 时,会访问 Radius1Property 字段,而不是 Radius1 属性。因此,除了调用 GetValue 和 SetValue 外,应当避免在 Radius1 属性中进行其他任何操作。
Radius1Property 的定义包括对 Cylinder 类中两个方法的引用。因为这些方法是由静态字 段引用的,因此方法自身也必须是静态的。ValidateNonNegative 方法只是检查 Radius1 是否 未设置为负数值: static bool ValidateNonNegative(object value) { return (double)value >= 0; } 如果某些代码将 Radius1 设置为负数值,将发生异常,但这不是您的类的责任。 其他方法用于通知类:属性已更改。由于可以在不访问 CLR 属性的情况下更改属性,因此 该通知方法是对此属性更改做出反应的唯一机会。下面是我称为 PositionsChanged 的方法, 当 Radius1 属性已更改时将调用该方法: static void PositionsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { Cylinder cyl = (Cylinder)obj; cyl.GeneratePositions(); } 该方法是静态的,但第一个参数是其属性发生改变的实际 Cylinder 对象。 DependencyPropertyChangedEventArgs 对象具有关于发生更改的特定属性、它的旧值和新值 的相关信息,但 Cylinder 将忽略这些信息,而直接调用实例方法:GeneratePositions。此方法 负责基于新的半径生成 MeshGeometry3D 对象的 Positions 和 Normals 属性。 正确处理集合 为了定义 MeshGeometry3D 对象的四个集合,Cylinder 类定义了三个单独的方法。 GeneratePositions 负责 Positions 和 Normals 集合;其他两个方法名为 GenerateTriangleIndices 和 GenerateTextureCoordinates。Cylinder 的构造函数将调用所有 三个方法以初始化对象;此后,将在响应依赖性属性的更改时调用它们。对于大多数属性(Point1、 Point2、Radius1、Radius2),只有 Positions 和 Normals 集合需要重新计算。但对于 Slices 属性,所有四个集合都需要重做。类似于 Slices 属性的是另一个名为 Stacks 的属性,该属性 控制柱状体纵向的细分。在很多情况下,可以将 Stacks 设置为等于 1,这是默认值。但如果 使用 PointLight 或 SpotLight 照射柱状体,则需要生成小很多的三角形来使光线的照射正确。 当诸如 Point1 和 Radius1 这样的属性启用时,可以非常频繁和尽可能快速地调用像 GeneratePositions 这样的方法。下面是一些潜在的问题和解决方案。 特别是,方法应当避免分配内存。您希望的最后一件事是让 CLR 垃圾收集器处理您的动画, 清理您留下的垃圾。如果全都有可能,则任何新的表达式都应当引用结构,而不是类。例如,在 Cylinder 类中,我让构造函数创建了 RotateTransform3D 和 AxisAngleRotation3D 类型的对 象,它们作为字段进行存储并且可在每次调用 GeneratePositions 时重用。 您设置为 MeshGeometry3D 属性的集合属于 Point3DCollection、Vector3DCollection、 Int32Collection 和 PointCollection 类型。所有集合都派生自 Freezable,并且包括在集合的任 何元素更改时所触发的 Changed 事件。这提供了功能强大的工具:例如,这些集合的单个成 员可以启用,以创建 3D 物体的变体。但如果您全部重新计算整个集合,则只是想让通知在您 完成时才发生。因此,应当将集合与 MeshGeometry3D 对象分离,并在您完成时再重新连接 它。下面显示了 GeneratePositions 如何开始和结束:
分享到:
收藏