WPF,Silverlight与XAML读书笔记第三十九 - 可视化效果之3D图形

2023-05-10,,

原文:WPF,Silverlight与XAML读书笔记第三十九 - 可视化效果之3D图形

说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。

WPF将易用性的理念带入了3D世界,WPF中3D也工作在一种保留模式下,这意味着系统会负责刷新与重绘。WPF中2D图形与3D图形系统有着很紧密的融合,首先在绘图系统基础及2D图形篇所介绍的概念对3D图形是适用的。2D媒体,如Video,Drawing和Visual,可以显示在3D模型表面。而Viewport3D中的场景也可以融合到程序中其它UI元素,也可以放入ItemsControl中。

3D图形入门

3D图形系统的目的是使3D模型作为2D图形输出到屏幕等设备上。对3D系统的第一印象,来看这段XAML,其中定义了一个3D的小房子:

 <Page Background="Black"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Viewport3D>
<Viewport3D.Camera>
<OrthographicCamera Position="5,5,5" LookDirection="-1,-1,-1" Width="5"/>
</Viewport3D.Camera>
<Viewport3D.Children>
<ModelVisual3D x:Name="Light">
<ModelVisual3D.Content>
<AmbientLight/>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup x:Name="House">
<GeometryModel3D x:Name="Roof">
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Blue"/>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,1,1 0,2,1 0,2,-1 -1,1,-1 0,2,1 1,1,1
1,1,-1 0,2,-1"
TriangleIndices="0 1 2 0 2 3 4 5 6 4 6 7"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D x:Name="Sides">
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Green"/>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,1,1 -1,1,-1 -1,-1,-1 -1,-1,1 1,1,-1
1,1,1 1,-1,1 1,-1,-1"
TriangleIndices="0 1 2 0 2 3 4 5 6 4 6 7"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D x:Name="Ends">
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Red"/>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D
Positions="-0.25,0,1 -1,1,1 -1,-1,1 -0.25,-1,1 -0.25,0,1
-1,-1,1 0.25,0,1 1,-1,1 1,1,1 0.25,0,1 0.25,-1,1 1,-1,1
1,1,1 0,2,1 -1,1,1 -1,1,1 -0.25,0,1 0.25,0,1 1,1,1 1,1,-1
1,-1,-1 -1,-1,-1 -1,1,-1 1,1,-1 -1,1,-1 0,2,-1"
TriangleIndices="0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 15
17 18 19 20 21 19 21 22 23 24 25"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D.Children>
</Viewport3D>
</Page>

随着下文介绍的深入我们将了解其中这些元素的作用。

我们通过与2D图形系统中的类对比来对3D图形系统有个大致的认识。

2D类型

3D类型

描述

Drawing

Model3D

Drawing表示一片2D内容,如Geometry可以由一个Visual对象渲染

Model3D表示一个3D模型,可以由一个Visual3D渲染

Geometry

Geometry3D

Geometry表示一个2D形状Geometry,能够回答像边界和交叉点这样的问题。Geometry自身不能被渲染。GeometryDrawing结合了一个Geometry和一个Brush来呈现它的外观

Geometry3D表示3D表面。为了渲染Geometry3D,其使用GeometryModel3D将它和一个Material结合起来

Visual

Visual3D

Visual是渲染2D内容的元素的基类。其中包括所有的DrawingVisual和所有的FrameworkElement,后者如Control和Shape

Visual3D是渲染3D内容的元素的基类,ModelVisual3D是其一个子类,用于渲染Model3D的3D内容

Transform

Transform3D

Transform(的子类)用于进行2D Drawing和Visual的变换,如平移,旋转和拉伸

类似于Transform,在3D世界中,Transform3D用于对Model3D和Visual3D执行变换操作

除了表格中列出的扩展自2D图形的类, 3D世界中还有如下两个独有的概念。

Camera:场景中放置的虚拟相机,在特定位置,以特定的角度产生3D模型的图形。
Material与Light:在Brush填充过的表面上,进一步添加光照效果。

坐标系统

WPF中,2D,3D分别使用如下坐标系统

