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

将 3D 对象从 3DS 文件导入 Avalon

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (16投票s)

2005 年 4 月 27 日

16分钟阅读

viewsIcon

163469

downloadIcon

4039

用于将 3D 对象从 3DS 文件导入 Avalon 的类库,以及一个简单的 3D 对象查看器。

目录

引言

当 Microsoft 发布 Avalon 作为社区技术预览版时,我对它的 3D 功能产生了浓厚的兴趣。我相信 3D 是显示技术发展的必然趋势。这种演变经历了从电传打字机到黑白显示器(还有黑绿和“更好”的黑橙)。然后出现了第一批彩色显示器。而现在,它们的时代几乎结束了——LCD 显示器很快就会取而代之。10 年前,要想象一个 1 厘米厚的显示器能显示出如此逼真的画面是很难的。而这种演变当然还没有结束。第一批 3D 显示器已经上市了。

关于 3D 显示技术的更多信息可以在 这里找到。好吧,这项技术仍然有很多局限性,但这仅仅是开始。最近我读到一篇 文章,说夏普发布了一款带有 3D 屏幕的手机,而且效果很棒。

我相信在未来,操作系统中的所有图标、按钮和窗口都将是三维的。我们也会以 3D 的方式看到它们。对我来说,Avalon 就是这样一个世界的开始。

在本文中,我将首先关注从 3DS 文件导入 3D 对象。在第二部分,我将介绍一个示例 Avalon 应用程序,用户可以在其中查看 3DS 文件中的对象并将其导出为 XAML 文本。

系统要求

  • Avalon RCL
  • Visual Studio 2005 Beta 2

从 3DS 文件导入 3D 对象

网格定义问题

让我快速回顾一下计算机 3D 图形的基础知识。计算机图形学中的 3D 对象由许多面组成(Avalon 中是 `TriangleIndices`)。每个面由三个顶点定义(`Positions`)。每个顶点还可以定义一个法线。法线是一个用于对对象进行着色的 3D 向量。

有关 3D 计算机图形的更多信息,我推荐以下链接

在 Avalon 中定义 3D 对象有两种方式。它们可以在代码隐藏(code-behind)中定义,也可以在 XAML 的 `<Model3Dgroup>` 中定义。代码隐藏的定义通常用于可以通过数学定义的物体,例如球体或圆环体。在这种情况下,可以使用数学公式计算所有点、三角形索引和法线。

在 XAML 对象定义中,所有数据都以文本格式编写。以下代码表示一个简单四面体的定义

<GeometryModel3D.Geometry>
  <MeshGeometry3D
    TriangleIndices="0 1 2  1 2 3  2 3 0  0 1 3"
    Normals="-1,-1,0 1,-1,0 1,0,0 0,0,1"
    Positions="-2,-2,-2  2,-2,-2  0,2,-2  0,0,1"/>
</GeometryModel3D.Geometry>

正如你所见,即使对于非常简单的 3D 对象,XAML 定义也相当复杂。请注意,上面的定义是针对最简单的 3D 对象——它只有四个角(`Positions`)和四个三角形(`TriangleIndices`)。对于这样的对象,仍然有可能自己定义数据——用铅笔和纸。但即使是简单的立方体,这也会是一项令人筋疲力尽的工作。此外,常见的 3D 对象可以有数百个具有复杂法线和纹理坐标的角和三角形。因此,我们几乎无法自己计算 3D 对象的数据。

定义解决方案

另一方面,有一些很棒的 3D 建模程序。在这些程序中定义对象并直接导入 Avalon 将非常方便。

我找到了 3DS 文件格式的描述。这种文件格式曾由 Autodesk 的 3D Studio 程序使用。由于该程序的巨大流行度,该文件格式已变得非常广泛使用。现在几乎所有 3D 建模程序都支持某种形式的导入或导出该文件格式。此外,还有巨大的网络库提供该文件格式的对象(您可以在 这里 找到大量免费 3D 对象库的列表)。

想法是能够做类似这样的事情

Viewport1.Models = reader3ds.ReadFile("sample_scene.3ds");

解决方案的读取部分实现在 MeshUtilites 项目中。主要的类是 `Reader3ds`,它实际读取 3DS 文件。还有一个 `ChunkIds` 类,其中包含读取 3DS 文件所用的常量。在编写 `Reader3ds` 后,我发现如果能够将读取的 3D 对象转换为平面或高洛着色对象,那将非常有益。这是在 `MeshFactory` 类中完成的。最后还有一个 `XamlExport` 类,用于将对象(实际上是整个 `Viewport3D`)导出为 XML 文本。

