在Java 3D中载入外部3D模型文件

Java 3D虽然能支持众多的外部3D模型文件,但能支持被Java 3D使用的外部模型文件仅为.obj和.lwd两种;分别对应ObjectFile类和Lw3dLoader类。相比之下几款主流的3D建模软件都能生成.obj格式的文件,因此本文主要介绍使用ObjectFile类载入.obj文件的方法。

 

ObjectFile类有三个构造方法,分别为:
ObjectFile()
ObjectFile(int flags)
ObjectFile(int flags, float radians)
其中flags为一个整型的常量参数,用于决定载入的3D模型以什么方式生成。
参数radians用于决定载入模型的可显示半径。

 

flags参数可在以下四个值之间任取一个或者用逻辑或("|")将几个参数组合使用。
ObjectFile.RESIZE:忽略被载入的模型大小,直接把载入的模型放在一个范围在(1,1,1)到(-1,-1,-1)之间的立方体空间内,并把坐标原点设为(0,0,0)。
ObjectFile.REVERSE:反转载入的外部模型,即可能看模型的后面。
ObjectFile.TRIANGULATE:将模型的面以三角形方式显示,此参数主要便于观察模型凹凸面。
ObjectFile.STRIPIFY:以模型文件内模型的实际情况显示,此参数也是默认参数。

 

当初始化了ObjectFile对象后就可以用load方法载入.obj格式的文件,如果模型中已包含了贴图和光照的话也将一起被载入。load方法需要一个参数用于指出.obj格式文件所在的路径,load方法有多个重载方法可以使参数即可以接受String类的值也可以接受Url类的值或者从输入法流读入,如果模型载入成功将返回一个Scene类的对象,如果载入失败将抛异常。Java 3D针对load方法定义了三个异常类:
FileNotFoundException类:表示文件未找到。
IncorrectFormatException类:表示文件格式不正确。
ParsingErrorException类:装载器解析文件时出错。

 

下面的代码用以演示如何用load方法将一个模型载入到Scene类的实例:
Scene loadScene = null; int flag = ObjectFile.STRIPIFY; ObjectFile obj = new ObjectFile(flag); try { loadScene = obj.load(this.getClass().getClassLoader().getResource(filename)); } catch (FileNotFoundException e) { System.out.println("文件未找到或文件路径不正确"); e.printStackTrace(); } catch (IncorrectFormatException e) { System.out.println("文件格式不正确"); e.printStackTrace(); } catch (ParsingErrorException e) { System.out.println("装载器解析文件时出错"); e.printStackTrace(); }

虽然至此我们已经载入了一个.obj格式文件的3D模型,但把它载入到场景后我们却模型并没有按我们想像的那么显示。对比在3D建模工具中看到模型的样子,我们的模型被绕X轴逆时针的旋转了90度,这主要是Java 3D的坐标系和大多数的3D建模工具的坐标系不同。我们假设用户的显示器是垂直于桌面上,那么在Java 3D中显示器的宽代表X轴,显示器的高代表Y轴,显示器垂直朝向用户的方法为Z轴(此方向也是Z轴的正数方向)。而多数的建模工具使用的是世界坐标系,即将显示器的高代表Z轴。

 

因此我们需要在程序将模型绕X轴顺时针旋转90度,旋转轴坐标的方法是使用Transform3D类的rotX方法,相应的还有rotY和rotZ方法。
Transform3D t3d = new Transform3D(); t3d.rotX(-Math.PI/2); TransformGroup tg = new TransformGroup(t3d); tg.addChild(loadScene.getSceneGroup);

注意:这里有一个容易混淆的概念,就是我们刚才的步骤是旋转的坐标系,而不是模型,模型是附加在坐标系的上,没有法被旋转。而在刚才的步骤完成后就是将Z轴转向了上方(即显示器的高),而此时场中如还有其它的模型的话,它们的坐标未受影响,仍是Java 3D的坐标系。

通常情况下我们载入的模型大小并不是我们所要的,我们必须要在场景中对模型进行缩放操作。Java 3D中对模型进行缩放需要用到Transform3D的setScale方法,方法可以接收一个double值或一个Vector3d对象的实例,当使用double值做参数时模型将在XYZ轴上使用同样的比例因子进行缩放,而Vector3d实例则可以分别为XYZ轴指定不同的比例因子,比例因子越接近0,模型就越小,当设为0时模型即小的不可见了。
t3d.setScale(0.05d);

t3d.setScale(new Vector3d(0.01d,0,02d,0.03d));

 

好了,现在我将代码整理如下:

Gamemain.java 程序主入口