3D使用的坐标系统除了比2D坐标系统多z轴外,y轴的正方向是向上而非向下。另外2D图形系统中很少用负数坐标,但3D中负数坐标很普遍。所以2D图形中一般把左上角作为图像的中心,而3D中把原点作为空间的中心。

提示:WPF中使用右手坐标系统

左手坐标系统与右手坐标系统的不同在于z轴与x轴,y轴关系的区别,下面的图很好的说明了这个问题:

你只需要这样记:右手法则中,z轴是靠近我们,而左手法则中z轴是远离我们的,基本上就不会弄错了。

Camera

在3D应用设计中,最常打交道的两个Camera分别是OrthographicCamera和PerspectiveCamera,通过这两个类提供的属性可以设置Camera在3D坐标系中的位置和方向,下文将详细说明其中的属性及其控制效果。

Position属性

该属性控制Camera在空间中的位置。改变此属性即可移动Camera,从而建立场景的不同视图。该属性为Point3D类型,其中包括了x,y,z坐标来定义此点在坐标系中的位置。如下XAML(前文示例的一部分),我们在OrthographicCamera中设置了Position属性:

 <Viewport3D>
<Viewport3D.Camera>
<OrthographicCamera Position="5,5,5" LookDirection="-1,-1,-1" Width="5"/>
</Viewport3D.Camera>
</Viewport3D>

我们指定的属性值"5,5,5"表示我们将Camera放置在x,y,z轴正方向上各5个像素的一点,如图:

上面XAML中用到了我们接下来要介绍的一个属性。

LookDirection属性

该属性定义了Camera的朝向。其是Vector3D类型,该类型也包含x,y,z三个属性,但它们的作用是定义方向与幅度(Length)。以上文XAML中该属性设置的值"-1,-1,-1"来说,假设我们以上北下南前后来描述右手规则的坐标系,"-1,-1,-1"定义的方向为西北后方(见下图)。

而幅度通过这个公式得到。

提示:

一般情况下,对于Vector3D,系统只会确定其方向而不会立即计算出Length。Length主要用在如我们将Vector3D与Point3D进行相加(这样会得到一个新的Point3D对象)等计算,这时系统会自动计算Length值。

注意,Point3D与LookDirection的设置一定要搭配,如还是前面的例子,我们把Position改成(-5,-5,-5),而不改变LookDirection的话,目标就会在镜头中消失。得到LookDirection一个简单的方法是,在坐标系中找到一个想要看到的点,用其x,y,z依次与Position的x,y,z值相减得到的结果就可作为LookDirection的x,y,z值。

可以使用如下的代码进行计算,以用于Camara移动的场景下LookDirection的变化。

 camera.LookDirection = lookAtPoint - camera.Position;

UpDirection属性

通过LookDirection确定相机的朝向后,我们还需要通过UpDirection属性确定镜头不会绕中心点旋转,就像一个真实的相机,我们要固定其是横放还是竖放。UpDirection属性默认值是<0,1,0>(对于真实相机,这是最常见的横放的方法),而如<1,0,0>就是将相机"竖起来"。

如果是在场景中放置静态Camera,最好的方法就是直接设置上面介绍的这三个属性 – Position, UpDirection和LookDirection。而如果需要移动和旋转相机应该使用Camera的Transform属性。使用这个属性可以很容易的使Camera与目标对象保持一致变化,如使相机跟随目标对象移动,只需将Camera的Transform设置的与目标对象的Transform一致即可。

注意:要及时变换UpDirection

正如改变Position时,需要及时调整LookDirection来使目标一直出现在镜头中,在LookDirection发生某些改变时,UpDirection也要随之进行改变。例如,镜头从目标的一面移动到另一面,LookDirection也进行了相应改变,从望向一面转到另一面。这时UpDirection务必跟随LookDirection更新。否则当在一面时,Camera拍出来的照是正的,到了另一面Camera中目标会倒过来。

NearPlaneDistance属性与FarPlaneDistance属性

这两个属性用于避免这样的问题,即当Camera离目标对象过近或过远时会出现渲染问题(这是一种深度冲突问题)。NearPlaneDistance默认值为0.125(这个值一般不需要自行调整)。与Camera距离小于NearPlaneDistance定义的值目标对象的部分都会被裁减掉。而由于极远物体深度冲突不常发生,所以FarPlaneDistance默认值被设置为无限远。