MeshUtilities 项目

Reader3ds 和 ChunkIds

`Reader3ds` 类用于从 3DS 文件读取数据。为此,我们必须首先了解 3DS 文件格式的结构。

3DS 文件中的数据被划分为所谓的“块”。每个块都有一个 ID,一个描述块类型的两字节值。例如,块 ID 中的 0x4110 表示该块包含顶点信息(Avalon 的 `Positions`)。在 ID 之后是块的长度(作为 `long)。这意味着,如果块是未知或不重要的,则可以跳过它。长度之后是实际数据。数据的格式取决于块的类型。例如,顶点数据以顶点数量开头,然后是 3 * 顶点计数 `float` 数字,表示每个顶点的 x、y 和 z 坐标。

3DS 文件包含表示相当复杂的 3D 世界的所有数据。除了顶点和三角形,它还包含有关材质、相机、灯光以及动画的数据。

`Reader3ds` 的当前实现读取 `Positions`、`TriangleIndices`、纹理坐标以及相机和灯光。它不读取材质和动画数据。

要读取 3DS 文件,必须实例化一个 `Reader3ds` 类。然后可以调用其 `ReadFile` 方法,并将文件名作为参数。该方法返回一个 `Model3Dgroup` 对象,可以将其设置到 `Viewport3D` 的 `Models` 集合中。

由于未导入材质,因此可以设置用于所有对象的默认材质——这可以通过设置 `DefaultMaterial` 属性来完成。如果未设置此属性,则使用实心银色笔刷材质作为默认。

许多 3DS 文件不包含定义的灯光。在这种情况下,会将一个默认灯光添加到 `Model3Dgroup`(方向 = 0, 0, -1),这样渲染时场景就不会显得完全黑暗。也可以通过将 `AddDefaultLight` 设置为 `false` 来禁用此功能。

以下代码首先将默认材质设置为金色,然后读取“coins.3ds”文件。然后将对象添加到 `Viewport1.Models`。最后,将前五个对象(硬币)设置为银色。

Reader3ds coinsReader = new Reader3ds();
coinsReader.DefaultMaterial = 
  new DiffuseMaterial(new SolidColorBrush(Colors.Gold));
Viewport1.Models = coinsReader.ReadFile("coins.3ds");
for (int i=0; i<5; i++)
    ((GeometryModel3D)Viewport1.Models.Children[i]).Material =
          new DiffuseMaterial(new SolidColorBrush(Colors.Silver));

MeshFactory

Avalon 使用高洛着色来渲染对象。这意味着对象是平滑的,面与面之间没有明确的边界,因此表面不会显得扁平。没有指定法线的对象使用高洛着色进行渲染。在大多数情况下,这是可以接受的,但有时我们需要平面——例如,对于立方体或金字塔。

在这些情况下,我们需要将顶点法线设置为垂直于表面。构成面的所有三个顶点都应具有指向面外部的法线。在这种情况下,面将只用一种颜色着色——顶点之间不会有颜色渐变。

由于对象由三角形面组成,每个顶点在三个相邻的面中使用。一个顶点只能有一个法线。对于高洛着色来说,这没问题,因为法线指向所有三个面的“平均方向”。但对于平面,一个面的所有法线应指向同一方向。不幸的是,这意味着对于平面对象,所有顶点都必须三倍化。因此,3D 空间中的每个点都必须由三个顶点表示——仅仅因为法线不同。

起初,我希望仅从 3DS 文件读取 `Positions` 和 `TriangleIndices` 就足以在 Avalon 中表示 3D 对象。对于许多对象来说,这就足够了。但平面对象确实需要额外的法线。此外,一些从 3DS 导入的对象未正确渲染——它们看起来好像在某些边上有张力。发生这种情况是因为某些顶点是重复的——一个空间点定义了多个顶点。在这种情况下,渲染引擎无法正确计算用于着色的法线。

Shading examples

上图代表了三种不同情况下的相同对象。第一个是未定义法线的对象。应用了默认的高洛着色,但存在一些异常——顶点数量不正确。第二个图像使用高洛着色正确渲染,因为相同的顶点被合并了。第三张图像显示了相同的对象,但指定了法线和三倍化顶点,使对象看起来是平面的。

`MeshFactory` 的目的是将原始网格转换为高洛或平面着色网格。 `MeshFactory` 中的所有方法都是静态的,因此使用起来很简单

MeshGeometry3D one3dObject;
Model3DGroup whole3dScene;

// convert only one MeshGeometry3D
one3dObject = MeshUtilities.MeshFactory.GetGouraudShadedMesh(original3dObject);
one3dObject = MeshUtilities.MeshFactory.GetFlatShadedMesh(original3dObject);

// convert all objects in Model3DGroup
whole3dScene = MeshUtilities.MeshFactory.GetGouraudModelGroup(originalScene);
whole3dScene = MeshUtilities.MeshFactory.GetFlatModelGroup(originalScene);

XamlExport

如其名称所示,`XamlExport` 类将整个 `Model3DGroup`(包括所有 3D 对象、灯光和相机)转换为表示相同场景的 XAML 文本。导出的 XAML 可以在 XamlPad 中预览,或直接用于 XAML 文件。

使用 `XamlExport`

MeshUtilities.XamlExporter thisXamlExporter = new XamlExporter();

thisXamlExporter.DefaultMaterialXaml = 
   "<GeometryModel3D.Material><DiffuseMaterial> ...";
thisXamlExporter.DoubleFormatString = "#0.###";
thisXamlExporter.LoadXamlTemplate("XamlExportDefaultTeplate.xaml");

string xamlText = thisXamlExporter.ExportModel3DGroup(Viewport1.Models, 
                                                     Viewport1.Camera);

首先,我们必须创建一个 `XamlExport` 类的实例。然后我们可以(但不是必需)定义默认材质 XAML 字符串和 `DoubleFormatString`(用于格式化所有 `double` 值)。我们还必须定义 `XamlTemplate`。这可以通过从文件加载(如上所示)来完成,或者我们可以将 `XamlTemplate` 属性设置为模板文本。现在我们可以调用 `ExportModel3DGroup` 方法,并将 `Model3DGroup` 和 `Camera`(也可以是 `null`)作为参数传递。该方法返回一个表示整个 3D 场景的 XAML 字符串。

`XamlExport` 使用模板来定义正确的 XAML 结构——即 3D 网格、灯光和相机插入的位置。如果您打开 XamlExportDefaultTeplate.xaml(也已包含在内),您会注意到添加了三个注释:"<!-- IMPORT_MESH_RESOURCES -->""<!-- IMPORT_CAMERA -->""<!-- IMPORT_CHILDREN -->"。在 `ExportModel3DGroup` 方法中,这些注释被替换为相应的数据。如果您定义了自己的模板,只需将这三个注释放入其中即可。

我建议使用导出的 XAML 处理简单场景,对于更复杂的场景,最好使用 `Reader3ds` 在代码隐藏中读取 3DS 文件。

Viewer3D

Viewer3D

在我描述 Viewer3D 之前,我想祝贺 Microsoft 带来了 Avalon 的各种可能性。我在 Web 和 Windows 界面的编程方面拥有丰富的经验,我认为 Avalon 是一个巨大的进步——在 UI 定义的简便性、功能强大和无限的可能性方面。它非常容易定义出在其他技术中几乎不可能定义的非常复杂且外观精美的用户界面。

上面带有 3D 背景、圆角和半透明面板的图片展示了 Avalon 带来的部分“炫酷”功能。Viewer3D 还使用了一些更高级的功能——触发器、动画……我稍后将介绍其中一些。首先,请允许我描述 Viewer3D 的功能。

Viewer3D“用户指南”

正如您从上图所见,窗口中的大部分空间被来自 3DS 文件的 3D 对象(房子和人)占据。左侧有各种控制输出或执行其他操作的面板。底部有四个按钮,用于显示或隐藏面板。按钮的右侧有一个文本框,可以在其中写入文件名。右下角有一个按钮,用于加载指定的文件。

第一个面板代表当前用于 3D 对象的着色模型。如果选择了“原始”单选按钮,则对象与从 3DS 文件读取时相同。如果选择了“平面”或“高洛”按钮,则使用适当的着色技术转换对象——有关更多详细信息,请参阅 MeshFactory。还有一个“显示线框”复选框——选中时,场景会添加一个线框(大量的 `ScreenSpaceLines3D`)。在速度较慢的机器上,此操作可能需要一些时间——所以如果操作不立即发生,请不要将鼠标扔到墙上。此功能对于检查模型的复杂性非常有益。

下一个面板非常简单。它有一个用于输入小数位数的文本框和一个用于将整个场景作为 XAML 复制到剪贴板的按钮。使用更少的小数位数(2 或 3)可以生成更小的 XAML,而使用更多的小数位数可以获得更精确的输出(也适用于小对象)。正如我之前所描述的,XAML 模板用于创建 XAML 字符串。Viewer3d 使用包含的 XamlExportDefaultTeplate.xaml 模板。可以更改此模板以生成不同的输出。

然后是灯光面板。在此面板中显示了从 3DS 文件读取的所有灯光。还会添加一个自由灯——如果 3DS 文件中没有定义灯光,则自由灯就是默认灯。您可以打开或关闭每个灯。如果打开了自由灯,则可以通过复选框下方的两个滑块设置其方向。灯光的颜色也是从 3DS 文件导入的。上面的场景实际上是使用默认的银色材质渲染的,但导入的灯光赋予了它偏黄的色调。自由灯以白色发光。

最后一个面板是相机。其中包含所有导入的相机以及一个始终可用的自由相机。由于一次只能激活一个相机,因此相机选择是通过单选按钮完成的。如果选择了自由相机,用户可以设置其方向、旋转以及与中心点(坐标 0, 0, 0)的距离。还有一个文本框,可以设置最大距离——这是当最后一个滑块在其最右位置时的距离。如果用户将鼠标移到相机面板上,右侧会显示一个小相机预览面板。此面板有助于查看相机“看”物体的方向。

应该可以加载大多数 3DS 文件。不幸的是,无法加载材质信息,而且更复杂的对象可能看起来很奇怪,但对于更简单的对象,Viewer3d 可能会非常有用(希望如此 :))。

Viewer3d 幕后

Viewer3d 由 600 多行 C# 代码组成。这段代码没什么特别之处。更有趣的部分是 XAML。在那里定义了视觉外观。我不会写关于我如何定义控件以及它们如何定位到窗口。如果您是初学者,我建议阅读 WinFX SDK 文档。我想指出 Viewer3d 中使用的一些更有趣和更高级的功能。

按钮样式

Viewer3d 窗口底部有五个按钮。当鼠标指针悬停在每个按钮上时,它会变得更亮。在 Avalon 中创建此类功能非常简单。以下代码定义了用于按钮的样式

<Style x:Key="SimpleButton" TargetType="{x:Type Button}">
    <Setter Property="Background" Value="VerticalGradient #ddd #888"/>
    <Setter Property ="VerticalAlignment" Value="Center"/>
    <Style.Triggers>
        <Trigger Property ="Button.IsMouseOver" Value="True">
            <Setter Property="Background" Value="VerticalGradient #fff #aaa"/>
        </Trigger>
    </Style.Triggers>
</Style>

上面的代码定义了一个 `SimpleButton` 样式,并设置了两个属性:`Background` 和 `VerticalAlignment`。它还定义了一个属性触发器,该触发器会在鼠标悬停在按钮上时更改 `Background` 属性。这只是一个非常简单的例子。通过这种方式,可以定义非常复杂的样式。最好的地方在于,这种功能无需使用任何代码即可定义。这意味着可以将样式定义在单独的资源文件中,然后只需简单地替换它们,就可以获得完全不同的外观,从而使应用程序焕然一新。

错误消息

其次,我想介绍 Viewer3d 显示加载 3DS 文件时发生的错误消息的方式。下图显示了一个示例错误消息。它显示在窗口中央,覆盖在之前的 3D 对象之上。当然,它可以做得更好——尤其是错误文本,它现在被截断了。但这总比没有好。而且这不仅仅是一个简单的错误消息。如果您在 Viewer3d 中输入错误的文件名,您会看到它更有趣的一面。

Error message

您会发现,4 秒后,整个消息将开始淡出。这是在 3DS 文件加载后 catch 块中完成的

this.FindStoryboardClock(errorDisappearing).ClockController.Begin();

上面的语句简单地启动了一个 `errorDisappearing` 动画。动画在 XAML 中定义

<Window.Storyboards>
    <ParallelTimeline x:Name="errorDisappearing">
        <SetterTimeline TargetName="errorGrid" Path="(Grid.Opacity)">
            <DoubleAnimation To="1" Duration="0:0:0.5"/>
            <DoubleAnimation To="0" Duration="0:0:2" BeginTime="0:0:4"/>
        </SetterTimeline>
    </ParallelTimeline>
</Window.Storyboards>

动画在 `Window.Storyboards` 元素中定义。它更改 `errorGrid` 的 `Grid.Opacity` 属性。在前 0.5 秒内,它将 `Opacity` 的当前值更改为 1——如果 `errorGrid` 被之前的动画隐藏了,它就会显示出来。从动画开始的 4 秒后,它开始将不透明度更改为 0(隐藏 `errorGrid`)。隐藏网格需要两秒钟。

相机预览面板

Viewer3d 中最后也是最复杂的 UI 元素是相机预览面板。它显示在相机面板的右侧,位于底部按钮的上方。但大多数时候它都是隐藏的。只有当鼠标指针在相机面板中时才会显示。它显示一个 3D 相机,该相机绕着一个金色球体旋转,并显示相机从哪个角度看向加载的 3D 对象。

预览面板的定义与其他面板类似。但它包含一个 `Viewport3D` 对象,而不是按钮、文本框和其他控件。除了相机,没有定义 3D 对象。它们在 `InitPreview` 方法中添加。首先,从 3DS 文件加载相机和球体。然后设置材质——相机为浅灰色,球体为金色。然后向场景添加两个旋转变换对象。它们用于围绕球体旋转相机。在通过移动自由相机或更改选定相机移动相机后,会计算新的旋转角度(在 `SelectedCameraChanged` 中),并旋转预览相机以显示新的相机位置。

这一切都在代码隐藏中完成。但面板功能也有 XAML 部分,它定义了面板的显示和隐藏

<Window.Triggers>
    <EventTrigger RoutedEvent="Panel.MouseEnter" SourceName="CamerasPanel">
        <EventTrigger.Actions>
            <StopAction TargetName="hidePositionPreviewPanel" />
            <BeginAction TargetName="showPositionPreviewPanel" />
        </EventTrigger.Actions>
    </EventTrigger>
    <EventTrigger RoutedEvent="Panel.MouseLeave" SourceName="CamerasPanel">
        <EventTrigger.Actions>
            <StopAction TargetName="showPositionPreviewPanel" />
            <BeginAction TargetName="hidePositionPreviewPanel" />
        </EventTrigger.Actions>
    </EventTrigger>
</Window.Triggers>

<Window.Storyboards>
    <ParallelTimeline x:Name="showPositionPreviewPanel">
        <SetterTimeline TargetName="PositionPreviewPanel" Path="(Grid.Opacity)">
            <DoubleAnimation From="0" To="1" Duration="0:0:0.5" />
        </SetterTimeline>
    </ParallelTimeline>
    <ParallelTimeline x:Name="hidePositionPreviewPanel">
        <SetterTimeline TargetName="PositionPreviewPanel" Path="(Grid.Opacity)">
            <DoubleAnimation From="1" To="0" Duration="0:0:0.5"/>
        </SetterTimeline>
    </ParallelTimeline>
</Window.Storyboards>

首先,定义了两个事件触发器。第一个在鼠标指针进入 `CamerasPanel` 时启动。第二个在鼠标离开面板时启动。仔细查看会发现,代码实际上启动和结束了两个操作:`hidePositionPreviewPanel` 和 `showPositionPreviewPanel`。操作在事件触发器下方定义。它们是简单的故事板,通过更改不透明度来显示或隐藏 `PositionPreviewPanel`。

结论

我希望您会觉得这个应用程序很有用——既可以用于从 3DS 文件加载 3D 对象,也可以作为具有一些“Avalon 炫酷功能”的示例 Avalon 应用程序。我认为直接从文件加载 3D 对象非常方便,也许将其作为 Avalon 对象模型的一部分是个不错的主意。

我必须说,我对 Microsoft 的工作非常满意。他们真正吸取了 Windows 和 Web 世界的优点,并将它们融入 Avalon。我迫不及待地等待 Longhorn 的发布以及为 Avalon 设计的新应用程序。

最后,我鼓励您将使用 Reader3ds 的演示或示例发布到 Avalon 的网络社区。

© . All rights reserved.