ScreenManager.java 窗口框架类

LoadModelDemo.java 演示载入一个外部3D模型文件

ColourTile.java 实现一个平面用于地面中的单块地砖

CheckedFloor.java 实现场景中的地面

 

GameMain.java

import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; public class GameMain { private static int scrWidth = 800; private static int scrHeight = 600; private static int scrBitdepth = 32; private JFrame gameFrame; private JPanel gamePanel; public static void main(String[] args) { GameMain game = new GameMain(); } public GameMain() { ScreenManager screen = new ScreenManager(scrWidth,scrHeight,scrBitdepth,"Java 3D Test"); screen.setWindowMode(); gameFrame = screen.getFrame(); gamePanel = new LoadModelDemo(scrWidth,scrHeight); gameFrame.add(gamePanel); } }

 

ScreenManager.java

import java.awt.Dimension; import java.awt.DisplayMode; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.JFrame; import javax.swing.JOptionPane; public class ScreenManager { private GraphicsDevice device; private JFrame frame; private String title; private boolean isResizable; private boolean isWindowMode; private int scrWidth; private int scrHeight; private int scrBitdepth; public ScreenManager(int scrWidth,int scrHeight,int scrBitdepth,String title) { this.scrWidth = scrWidth; this.scrHeight = scrHeight; this.scrBitdepth = scrBitdepth; this.title = title; } public ScreenManager(String title) { this.title = title; this.frame.setTitle(title); } public void setFullScreenMode() { device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); if(isSupportDisplayMode(scrWidth,scrHeight,scrBitdepth)) { frame = new JFrame(); frame.setUndecorated(true); frame.setResizable(false); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); frame.setVisible(true); device.setFullScreenWindow(frame); try{ if(device.isFullScreenSupported()&&device.isDisplayChangeSupported()) device.setDisplayMode(new DisplayMode(scrWidth, scrHeight, scrBitdepth, DisplayMode.REFRESH_RATE_UNKNOWN)); }catch(IllegalArgumentException e) { e.printStackTrace(); System.exit(0); } } else { JOptionPane.showMessageDialog(null, "不支持的显示分辨率!","错误",JOptionPane.ERROR_MESSAGE); System.exit(0); } } private boolean isSupportDisplayMode(int width,int height,int bitdepth) { DisplayMode[] modes = device.getDisplayModes(); for(DisplayMode mode : modes) { if(mode.getWidth()==width && mode.getHeight()==height && mode.getBitDepth()==bitdepth) return true; } return false; } public void setWindowMode() { frame = new JFrame(); frame.setResizable(false);//禁止窗体改变大小 frame.setPreferredSize(new Dimension(scrWidth,scrHeight)); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);//响应窗体的关闭事件,但不关闭窗体 frame.setVisible(true); // 侦听窗体事件并捕获窗体关闭中的事件,在用户确认后退出程序 frame.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent e) { int res = JOptionPane.showConfirmDialog(null, "是否退出!","退出",JOptionPane.YES_NO_OPTION); if(res == JOptionPane.YES_OPTION) closeFrame(); } }); this.setFrametoCenter(); } public void setFullWindowMode() { if(frame != null) { device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); DisplayMode displayMode = device.getDisplayMode(); frame.setPreferredSize(new Dimension(displayMode.getWidth(),displayMode.getHeight())); } } public int getWidth() { return scrWidth; } public int getHeight() { return scrHeight; } public JFrame getFrame() { return frame; } // 将窗体在显示屏幕内居中显示 public void setFrametoCenter() { if(device!=null) return; Insets inset = frame.getInsets(); int scrx=0; int scry=0; Dimension scrSize = Toolkit.getDefaultToolkit().getScreenSize(); if(scrSize.width > scrWidth) scrx = (scrSize.width-scrWidth)/2; if(scrSize.height > scrHeight) scry = (scrSize.height-scrHeight)/2; frame.setBounds(scrx-inset.left, scry-inset.top, scrWidth+inset.right+inset.left, scrHeight+inset.bottom+inset.top); } // 关闭窗体事件 public void closeFrame() { frame.dispose(); System.exit(0); } }

 

LoadModelDemo.java

import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.GraphicsConfiguration; import java.io.FileNotFoundException; import java.util.Enumeration; import javax.media.j3d.Background; import javax.media.j3d.BoundingBox; import javax.media.j3d.BoundingSphere; import javax.media.j3d.BranchGroup; import javax.media.j3d.Canvas3D; import javax.media.j3d.Transform3D; import javax.media.j3d.TransformGroup; import javax.swing.JPanel; import javax.vecmath.Color3f; import javax.vecmath.Point3d; import javax.vecmath.Vector3d; import javax.vecmath.Vector3f; import com.sun.j3d.loaders.IncorrectFormatException; import com.sun.j3d.loaders.ParsingErrorException; import com.sun.j3d.loaders.Scene; import com.sun.j3d.loaders.objectfile.ObjectFile; import com.sun.j3d.utils.behaviors.vp.OrbitBehavior; import com.sun.j3d.utils.geometry.Text2D; import com.sun.j3d.utils.universe.SimpleUniverse; import com.sun.j3d.utils.universe.ViewingPlatform; public class LoadModelDemo extends JPanel{ private BranchGroup sceneBG; private SimpleUniverse universe; private BoundingSphere bounds; private double boundRadius = 100; public LoadModelDemo(int width,int height) { this.setLayout(new BorderLayout()); GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); Canvas3D canvas = new Canvas3D(config); canvas.setSize(width, height); this.add(canvas,BorderLayout.CENTER); universe = new SimpleUniverse(canvas); createSceneGroup(); initUserPosition(); orbitControls(canvas); universe.addBranchGraph(sceneBG); } public void createSceneGroup() { sceneBG = new BranchGroup(); bounds = new BoundingSphere(new Point3d(0,0,0),boundRadius); addBackground(); TransformGroup[] objModel = new TransformGroup[4]; sceneBG.addChild(objModel[0] = loadModel("leet/Liit.obj",ObjectFile.RESIZE,new Vector3d(-5.4,0,0))); sceneBG.addChild(objModel[1] = loadModel("leet/Liit.obj",ObjectFile.REVERSE,new Vector3d(-1.8,0,0))); sceneBG.addChild(objModel[2] = loadModel("leet/Liit.obj",ObjectFile.TRIANGULATE,new Vector3d(1.8,0,0))); sceneBG.addChild(objModel[3] = loadModel("leet/Liit.obj",ObjectFile.STRIPIFY,new Vector3d(5.4,0,0))); sceneBG.addChild(new CheckerFloor().getBG()); //对sceneBG有关的对象进行编译和缓存,如果在编译之后再次添加其它分支的话将抛异常 sceneBG.compile(); } public void addBackground() { Background back = new Background(); back.setApplicationBounds(bounds); back.setColor(0.17f, 0.62f, 0.92f); sceneBG.addChild(back); } private void initUserPosition() { //返回当前虚拟世界的观察平台 ViewingPlatform vp = universe.getViewingPlatform(); //得到观察平台的坐标枝花点 TransformGroup steerTG = vp.getViewPlatformTransform(); Transform3D t3d = new Transform3D(); steerTG.getTransform(t3d); //设置观察点坐标在(0,5,20),看向坐标(0,0,0)处,指定Y轴向正数沿伸方向为正方向 t3d.lookAt(new Point3d(0,5,20), new Point3d(0,0,0), new Vector3d(0,1,0)); t3d.invert(); steerTG.setTransform(t3d); } private void orbitControls(Canvas3D canvas) { OrbitBehavior orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL); orbit.setSchedulingBounds(bounds); ViewingPlatform vp = universe.getViewingPlatform( ); vp.setViewPlatformBehavior(orbit); } private TransformGroup loadModel(String filename,int flag,Vector3d translation) { Scene loadScene = this.loadFromFile(filename,flag); Transform3D t3d = new Transform3D(); t3d = this.rotateModel(); t3d.setScale(this.calcScaleFactor(loadScene.getSceneGroup())); t3d.setTranslation(translation); TransformGroup tg = new TransformGroup(t3d); tg.addChild(loadScene.getSceneGroup()); return tg; } private Scene loadFromFile(String filename,int flag) { Scene loadScene = null; ObjectFile obj = new ObjectFile(flag); try { loadScene = obj.load(this.getClass().getClassLoader().getResource(filename)); } catch (FileNotFoundException e) { // TODO Auto-generated catch block System.out.println("文件未找到或文件路径不正确"); e.printStackTrace(); } catch (IncorrectFormatException e) { // TODO Auto-generated catch block System.out.println("文件格式不正确"); e.printStackTrace(); } catch (ParsingErrorException e) { // TODO Auto-generated catch block System.out.println("装载器解析文件时出错"); e.printStackTrace(); } finally { return loadScene; } } //obj格式的文件坐标空间是Z轴指向上方,Y轴指向屏幕。而Java 3D //的坐标空间是Y轴指向上方,Z轴指向屏幕。因此需要把导入的外部模型 //作一个坐标转换,即将X轴转动90度 private Transform3D rotateModel() { Transform3D t3d = new Transform3D(); t3d.rotX(-Math.PI/2); return t3d; } private Vector3d calcScaleFactor(BranchGroup bg) { //根据枝节点得到一个立方位形的范围空间 BoundingBox boundBox = new BoundingBox(bg.getBounds()); Point3d upper = new Point3d(); //范围空间的XYZ轴在正方向的最大值载一个Point3d对象 boundBox.getUpper(upper); //范围空间的XYZ轴在负方向的最大值载一个Point3d对象 Point3d lower = new Point3d(); boundBox.getLower(lower); double max = 0.0; //物体的长宽高三个数值中找出最大的一个值 if((upper.getX()-lower.getX())>max) max = upper.getX()-lower.getX(); if((upper.getY()-lower.getY())>max) max = upper.getY()-lower.getY(); if((upper.getZ()-lower.getZ())>max) max = upper.getZ()-lower.getZ(); //计算比例因子,当值越接近0时,所显示的物体越小,当等于0时物体即不可见 double scaleFactor = 5/max; if(scaleFactor<0.005) scaleFactor = 0.005; return new Vector3d(scaleFactor,scaleFactor*2,scaleFactor); } }

 

ColouredTile.java

import java.util.ArrayList; import javax.media.j3d.Appearance; import javax.media.j3d.BranchGroup; import javax.media.j3d.GeometryArray; import javax.media.j3d.PolygonAttributes; import javax.media.j3d.QuadArray; import javax.media.j3d.Shape3D; import javax.vecmath.Color3f; import javax.vecmath.Point3d; import javax.vecmath.Point3f; public class ColouredTile extends Shape3D { private ArrayList<Point3d> coord; private Color3f color; private QuadArray plane; public ColouredTile(ArrayList<Point3d> coord,Color3f color) { this.coord = coord; this.color = color; plane = new QuadArray(coord.size(),GeometryArray.COORDINATES | GeometryArray.COLOR_3); createGeometry(); createAppearance(); } public void createGeometry() { int numPoints = coord.size(); Point3d[] points = new Point3d[numPoints]; coord.toArray(points); plane.setCoordinates(0, points); Color3f[] colors = new Color3f[numPoints]; for(int i=0;i<numPoints;i++) colors[i] = this.color; plane.setColors(0, colors); this.setGeometry(plane); } public void createAppearance() { Appearance app = new Appearance(); PolygonAttributes pa = new PolygonAttributes(); pa.setCullFace(PolygonAttributes.CULL_NONE); app.setPolygonAttributes(pa); this.setAppearance(app); } }

 

CheckedFloor.java

import java.awt.Font; import java.util.ArrayList; import javax.media.j3d.BranchGroup; import javax.media.j3d.Transform3D; import javax.media.j3d.TransformGroup; import javax.vecmath.Color3f; import javax.vecmath.Point3d; import javax.vecmath.Vector3f; import com.sun.j3d.utils.geometry.Text2D; public class CheckerFloor { private BranchGroup floor; public CheckerFloor() { floor = new BranchGroup(); } public BranchGroup getBG() { Color3f blue = new Color3f(0, 0, 1); Color3f green = new Color3f(0, 1, 0); boolean isBlue = true; for (int j = -9; j < 10; j++) { for (int i = -9; i < 10; i++) { Point3d t1 = new Point3d(i, 0, j); Point3d t2 = new Point3d(i + 1, 0, j); Point3d t3 = new Point3d(i + 1, 0, j + 1); Point3d t4 = new Point3d(i, 0, j + 1); ArrayList<Point3d> tileCoord = new ArrayList<Point3d>(); tileCoord.add(t1); tileCoord.add(t2); tileCoord.add(t3); tileCoord.add(t4); if (isBlue) floor.addChild(new ColouredTile(tileCoord, blue)); else floor.addChild(new ColouredTile(tileCoord, green)); isBlue = !isBlue; floor.addChild(makeText(new Vector3f(i,0,j),"("+i+","+j+")")); } } return floor; } public TransformGroup makeText(Vector3f pt, String text) { Color3f white = new Color3f(1, 1, 1); Text2D message = new Text2D(text, white, "SansSerif", 36, Font.BOLD); TransformGroup tg = new TransformGroup(); Transform3D t3d = new Transform3D(); t3d.setTranslation(pt); tg.setTransform(t3d); tg.addChild(message); return tg; } }

以上程序皆来本人研究国外书籍中的代码而来,故有些说法会有不正确的地方还请指教

 

你可能感兴趣的:(在Java 3D中载入外部3D模型文件)