原文:WPF中对三维模型的控制
(以下选自南开大学出版社出版的《WPF和Silverlight教程》)
3Dmax中的建模模型可以导出为obj文件格式,将此文件导入WPF项目中,由WPF完成对三维造型的贴图和控制设计。本例在3Dmax中设计了1个双翼开瓶器模型,将“开瓶器.obj”和贴图材质文件都添加到项目中(“素材”文件夹)。图2-206 的左侧是“开瓶器.obj”文件拖入到【设计面板】后,在【对象和时间线】面板中看到的结构,右侧是贴图后的开瓶器模型,中间是本例完成的对开瓶器部件进行拆卸和装配的控制按钮。下面说明设计过程。
1.obj文件导入后的对象
图2-206左侧看到的是obj文件导入后的对象结构,ViewPort3D(命名为viewport3)是三维对象的容器,其中包含相机元素Camera(ModelVisual3D),默认相机是透视相机PerspectiveCamera(已经命名为ppc);World元素(ModelVisual3D)中包含了环境光子元素AmbientLightContainer(ModelVisual3D)、方向光子元素DirectionalLightContainer(ModelVisual3D)和三维造型子元素RootGeometryContainer(ModelVisual3D),后者又包含了qua02(3Dmax中的原名)等6个ModelVisual3D子元素,每个子元素包含造型元素材质设置DefaultMaterial(GeometryModel3D)。
图2-206
开瓶器结构、外观和控制按钮
表2-1三维造型元素名称(3Dmax中的原名)和开瓶器部件名称对照
三维造型元素名 |
开瓶器部件名 |
三维造型元素名 |
开瓶器部件名 |
qua01 |
左翼 |
Object |
开瓶器座 |
qua02 |
右翼 |
Object03 |
左翼螺钉 |
pemo |
开瓶器把手 |
Object04 |
右翼螺钉 |
2. 三维造型元素初始位置
obj文件拖入Window3.xaml【设计面板】后,尽量发大到和设计窗口界面一样大小,这时的大小不一定合适,可以调节的照相机初始位置,如图2-207左图。其中参数是调整后的参数,比如Position,在obj文件刚导入时,X、Y和Z不一定是目前的数值,改变Z参数的数值可以调节三维造型在屏幕中的大小。Direction参数中的X、Y调节到0,相机面向Z坐标轴的负方向。本例中Far
Clipping Plane的参数调节的比较大,当3D对象缩小的很小时还能完整看到造型全貌。
图2-207
照相机初始位置参数设置和World初始变换设置
另外,“World”元素做了位移变换,见图2-207右图,Z坐标使造型大小改变,Y坐标产生上下位移。所有三维造型元素的其他变换参数默认是0,知道这些参数的初始值对后面的故事板设计很重要。
3. 贴图和光线设置
贴图需要的材质图片“金属7.jpg”和“外壳.jpg”已经添加到项目的“素材”文件夹中,将这些图片拖入【设计面板】后生成画刷资源,保存到ResourceDictionary1.xaml资源文件中,用于三维造型元素的材质贴图。“Object”元素(“开瓶器座”)的“DefaultMaterial”贴图使用了“外壳.jpg”生成的画刷资源,“螺钉”没有贴图,采用“白色”材质,其余采用“金属7.jpg”
生成的画刷资源,贴图过程略。
环境光“AmbientLight”采用定向光,设置为白色,方向光DirectionalLight也采用定向光,设置为白色,调整初始角度可以明亮照射3D对象。
4. 开瓶器旋转故事板设计
示例中设计了1个故事板StoryBoard0(程序中的命名为mystoryboard0),用于实现整个造型的三维空间旋转,如图2-208所示。
图2-208 电动机三维空间旋转故事板设计
故事板StoryBoard0针对三维元素“World”和方向光DirectionalLight设置了动画,故事板中有5个关键帧,“World”围绕Y轴进行旋转变换(参考图2-203),分别是0、90、180、270、360,时间间隔供8秒。同时,对方向光进行跟踪设置,保证旋转时方向光能够明亮照射到3D对象。图2-206中的“旋转”按钮(名为xuanzhuan)的事件代码就是启动此故事板。WPF中设计故事板时会自动生成事件触发器,自动启动故事板,本例将触发器全部删除,用代码启动后停止。
5. 开瓶器部件拆卸和装配故事板设计
开瓶器部件拆卸共设计了5个故事板,StoryBoard1到StoryBoard5(程序中的名称分别是mystoryboard1到mystoryboard5),分别顺序用于设计拆卸“左翼螺钉”、“右翼螺钉”、“左翼”、“右翼”、“开瓶器把手”的动画。
开瓶器部件装配也设计了5个故事板,StoryBoard6到StoryBoard10(程序中的名称分别是mystoryboard6到mystoryboard10),分别顺序用于设计装配“开瓶器把手”、
“右翼” 、“左翼”、“右翼螺钉”和“左翼螺钉”的动画。
拆卸动画和装配过程的动画运动过程是相反的,拆卸动画的终点参数应该是装配动画的起点参数,装配动画的终点参数是拆卸动画的起点参数,动画时间间隔可以一样,运动路径可以有差异,但起点和终点参数必须对应,否则部件就不能还原到原来位置了。动画设计过程是雷同的,图2-209左图是开瓶器所有可拆卸部件全部拆卸后在屏幕中的放置位置布局。
图2-209
开瓶器部件拆卸后放置在屏幕的位置布局和“开瓶器把手”拆卸故事板设计
下面以“开瓶器把手”为例,说明其拆卸动画和装配动画的设计。
“开瓶器把手”的拆卸动画故事板是StoryBoard5(程序中名为mystoryboard5),设计图如图2-209右下图。拆卸故事板有10个关键帧。“开瓶器把手”的装配动画故事板是StoryBoard6(程序中名为mystoryboard6),设计图如图2-209右上图。装配故事板同样有10个关键帧。对应的变换参数如表2-2。
从表2-2的参数中可以看出拆卸动画的终点参数是装配动画的起点参数,装配动画的终点参数是拆卸动画的起点参数,中间的参数有差异仅仅反映中间运动过程有异,这并不重要。
其他故事版的设计雷同,不再列出。
6. 程序设计
程序设计有下面几点要说明:
第一,图2-206中有4个按钮,其中有1个“复位”按钮,恢复三维对象的原来状态,使用删除多于变换的方法。“旋转”按钮启动的是StoryBoard0故事板。“自动拆卸”按钮单击后将会依次启动故事板StoryBoard1到StoryBoard5,“自动装配”按钮单击后将会依次启动故事板StoryBoard6到StoryBoard10。
第二,故事板的控制没有使用触发器,自动生成的所有触发器均被删除,故事板的控制采用前面介绍过的利用故事板资源设置代码控制故事板。
第三,故事板的依次启动指前一个故事板完成后才能启动后一个故事版,这样在程序上需要设置故事板的Completed事件。
表2-2 StoryBoard5和StoryBoard6关键帧参数设置
时间 |
拆卸动画StoryBoard5 |
装配动画StoryBoard6 |
||
位移变换参数 坐标X、Y、Z |
旋转变换参数 角度X、Y、Z |
位移变换参数 坐标X、Y、Z |
旋转变换参数 角度X、Y、Z |
|
0 |
0,0,0 |
0,0,0 |
0,110,0 |
0,0,0 |
1 |
0,10,0 |
0,90,0 |
0,90,0 |
0,0,0 |
2 |
0,20,0 |
0,180,0 |
0,70,0 |
0,0,0 |
3 |
0,30,0 |
0,270,0 |
0,60,0 |
0,0,0 |
4 |
0,40,0 |
0,360,0 |
0,50,0 |
0,0,0 |
5 |
0,50,0 |
0,90,0 |
0,40,0 |
0,0,0 |
6 |
0,60,0 |
0,180,0 |
0,30,0 |
0,-90,0 |
7 |
0,70,0 |
0,270,0 |
0,20,0 |
0,-180,0 |
8 |
0,90,0 |
0,360,0 |
0,10,0 |
0,-270,0 |
9 |
0,110,0 |
0,360,0 |
0,0,0 |
0,-360,0 |
下面是程序代码,有相关解释,不再赘述。
public partial class Window3 :
Window
{
//旋转故事板
Storyboard mystoryboard0=new Storyboard();
//拆卸故事板
Storyboard mystoryboard1=new Storyboard();
Storyboard mystoryboard2=new Storyboard();
Storyboard mystoryboard3=new Storyboard();
Storyboard mystoryboard4=new Storyboard();
Storyboard mystoryboard5=new Storyboard();
//装配故事板
Storyboard mystoryboard6=new Storyboard();
Storyboard mystoryboard7=new Storyboard();
Storyboard mystoryboard8=new Storyboard();
Storyboard mystoryboard9=new Storyboard();
Storyboard mystoryboard10=new Storyboard();
//定义鼠标跟随对象,FollowMouse3D是自定义类
FollowMouse3D fm3d=new FollowMouse3D();
Point mouseLastPosition;
//定义变量,记忆相机位置坐标
double cameraX,cameraY,cameraZ;
//设置三维变换组变量
Transform3DGroup GroupTF3D;
//记忆三维变换组中的子变换数
int transforms;
public Window3()
{
this.InitializeComponent();
mystoryboard0=(Storyboard)this.FindResource("Storyboard0");
mystoryboard1=(Storyboard)this.FindResource("Storyboard1");
mystoryboard2=(Storyboard)this.FindResource("Storyboard2");
mystoryboard3=(Storyboard)this.FindResource("Storyboard3");
mystoryboard4=(Storyboard)this.FindResource("Storyboard4");
mystoryboard5=(Storyboard)this.FindResource("Storyboard5");
mystoryboard6=(Storyboard)this.FindResource("Storyboard6");
mystoryboard7=(Storyboard)this.FindResource("Storyboard7");
mystoryboard8=(Storyboard)this.FindResource("Storyboard8");
mystoryboard9=(Storyboard)this.FindResource("Storyboard9");
mystoryboard10=(Storyboard)this.FindResource("Storyboard10");
//声明故事板完成事件
mystoryboard1.Completed+=new
System.EventHandler(mystoryboard1_Completed);
mystoryboard2.Completed+=new
System.EventHandler(mystoryboard2_Completed);
mystoryboard3.Completed+=new
System.EventHandler(mystoryboard3_Completed);
mystoryboard4.Completed+=new
System.EventHandler(mystoryboard4_Completed);
mystoryboard5.Completed+=new
System.EventHandler(mystoryboard5_Completed);
mystoryboard6.Completed+=new
System.EventHandler(mystoryboard6_Completed);
mystoryboard7.Completed+=new
System.EventHandler(mystoryboard7_Completed);
mystoryboard8.Completed+=new
System.EventHandler(mystoryboard8_Completed);
mystoryboard9.Completed+=new
System.EventHandler(mystoryboard9_Completed);
mystoryboard10.Completed+=new
System.EventHandler(mystoryboard10_Completed);
//远景相机初始位置
cameraX=ppc.Position.X;
cameraY=ppc.Position.Y;
cameraZ=ppc.Position.Z;
//声明或获取当前World的三维变换组(xaml中)Transform3DGroup
GroupTF3D = World.Transform as Transform3DGroup;
//记录三维变换组中子变换的总数
transforms=GroupTF3D.Children.Count;
//故事板属性设置
this.mystoryboard0.RepeatBehavior=RepeatBehavior.Forever;
this.mystoryboard0.FillBehavior=FillBehavior.Stop;
this.mystoryboard0.BeginTime=TimeSpan.FromSeconds(2);
this.mystoryboard1.BeginTime=TimeSpan.FromSeconds(2);
this.mystoryboard6.BeginTime=TimeSpan.FromSeconds(2);
this.mystoryboard0.Begin();
}
//复位按钮,调用自定义方法(复位操作)
private void reset_Click(object
sender, System.Windows.RoutedEventArgs e)
{
Reset();
}
//自定义方法,复位操作
private void Reset(){
this.mystoryboard0.Stop();
//恢复相机初始位置
ppc.Position = new Point3D(cameraX, cameraY,cameraZ);
int j=GroupTF3D.Children.Count;
//保留原来的变换数,其余删除
if (j>transforms){
for (int k=j-1;k>transforms-1;){
GroupTF3D.Children.RemoveAt(k);
k=GroupTF3D.Children.Count-1;
}
}
}
//旋转按钮事件
private void xuanzhuan_Click(object sender,
System.Windows.RoutedEventArgs e)
{
this.mystoryboard0.Begin();
}
//自动拆卸
private void button6_Click(object sender,
System.Windows.RoutedEventArgs e)
{
Reset();
this.mystoryboard1.Begin();//左翼螺钉拆卸
}
private void mystoryboard1_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard2.Begin();//右翼螺钉拆卸
}
private void mystoryboard2_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard3.Begin();////左翼拆卸
}
private void mystoryboard3_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard4.Begin();//右翼拆卸
}
private void mystoryboard4_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard5.Begin();//开瓶器把手拆卸
}
private void mystoryboard5_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard0.Begin();//拆卸完成启动旋转故事板
}
//自动装配
private void button7_Click(object sender,
System.Windows.RoutedEventArgs e)
{
Reset();
this.mystoryboard6.Begin();//开瓶器把手装配
}
private void mystoryboard6_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard7.Begin();//右翼装配
}
private void mystoryboard7_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard8.Begin();//左翼装配
}
private void mystoryboard8_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard9.Begin();//右翼螺钉装配
}
private void mystoryboard9_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard10.Begin();//左翼螺钉装配
}
private void mystoryboard10_Completed(object sender,
System.EventArgs e)
{
this.mystoryboard0.Begin();//装配完成启动故事板
}
}