平行投影与透视投影

回到两种Camera – OrthographicCamera与PerspectiveCamera,PerspectiveCamera呈现的图像接近人眼观看现实世界的效果,即离Camera越远的对象看起来越小,而OrthographicCamera中的目标对象无论距Camera远或近都会以实际大小呈现,可以用于精确测量分析。

由空间中的3D模型到Camera呈现出的2D画面,完成了一个投影的过程。OrthographicCamera与PerspectiveCamera分别使用了平行投影透视投影

在平行投影模式下投影到的平面与可视空间的大小是一致的(一个一对一映射的过程)。通过OrthographicCamera的Width属性可以指定可视区域的大小,而高度由Viewport3D自动计算以保证宽高比。下面的代码展示了Width的设置:

 <Viewport3D>
<Viewport3D.Camera>
<OrthographicCamera Position="5,5,5" LookDirection="-1,-1,-1" Width="5"/>
</Viewport3D.Camera>
</Viewport3D>

在透视投影模式下,即使用PerspectiveCamera时,可视区域宽度随着与Camera的距离的增大而增大。由于离Camera越远可视范围区域越大,距离远的对象在投影中会更小。通过PerspectiveCamera的FieldofView属性可以控制视野扩张的水平角度。理论上这个角度越大,可视区域的范围越大,远处的物体看起来会越小。FieldOfView可以理解为真实相机变焦的作用。

PerspectiveCamera的Width属性的作用与OrthographicCamera的Width属性作用类似。Width与FieldOfView搭配使用,当这两个值较小时会对目标对象的某一部分进行放大(如使用相机时,我们常说的拉近镜头的效果),反之当这两个属性取较大值时可以展示更多的场景。

MatrixCamera

WPF中提供的MatrixCamera是一种高级相机,其让用户可以通过Matrix3D定义视图及投影变换。如PerspectiveCamera与OrthographicCamera支持的投影模式都是一种预定义的变换矩阵。当需要获得高级效果时需要使用MatrixCamera。另外MatrixCamera使用的变换矩阵模型与Direct3D完全一致,可以很容易的实现应用的移植。

Transform3D

Transform3D用于Model3D,ModelVisual3D和Camera的Transform熟悉国内,来对3D对象执行移动,旋转或拉伸。

Transform3D有如下5种子类,用于执行3D变换:

TranslateTransform3D
ScaleTransform3D
RotateTransform3D
MatrixTransform3D
Transform3DGroup:用于包含一组Transform3D集合,以将多个变化应用到3D对象上。

下文将详细介绍这些类及其使用。

TranslateTransform3D

TranslateTransform3D将对象相对容器进行偏移,其OffsetX,OffsetY和OffsetZ三个属性用来指定各个方向上的偏移量。如下面的代码将位于坐标系原点的模型移动到(3,2,1)这个位置。

 <ModelVisual3D>
<ModelVisual3D.Transform>
<TranslateTransform3D OffsetX="3" OffsetY="2" OffsetZ="1" />
</ModelVisual3D.Transform>
</ModelVisual3D>

ScaleTransform3D

ScaleTransform3D用于改变3D对象的大小,ScaleX,ScaleY和ScaleZ属性分别控制每个方向上的缩放比例。要在对3D对象执行缩放时维持比例需要将这三个属性设置成相同的值,当然这三个值可以独立设置(甚至只设置其中的1到2个)。

另外还有三个属性CenterX,CenterY和CenterZ属性用来设置缩放的中心点。默认缩放的中心点是坐标系的原点。若3D对象内没有一点与原点重合,缩放就会导致3D对象移位,只需要将缩放中心点设置为3D对象中的任一点就可以实现原地缩放。另一种解决这个问题的方法是缩放后平移,平移的值就是坐标原点(即默认缩放中心)与3D对象上要以此为缩放固定点进行原地缩放的点之间的坐标距离。

另外,当缩放值设置为0时(所有方向上),对象会被缩成一个点(注意,需要等比例缩放时,应该讲缩放比例设成1),而如果缩放值设置成负数,则会在被设为负值的方向的反方向上产生镜像效果,并且其也按负值的大小进行了比例缩放。

