自从上一次出版到现在已经有一段时间了。但是其间,三次重要的会议让我远离了博客。我对自己说,针对我说过的题目,我必须写博客,但是我根本没有机会。

现在,离JavaOne发布还比较遥远,而且我已完成我的新书 《JavaFX8,Introduction by Example》,该书与我的朋友Carl Dea, Gerrit Grunwald, Mark Heckler and Sean Phillips共同编写完成,很快就会印刷出版。我有机会使用Java 8和JavaFX 3D几个星期,这篇文章就是我探索的结果。

很偶然我的孩子最近在家摆弄魔方,为了感谢源自David Gilday的这个令人惊讶的项目,我们建立一个乐高头脑风暴EV3处理机器人,David Gilday在CubeStormer3中最快复原了魔方,EV3见下图。

javafx例子(RubikFX:用JavaFX3D解决魔方难题)(1)

图一

在我玩魔方一段时间后,我在考虑创建JavaFX应用程序解决魔方难题的可行性,以及RubikFX如何诞生的问题。如果你急切地想了解RubikFX是怎么一回事,以下YouTube上的视频将向你展示大部分内容。https://www.youtube.com/watch?v=ZVPIBkDgZV4

总体上讲,在这篇文章中我将讨论在一个Scene中导入3D模型,使用放大、旋转、缩放的功能,加入光线,移动摄像头等等。一旦我们有一个非常好的3D魔方模型,我们将努力找到一种独立于模型剩下的部分方式,移动模型层块,保留变化的轨迹。最后,我们将加入通过鼠标点击,选择面、转动层块。

如果你不熟悉魔方的概念和复原它的基本步骤,请阅读以下内容。

在我们开始以前

你也许知道Java8已经在3月18日发布了,所以本项目的代码是基于这个版本的。如果你还没有Java8,请下载新的SDK更新你的系统。我采用NetBeans8.0开发本项目,它支持lambdas表达式和新的Stream API。你可以从以下链接更新你的IDE。地址:https://netbeans.org/downloads/

我使用了2个独立的构件,一个用来导入模型,它是OpenJFX项目的一部分,来源于一个实验性的项目3DViewer。我们需要下载和编译它。另一个来源于ControlsFX项目,它可以为应用程序添加酷炫的对话框。

下载地址:http://fxexperience.com/downloads/controlsfx-8.0.5.zip

最后,我们的项目需要一个3D模型,你可以自己建立一个或者使用一个免费的,你可以从下面的地址下载,采用3ds或者OBJ的格式下载。

地址如下:

http://tf3dm.com/3d-model/rubik39s-Cube-79189.html

(原作者提供的下载地址,据我尝试,该文件不能下载了。但是完整版的资源文件中,已经有这个3D模型了)

一旦你得到所有的组件,你可以很容易地得到这张图片。

用3DViewer程序打开3D模型

javafx例子(RubikFX:用JavaFX3D解决魔方难题)(2)

图二

解压文件,将‘Rubik's Cube.mtl'重命名为‘Cube.mtl',将'Rubik's Cube.obj'重命名为'Cube.obj',编辑该文件将第三行改为‘mtllib Cube.mtl',(用文本编辑器打开)然后保存。运行3DViewer应用程序,将'Cube.obj'拖到viewer中。打开设置面板,选择灯光,打开白色环境光源,关掉puntual(好像是拼错了)光源。你可以放大或者缩小(使用鼠标滚轮、右击、导航条)。旋转魔方用左键(编辑旋转速度用CTRL或者SHIFT),或者用鼠标按键全部按下。

选择Options面板,点击Wireframe,你可以看到建造模型的三角面块。

javafx例子(RubikFX:用JavaFX3D解决魔方难题)(3)

图三

27个立方体中的每一个都在OBJ文件中给予了一个名称,类似‘Block46’等。所有的三角面块都被集合在一起,且赋予了材质,每个立方体由1到6个面块组成,名字类似‘Block46', ‘Block46 (2)',总共有117个面块。

