保持Mathematica中3D模型的原始风貌

昨晚万达电影院9点场3厅5排7座的那个哥们儿,我的闺蜜看上你了。虽然你身边有女友,但是闺蜜说没有撬不动的墙角。希望有认识的人转告这哥们儿一下。对了,他的联系方式在本文最末尾。点开那个红色的“赏”字,微信扫一扫。帮帮忙,救救我那花痴的姐妹~

问题来源

x3d格式是比较主流的一个3d数据格式。当我们用Mathematica绘制三维数据图/曲线时,我们可以把它转化成x3d格式的文件,并结合x3dom来在网页上进行展示。然而,我们想象中的图形是这样儿的:

Figure 1: 想象中的3D图形
Figure 1: 想象中的3D图形

可实际上,你却会看到这个模模糊糊的鬼东西:

Figure 2: 实际上你会得到这个鬼玩意儿
Figure 2: 实际上你会得到这个鬼玩意儿

之所以会这样儿,是因为直接由Mathematica导出的x3d数据文件,丢失了背景颜色、纹理、材料颜色等信息。因此,当把这样的3d图形嵌入到网页中时,是无法原汁原味地呈现它们在Mathematica中的样式的。

这真是一个五雷轰顶的噩耗——毕竟,Mathematica就是以它那非常Fancy的图形配色见长。如果不能把Mathematica的图形原汁原味地保留下来,还不如让我买块豆腐把自己撞死算了。幸运的是, ͡° ͜ʖ ͡°)✧,Mathematica的数据结构是保留了这些图像信息的,因此我们可以通过编程的方法,将这些图像信息“翻译”成x3d可以理解的图像信息。

解决方法

首先我们剖析一下,x3d的数据格式究竟长什么样儿?

X3D的数据格式

翻阅x3d的官方网站,可以构造出嵌入x3d信息的html模板应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html >
<html>
<head>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
<title>X3DOM from MMA</title>
<script src='http://www.x3dom.org/release/x3dom.js'></script>
<link rel='stylesheet' href='http://www.x3dom.org/release/x3dom.css'>
</head>

<body style='background: #eee'>
<x3d style='background:white; margin-left:50px' width='600px' height='370px'>
<Scene>
<Viewpoint position='6.43873 -9.76285 2.77042' orientation='0.91920 0.28510 0.27164 1.40000'> </Viewpoint>
<Shape>
<appearance>
<material ambientIntensity='0.5' diffuseColor='0.880 0.611 0.142' specularColor='1 1 1'> </material>
</appearance>
<indexedFaceSet solid='false' coordIndex=$FACE_INDEX_STRING$> <Normal vector=$NORMAL$></Normal>
<Color color=$COLOR$></Color>
<coordinate point=$FACE_STRING$></coordinate>
</indexedFaceSet>
</Shape>
<Shape>
<IndexedLineSet coordIndex=$LINE_INDEX_STRING$>
<coordinate point=$LINE_STRING$></coordinate>
</IndexedLineSet>
</Shape>
</Scene>
</x3d>
</body>
</html>

该文档的重点是图形几何信息的定义部分。<x3d></scene>...</scene></x3d>中,嵌套了两个<Shape></Shape>标签。而这两个<shape></shape>完成了对数据的定义。为了称呼的方便,我称前一个<Shape></Shape>标签为TagShape1,后一个为TagShape2。TagShape1中的 <indexedFaceSet></indexedFaceSet>标签定义了“面”;TagShape2则通过<indexedLineSet></indexedLineSet>定义了“线”。

TagShape1和TagShape2结构比较像,都是由"coordIndex='...'-<Coordinate point='...'>-<Color color='...'>"的结构来定义3d的形状和颜色。因此,我们只需要从Mathematica中提取出相应的数据,并对$FACE_INDEX _STRING$等变量进行替换,即可实现从Mathematica的3D图形到标准x3d文件的转换。

值得一提的是,以下的两段代码在Mathematica中是没有的:

1
2
3
4
5
6
7
<Viewpoint position = ' 6.43873 - 11.76285 4.77042' orientation = ' 0.91920 0.28510 0.27164 1.40000' > </Viewpoint>

... ... ... ....

<appearance>
<material ambientIntensity = ' 0.5' diffuseColor = ' 0.880 0.611 0.142' specularColor = ' 1 1 1'> </material>
</appearance>