提示:在底层对于通过CenterX,CenterY和CenterZ指定缩放中心的变化的处理上使用的方法是先将指定的缩放中心移动到坐标原点,等缩放完成后再平移回去。

RotateTransform3D

RotateTransform3D用于在空间中旋转3D对象。旋转的定义通过Rotation3D对象描述,Rotation3D是一个抽象类,其有两个字类:

AxisAngleRotation3D – 沿指定轴将对象旋转Angle属性指定的度数。这是实现旋转最简单的方法。

QuaternionRotation3D – 使用Quaternion来定义一个旋转,Quaternion对Axis/Angle(轴/角度)旋转编码方法被许多其它3D工具使用。使用这种方式可以很容易将旋转变化进行导入导出。

通过RotationTransform3D定义的旋转,会使坐标系按指定的度数旋转,在右手坐标系中一个正值的角度会使坐标系逆时针旋转。这种旋转变化存在着与缩放变化同样的问题即当旋转轴不在3D对象上时,旋转坐标系后对象位置会发生移动。两种解决方法也同前面介绍,一是在旋转之后通过平移调整位置。而是通过CenterX,CenterY和CenterZ属性改变旋转中心。注意Axis属性指定的旋转轴不能确定旋转中心,如我们通过Axis指定绕Y轴旋转,还需要指定在X-Z平面上具体哪一点绕Y轴旋转。

Transform3DGroup

如上文中所提到的那样,在3D变化中,多种变化往往需要同时进行,如先缩放再平移或先旋转再平移,通过Transform3DGroup可以很方便的把1个以上的变换组合成一个变换,其使用与2D中TransformGroup很类似这里不再给出代码示例。

MatrixTransform3D

这是一种很复杂的变换,用于定义其他几种变换所无法实现的效果,或者将其它基于矩阵表示变换的程序移植到WPF中。另外,前面介绍的包括Transform3DGroup在内的变换都可以通过其Value属性的得到Matrix3D的表示。

Model3D

Model3D作用正如其名,为3D场景构建模型。通常把多个Model3D组合在一起来生成单个3D模型。Model3D在一些特性及使用上类似2D中Drawing。

Model3D有3个子类完成具体功能。

Light – Light提供的一些子类用于向场景中投射光线。在实际使用中常把Light结合到后文要介绍的Model3DGroup中来实现如车灯照射等效果。
GeometryModel3D – 与给定的Material结合使用来渲染表面。GeometryModel3D类似于2D中的GeometryDrawing。
Model3DGroup – 用于包含一组Model3D,如组合多个GeometryModel3D,组合Light来照射3D模型。

下面将详细介绍Model3D这三个组成部分。

Light

光照(Light)也是WPF 3D中独有的概念,其基本作用就是根据场景中3D对象与光源的远近动态计算3D对象的明暗。光照效果的出现也是通过三个部分的结合。Light对象用于把光线发射到场景中;Material用于把光线反射给Camera;模型几何体确定入射及反射光线的角度。这一部分中先重点讨论Light对象,从WPF支持的不同种类的Light开始:

DirectionalLight:从无限远处的光源发射平行光到场景,其可以模拟太阳光照射的效果。
PointLight:从场景中一个点向各个方向均匀发射光线,光线强度随着距光源距离增加而减弱。其用于提供一种没有聚焦的光源效果,现实生活中的灯泡就类似这种效果。
SpotLight:从场景中一点发射逐渐扩散的锥形光源,光线强度同样是随着距离增加而减弱。如手电筒所发的光线就是这种光源的效果。
AmbientLight:均匀照射模型的每一个表面。如果是白色的AmbientLight由于缺乏明暗变化会使目标的视觉感很"平"。而较暗的AmbientLight会在场景表面产生漫反射的效果。
下面我们详细了解每种光源的特性及使用。

    DirectionalLight

    由于光源距离近乎无限远,从而光线接近平行。DirectalLight中,Direction属性控制光线照射场景的方向,而Transform属性(继承自Model3D基类)可以影响光照方向。Color属性用来控制Light的颜色。