The color of each cubie meshes is asigned in the 'Cube.mtl' file with the Kd constant relative to the diffuse color.

每个面块的颜色都包含在'Cube.mtl'文件中,漫反射颜色也包含其中。

魔方—精简版

输入3D模型

当我们了解了我们的模型是怎么回事,我们就需要为面块(MeshView)建造节点了(Node)了。3DViewer中的ObjImporter类提供了getMeshes()方法,返回每个面块的名字。我们定义一个HashMap为每个MeshView绑定一个名字,针对每个面块的名字,我们用bulidMeshView(s)方法得到一个MeshView对象。

在设计时,模型中立方体的材质并不反射光线,我们修改它,允许它和Puntual光线交互,通过定义它为Phongmaterial类,编辑材质的特殊粒子属性。

最后,我们将旋转初始魔方,让白色面朝上,蓝色面朝前。

public class Model3D { /* Cube.obj contains 117 meshes, marked as "Block46",...,"Block72 (6)" in this set: Cube.obj包含117个mesh面,标识为“Block46”、“Block72 (6)” */ private Set<String> meshes; /* HashMap to store a MeshView of each mesh with its key */ /*哈希表用键值对存储MeshView*/ private final Map<String,Meshview> mapMeshes=new HashMap<>(); public void importObj(){ try {// cube.obj ObjImporter reader = new ObjImporter(getClass().getResource("Cube.obj").toExternalForm()); meshes=reader.getMeshes(); // set with the names of 117 meshes //得到117mesh面 Affine affineIni=new Affine(); affineIni.prepend(new Rotate(-90, Rotate.X_AXIS)); affineIni.prepend(new Rotate(90, Rotate.Z_AXIS)); meshes.stream().forEach(s-> { MeshView cubiePart = reader.buildMeshView(s); // every part of the cubie is transformed with both rotations: //魔方的每个部分都要一起变化 cubiePart.getTransforms().add(affineIni); // since the model has Ns=0 it doesn't reflect light, so we change it to 1 //当NS=0,它不反射光线,我们将它置为1 PhongMaterial material = (PhongMaterial) cubiePart.getMaterial(); material.setSpecularPower(1); cubiePart.setMaterial(material); // finally, add the name of the part and the cubie part to the hashMap: //最后,将魔方每一部份的名字加入hashMap中。 mapMeshes.put(s,cubiePart); }); } catch (IOException e) { System.out.println("Error loading model " e.toString()); } } public Map<String, MeshView> getMapMeshes() { return mapMeshes; } }

模型导入后自身是白色面朝右(X轴)红色面朝前(Z轴),这就要求2次旋转。

第一次绕X轴旋转-90度,把蓝色面置前,然后绕Z轴旋转90度把白色面置顶。

(魔方的初始状态是白色面朝上,蓝色面朝前)

数学计算上(矩阵运算),第二次绕Z轴旋转的矩阵必须在左边乘以第一次绕X轴旋转的矩阵。但是按照如下情况,如果我们使用add或者append方法在右侧操作,将产生错误。

链接地址:http://hg.openjdk.java.net/openjfx/8/master/rt/file/f89b7dc932af/modules/graphics/src/main/java/javafx/scene/transform/Affine.java

cubiePart.getTransforms().addAll(new Rotate(-90, Rotate.X_AXIS),new Rotate(90, Rotate.Z_AXIS)); cubiePart.getTransforms().addAll(new Rotate(90, Rotate.Z_AXIS),new Rotate(-90, Rotate.X_AXIS));

如果我们先在Z轴旋转,然后在X轴旋转,把红色面置顶黄色面置前,也会产生错误。

尽管进行右侧旋转,将需要次数更多地旋转,使魔方恢复到初始状态,这也比旋转脱离最后状态更加复杂。

所以prepend是正确可行的方法,我们需要采取把最后一次旋转的矩阵记录到Affine矩阵中,而且Affine矩阵中记录所有之前的旋转状态。

,