这两段代码是由作者手工定义的。<Viewpoint>用于对3D图形的默认视角进行定义;<appearance>则用于对材质、光线、纹理等进行定义(否则显示出来的x3d图形只能看出来是一团颜色,而看不出形状,就像Fig. 2中的那样儿,<(`^′)> )。具体的参数可以手工进行调整。大家知道有这回事儿就行了,此时不是我们研究的重点。

Mathematica的数据格式

WReach编写了一个工具1,可以很方便的查看Mathematica中的数据的组成。利用这个工具,可以发现,Mathematica中一个3D图形的组成(参见Fig. 3),主要由蓝色方块的坐标点、蓝色椭圆标出的坐标的索引、黄色高亮的几何微元的RGB颜色定义VertexColor和法线方向定义VertexNormals四部分组成。

Figure 3: Mathematica中3d图形的数据结构
Figure 3: Mathematica中3d图形的数据结构

实际上,这几部分已经足以重构x3d文件了。其方法就是,从Mathematica的数据结构中,将这些数据提取出来,然后在x3d的模板中对$Var$等变量进行替换。

转换方法

千言万语都比不上代码言简意赅。二话不说先甩一把代码:

解构Mathematica图形信息

1
2
3
4
5
6
7
8
9
(* 构造3D图形 *)
pic = Plot3D[Sin[x^2+y^2]/Sqrt[x^2+y^2],{x,y}\[Element]Disk[{0,0},Pi],Mesh->None,ColorFunction->"Rainbow"]

(* 从Mathematica生成的图形中提取数据信息 *)
pts = First@Cases[pic, GraphicsComplex[pts_, ___] :> pts, Infinity];
faces = Cases[pic, _Polygon, Infinity];
lines = Cases[pic, _Line, Infinity];
{vc} = Cases[pic, Rule[VertexColors, vc_List] :> vc, Infinity];
{vn} = Cases[pic, Rule[VertexNormals, vn_] :> vn, Infinity];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(* 将提取的数据按照x3d的格式进行"格式化" *)

(* 提取坐标点 *)
formatted =
Map[ToString[NumberForm[#, {8, 4}, ExponentFunction -> (Null &)]] &, Chop[pts], {2}];
coordString =
StringJoin @@ Join[{"'"}, Riffle[Flatten[Riffle[formatted, ","]], " "], {"'"}];

(* 提取“面”和“线”组成坐标的索引 *)
faceIndexString =
StringJoin @@ Join[{"'"}, ToString /@ Riffle[Flatten[ Riffle[Map[# - 1 &, Apply[Join, First /@ faces], {2}], -1]], " "], {"'"}];
lineIndexString =
StringJoin @@ Join[{"'"}, ToString /@ Riffle[Flatten[Riffle[Map[# - 1 &, First /@ mesh, {2}], -1]], " "], {"'"}];

(* 提取颜色、法线方向 *)
formattedNormal =
Map[ToString[NumberForm[#, {8, 4}, ExponentFunction -> (Null &)]] &, Chop[vn], {2}];
normalString =
StringJoin @@ Join[{"'"}, Riffle[Flatten[Riffle[formattedNormal, ","]], " "], {"'"}];

formattedColor = Map[ToString[NumberForm[#, {8, 4}, ExponentFunction -> (Null &)]] &, Chop[vc], {2}];
colorString = StringJoin @@ Join[{"'"}, Riffle[Flatten[Riffle[formattedColor, ","]], " "], {"'"}];

填充x3d模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(* 定义模板 *)
template="<!DOCTYPE html >
<html>
<head>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8' />
<title>X3DOM from MMA</title>
<script src='http://www.x3dom.org/release/x3dom.js'></script>
<link rel='stylesheet' href='http://www.x3dom.org/release/x3dom.css'>
</head>

<body style='background: #eee'>
<x3d style='background:white' width='598px' height='368px'>
<Scene>
<Viewpoint position='6.43873 -9.76285 2.77042' orientation='0.91920 0.28510 0.27164 1.40000'>
</Viewpoint>
<Shape>
<appearance>
<material
ambientIntensity='0.5'
diffuseColor='0.880 0.611 0.142'
specularColor='1 1 1'>
</material>
</appearance>
<indexedFaceSet solid='false'
coordIndex=$FACE_INDEX _STRING$>
<Normal vector=$NORMAL$></Normal>
<Color color=$COLOR$></Color>
<coordinate point=$FACE_STRING$></coordinate>
</indexedFaceSet>
</Shape>
<Shape>
<IndexedLineSet coordIndex=$LINE_INDEX _STRING$>
<coordinate point=$LINE_STRING$></coordinate>
</IndexedLineSet>
</Shape>
</Scene>
</x3d>
</body>
</html>
";
1
2
3
4
5
6
7
8
9
(* 利用模板生成包含3d图形的网页 *)
fileString =
StringReplace[
template, {"$LINE_STRING$" -> coordString,
"$FACE_STRING$" -> coordString,
"$LINE_INDEX_STRING$" -> lineIndexString,
"$FACE_INDEX_STRING$" -> faceIndexString,
"$NORMAL$" -> normalString, "$COLOR$" -> colorString}];
Export["X3DScene.html", fileString, "Text"]

Ok,大功告成。看一看效果如何?就是下面这个:

说实话,我还是更喜欢我一开始不小心画错的图。就是下面这个:

难道你不喜欢第二个么?它这么魔性~ (╯▼╰)/

后记

这个小工具目前能够把Mathematica的3D模型导出为自定义样式的x3d图形。但是远远谈不上完美。事实上,我是想用这个小工具导出Mathematica生成的3D数据图(而不是3D图形)。这样的话,就需要把3D数据图的axesboxLegend等也导出来——不过,这是下一步的工作啦。<( ̄) ̄)>

Mathematica这个科学计算软件的高效、便捷、功能强悍不得不赞一下。要知道,我们的这个小程序 仅仅 使用了Mathematica的“模式匹配”的功能。 丘吉尔曾经说过:

Tools rebuild us
Tools rebuild us

对此,我真是感同身受: 岂止是building反过来塑造我们,我们使用的“工具”也在反过来塑造我们。 当制造一个工具的时候,我们把自己的知识、理念、美学观点以各种方式融入这个工具。当这个工具造成之时,它的灵魂就是设计者的精神气质,它的使用方法则贯穿着设计者的行为风格。当这把工具被广泛使用的时候,一个用心的使用者是能够从工具的使用方式中感觉到设计者的思维方式、做事风格 —— 你使用工具的过程就是和设计者交谈的过程。在天长日久的过程中,设计者的“魂”不知不觉地潜移默化地影响了使用者——“随风潜入夜,润物细无声”。

所以,选一个美的、有灵魂的工具吧——它不仅是你的效率工具,它还是你的影子导师,你的图腾。 ლↀѡↀლ

Am I Right ?

致谢 这个x3d转换程序深受Mark McClure的启发。谢谢你,歪果仁儿!~~~^_^~~~


  1. 参见 Interactively inspecting parts of an object .

Donate comment here
Show comments from Gitment