提示:Color属性可以反应光照强度,#FFFFFF是完全强度的白光,而其值的一半#808080是半强度的白光。而alpha通道的设置对光照无影响,另外光照效果可以重叠。如:同方向的两个半强光叠加可以产生一个完全强度的光照,而方向不同的光也会交叉重叠。下面这行XAML展示了一个简单的例子:

 <ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Direction="1,-1,-0.5" Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>

单一的使用Direction所产生的效果不是很自然,可以结合AmbientLight使效果更自然。

    PointLight

    PointLight与DirecitonalLight不同的一点在于前者光线强度随着距离的增加而减弱。PointLight中,Position属性用于指定光源的位置,ConstantAttenuation,LinearAttenuation和QuadraticAttenuation三个属性一起控制光线随距离增加的衰减比。我们把三个属性分别简写为C,L,Q,另外用d表示光源与被照射点的位置。则衰减率公式为:

从公式可以看出,将C,L,Q分别设置为1,0,0,就可以得到距离无关,等强度的PointLight。

PointLight还有一个Range属性,用于定义光源的范围(以光源为中心,半径为指定值的范围),在这个范围之外光线不再有效。其默认值为无穷大最后给出一段XAML展示PointLight的使用:

 <ModelVisual3D>
<ModelVisual3D.Content>
<PointLight Color="White" Position="2,2,2"
ConstantAttenuation="0" LinearAttenuation="0"
QuadraticAttenuation="0.125" />
</ModelVisual3D.Content>
</ModelVisual3D>
    SpotLight

    现实世界中,使用凹透镜或反射镜会出现SpotLight的效果。WPF实现中,通过将PointLight发射的光限制在一个椎体上来模拟(角度范围限制)。SpotLight中,Direction属性指定了椎体的方向,而OuterConeAngle和InnerConeAngle属性控制者椎体的形状(椎体张开的角度)。InnerConeAngle与OuterConeAngle之间的区域是一个发散区间。光照强度会由指定值一直衰减到无。在InnerConeAngle区间内光照在同一半径上会保持指定强度。改变InnerConeAngle与OuterConeAngle的区间可以调整发散区域的大小。当OuterConeAngle小于InnerConeAngle时就没有发散区域了。

    AmbientLight

    AmbientLight通常用来在多个表面上产生漫反射的效果。Ambient最重要的属性是Color,其控制光线的强度与颜色;另外Transform属性对AmbientLight没有效果。

    前面提到在AmbientLight中使用强度过高的颜色,会使目标看起来白茫茫一片,最好的产生自然光效果的方法是仅使用一个AmbientLight,且使用低于三分之一的白色(#555555或更小)。

GeometryModel3D

GeometryModel3D对象用来定制3D几何体,3D几何体构成了可视对象的形状。而Geometry3D定义的3D几何体本身是无法展示出来的,为了可以看到3D几何体的表面,需要将其与Material一起使用。而GeemotryModel3D这个Model3D的作用就是将Geometry3D与Material属性相结合,而Model3D是可以最终被渲染的对象。

首先来看一个GeometryModel3D的具体例子,其中Material部分定义了一个蓝色的DiffuseMaterial,Geometry部分使用MeshGeometry3D描述了一个矩形。

 <ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Blue" />
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,1,0 -1,-1,0 1,-1,0 1,1,0"
TriangleIndices="0 1 2, 0 2 3" />
</GeometryModel3D.Geometry>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>

首先我们先详细介绍下例子中的第一部分 – Material。

Material

如前所述,Light对象的属性决定场景中光线的方向和颜色。Material对象的属性会最终确定反射到观察者的光线,即我们最终看到的图像。

我们先来介绍下色彩的原理,在现实中,比如一个苹果看起来是红色的,是因为果皮反射了红色的光,同时吸收了其他波长的光。Material对象的属性正是决定了反射回哪些颜色到Camera从而建立图像。

WPF内置的Material有如下几类:

DiffuseMaterial – 从所有角度散射抵达表面的光线,效果如新闻纸,平台但不光滑。
SpecularMaterial – 对入射光线以相同的角度反射,用于产生像塑料或金属等光滑表面的高光效果。
EmissiveMaterial – 近似于会发出光线的表面(但这个光不会照亮其他对象),所以无论场景中是否有光照对象,EmissiveMaterial看上去总是亮的,其用于创建总以完全亮度显示的图像,以及不需要阴影的图像。
MaterialGroup – 组合使用多个Material其中最后的Material显示在最前面,这样依次排列。

下面将详细介绍这些Material。

    DiffuseMaterial

DiffuseMaterial是最常用的一种Material。散射的强度与光线与被照射面的角度有关。光线直射时反射强度最大。所有对于一个球形物体,正对光源的部分看起来较亮,而两侧较暗。另外Camera的角度对反射没有影响。

DiffuseMaterial的Brush属性定义了其反射的颜色。如给一个物体定义的Material使用了红色的Brush(以SolidColorBrush为例),当我们向其投射白光(白光是所有色光在一起的光),其会表现为红色。当然我们还可以通过使用渐变画刷,甚至是ImageBrush,使反射光呈现渐变效果或更多自定的效果。这里需要注意,当使用SolidColorBrush之外的Brush时,需要使用TextureCoordinate来控制Brush的某一部分如何对应3D对象的某一部分。如果不指定TextureCoordinate模型不会被渲染,(这时WPF不能知道Brush颜色与模型表面点的映射关系,而对于SolidColorBrush模型上每个点映射到相同的颜色,所以可以不设置TextureCoordinate属性)。使用Brush来指定Material的另一大好处是灵活的数据绑定,不但可以通过数据绑定控制3D模型,还可以将2D中的DrawingVideo等作为对象表面的纹理。

DiffuseMaterial的Color属性用于过滤Light的颜色,只有其指定的颜色才可以被反射,其默认值为白色也就是不进行过滤。

最后要说的一个属性是AmbientColor,通过这个属性设置的颜色只会对来自AmbientLight中的颜色起过滤作用,其主要作用也就是控制表面对环境光的反射程度。

提示:WPF处理重叠表面的方式

对于层叠的对象有两种处理方式:一,将所有的对象排序,然后自后先前渲染。二是,深度缓冲,这是WPF使用的方式,这种方式下最靠近Camera的表面会最后渲染,这种方式比前一种快很多,当然副作用是远处的目标不再被渲染,当最前面的对象是透明时会有很大问题。对此的解决方向是将透明的DiffuseMaterial放在集合最后,来保证透明对象后的对象先被渲染。或者可以使用EmissiveMaterial,其不使用深度缓冲方式被混合 ,且其也可以创建类似透明的效果。

    SpecularMaterial

这种Material会反射光线,当Camera与光源夹角较小时,目标会像镜子般反射光线到Camera,且仅当Camera靠近反射光线时SpecularMaterial的效果才能被观察到。SpecularMaterial常与DiffuseMaterial结合使用,给硬的、闪亮的表面添加高亮效果。如下面这段XAML所示:

 <GeometryModel3D.Material>
<MaterialGroup>
<DiffuseMaterial Color="Red" />
<SpecularMaterial Color="White" SpecularPower="40" />
</MaterialGroup>
</GeometryModel3D.Material>

其中SpecularPower属性控制高亮反射的聚焦程度,值越大,聚焦程度越大高亮效果越强。

提示:组合Material得到常见的效果

如将明亮的DiffuseMaterial和白色的SpecularMaterial结合在一起,表面上看起来就像塑料。使用暗的DiffuseMaterial和具有相同色调且明亮的SpecularMaterial会得到金属效果的表面。

同DiffuseMaterial,最后反射到Camera的颜色也是由Light的Color属性,SpecularMaterial的Brush和Color属性共同决定的。且SpecularMaterial的Brush属性也可以指定为各类Brush对象,从而使反射光线呈现各种变化效果。另外注意,SpecularMaterial没有提供AmbientLight属性,因为这种光线是没有方向的。

    EmissiveMaterial

EmissiveMaterial总是向Camera发出可见光,其光线不会像Light那样被反射。

EmissiveMaterial向图像添加光照效果的原理是混合入图像,通俗表述就是使物体透亮,如我们在灯笼里点一根蜡烛,则灯笼表面就是透亮物。另外这种混合没有阻止后方物体反射的光线穿过自身。为了防止这种被穿透可以通过MaterialGroup将EmissiveMaterial与DiffuseMaterial结合使用,参见如下XAML:

 <MaterialGroup>
<DiffuseMaterial Brush="Black" />
<SpecularMaterial Brush="Green" />
</MaterialGroup>
    MaterialGroup

通过MaterialGroup可以在一个表面应用多个材质。MaterialGroup中材质的渲染顺序正是安其定义的顺序。在上面介绍其它材质时也提到了一些组合使用的技巧。

Geometry3D

接下来重点了解一下构成GeometryModel3D的另一个重要组成部分Geometry3D,当前Geometry3D只有一个子类MeshGeometry3D来实现功能。

MeshGeometry的作用是将一组指定的3D表面表示为三角形序列。MeshGeometry3D的主要属性如下:

Positions:定义了目标中包含的三角形的顶点。
TriangleIndices:定义了 Positions中三角顶点的连接关系,如果没有指定该属性,则表示按点在Positions中出现的顺序连接:0,1,2然后3,4,5依此类推。
Normals:该属性用来调整网格的光照
TextureCoordinates:该属性前文有提到,用于为Material使用的表面指定3D到2D的映射方法。

下面重点介绍这些属性及相关概念。

    Position

    网格中的三角形的顶点通过3D坐标定义。默认情况下Position集合中3个Point3D组成一个三角形。如下面这段XML定义了两个三角形:

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

    所有模型均使用三角形组合而成,包括一些弯曲的表面也是用小三角形拼接来达到近似的效果。小三角形拼接时会有很多共同边,这就需要通过自定义点连接来实现。这是使用TriangleIndices的默认值所做不到的。如下面的例子我们使用四个点构造了一个正方形(拼接两个三角形,上面例子中小房子的一面墙)

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

    如例子所示,通过TrangleIndices使三角形可以共享点的位置,此时三角形被认为是连续表面的一部分。而当不共享点位置时,三角形是分开的临接表面,有着不同的Normal和TextureCoordinates定义。

提示:WPF中的MeshGeometry3D会确保共享点的三角形间的相邻边被无缝渲染。而如果通过Transform3D使两个MeshGeometry3D重叠则可能由于误差产生小缝隙,而避免方法是使变换程度尽量大到产生微量叠加。

Normals

Normals属性表述一个几何学中法线的概念。法线是经过某一点垂直于表面的直线。在WPF 3D中通过指定经过各顶点的法线来通知系统三角形是表示平的表面还是表示近似的曲面。WPF中Normals集合中每个变量的类型都是Vector3D对象,且这些对象的个数与Position对象相同,来一对一的表示Position中每个点发现的方向。

我们以如下图形为例讲述法线的作用。

如在一个3D空间中我们有两个相邻但不在一个平面上的三角形。假设我们不为其显式指定法线,且两个三角形之间没有共享点,那么法线就是通过各项点垂直于面的线(面法线)。当我们由Y轴正方向向负方向垂直看去,三角形两条边及法线(蓝色标识)方向如下图所示:

则这时两个三角形的面看起来都是平的,交接处会有很明显的棱角的效果。

而如果两个三角形共享顶点,且没有显式指定法线。系统会根据共享点原本的两条面法线,取一个中间值作为法线,如下图:

这时,由于法线不与面垂直,其产生的阴影会被平滑的内插在三角形的表面,两个三角形相邻处也看不到任何明显的折痕。

对于一个平面(如单一一个三角形),三条法线也是平行且垂直于平面,这时平面看起来也是平坦,要想让其看起来有近似弯曲的表面。只需调整其中一到两条法线的方向即可。

TextureCoordinates

在2D图形中,当向一个GeometryDrawing应用Brush时,系统会很自然的把Brush应用到2D图形边界范围之内。在3D图形中需要使用TextureCoordinates手动指明映射,这个属性是一个集合属性,每一项均为Brush范围内的2D点。通过这些点将3D空间的三角映射到Brush空间的三角。

Model3DGroup

Model3DGroup派生自Model3D,用于将Model3D的对象集合组合为一个单独的模型,如将多个GeometryModel3D对象组合在一起建立一个使用不同Material的模型(一个GeometryModel3D中只能组合一套Material与Geometry3D)的模型。

Visual3D

如同可以被呈现到屏幕的2D元素都继承自Visual基类,Visual3D也是可以被呈现的3D内容的根结点。另外Visual3D同样支持命中测试等特性,且也是通过VisualTreeHelper来使用。下面将详细介绍Visual3D的具体子类ModelVisual3D

ModelVisual3D

ModelVisual3D类似于2D图形系统中的DrawingVisual。ModelVisual3D的Content属性用来设置内容,其接受并呈现的内容为Model3D的对象。而且同一个Model3D对象可以在多个ModelVisual3D间重用,甚至用于处于嵌套层次中 的ModelVisual3D。另外,ModelVisual3D有一个集合类型的Children属性用于嵌套其他ModelVisual3D对象,从而将多个Visual3D组合在一起。特别注意,不同于许多其它以Content作为内容属性的类,ModelVisual3D将Children作为内容属性,所以可以使用最自然的方式来将ModelVisual3D嵌套起来(但同时注意,设置Content时,XAML中一定要添加Content元素)。

提示:关于Model3DGroup和ModelVisual3D的选择

这两个类都具有组合子元素的作用,在它们使用的选择上也很简单,举一个例子就能明白,比如一个场景中有车,有房。则使用Model3DGroup来组合车的轮子,架子和玻璃,而ModelVisual3D用来将房子和车组合在一起。

提示:ModelVisual3D可以被扩展,建立一个增加了自定义行为的、可重用的Visual3D类。

3D命中测试

Visual3D的可视命中测试同样是用于找到哪一个可视对象(Visual)在指针点击处,在3D中做命中测试最简单的方法是监听Viewport3D元素的鼠标事件,参见下面的代码:

 <Viewport3D MouseDown="Viewport3D_MouseDown"> 
 private void Viewport3D_MouseDown(object sender, MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e); Viewport3D viewport3D = (Viewport3D) sender;
Point location = e.GetPosition(viewport3D); HitTestResult result = VisualTreeHelper.HitTest(viewport3D, location); if(result != null && result.VisualHit is Visual3D)
{
MessageBox.Show("点击了3D对象");
}
}

当然在3D中,使用委托来反馈结果的HitTest函数的重载也是被支持的。如同在2D中一样,结果以由后到前的顺序返回。HitTest还有一个重载接受一个Visual3D和一个HitTestParameters3D作为参数。

提示:在命中测试中,可以根据命中目标的不同将HitTestResult转化为PointHitTestResult或GeometryHitTestResult等类型。比较特殊的,当命中目标是一个3D网格时,可以将HitTestResult强制转化为一个RayMeshGeometry3DHitTestResult对象,其中会包含大量交叉点的详细信息。

Viewport3D元素

Viewport3D相当于2D中的FrameworkElement。Viewport3D的父元素是像Window或者Grid这样的2D元素。Viewport3D的子元素是Visual3D。Visual3D的子元素描述的3D场景是在Viewport3D矩形布局框中渲染。Viewport3D的Camera属性控制了从Viewport3D中看到的3D场景的视图。

提示:需要显示指定Viewport3D的Width和Height,Viewport3D 不能像Button那样根据其中的内容自动调整大小,也就说Viewport3D不知道其中的Visual3D有多大而默认会将Width和Height置0,从而无法显示内容。

作为一个FrameworkElement对象,Viewport3D支持系统的布局功能。这样可以将3D元素集成到几乎任何地方。如同过Sytle或ControlTemplate属性将控制外观换成可以交互的3D内容。

提示:在内部Viewport3D是通过Viewport3DVisual将3D可视树关联到2D可视树上2DVisual对象的。Viewport3D在Viewport3DVisual的基础上增加了Viewport属性,用来实现Framework层拥有而Visual层没有的布局的概念,以设置3D场景的显式范围。

本文完

参考:

《WPF揭秘》

WPF,Silverlight与XAML读书笔记第三十九 - 可视化效果之3D图形的相关教程结束。

《WPF,Silverlight与XAML读书笔记第三十九 - 可视化效果之3D图形.doc》

下载本文的Word格式文档,以方便收藏与打印。