Coffee Space


Listen:

JavaFX - Import STL and OBJ

Preview Image

Introduction

So, I want to create an open source FPS game for multiple platforms (mainly Linux), so decided to use Java. Looking at light weight game engines that just offer rendering, I finally decided on JavaFX as an extension of the javax functionality. I started this project with a simple mission, to be able to create 3D models with the following process:

OpenSCAD -> STL -> Blender -> OBJ + Diffuse Map

Initially I tried to go from STL -> Blender -> STL + Diffuse Map, but this didn’t work for reasons we will discuss later.

Hopefully this small write-up will save someone out there some serious time in the future and not put people off what should be one of the easier tasks in designing and building 3D applications in Java.

Problem

JavaFX does not import mesh models.

The first (and majority) suggestion is to use Interactive Mesh, but this is closed source freeware and there’s no way in hell this would fly with the majority of open source folk. It’s some old random binary that runs unknown code on people’s machines natively - not so awesome.

The other option is to use some game engine like JMonkey, but it’s way too big (and prone to crashing, incompatibility between versions, etc). Whilst being open source, it’s simply too big and feature complete for what I’m aiming to achieve, which is a low-requirements FPS written in Java with minimal dependencies.

The other problem with using a game engine is that the end goal is to have some relatively unique - almost server-less game system so that there is little dependency on any one particular server. If it’s going to stand the test of time (10 years plus) then it should be designed to be adaptable. It doesn’t feel as if these bloated game engines will stand the test of time without some serious continuous maintenance.

Loader

Import STL

STL File Structure

A good start point for writing an STL loader is Wikipedia, where you get the following definition:

0001 solid name
0002   facet normal ni nj nk
0003     outer loop
0004       vertex v1x v1y v1z
0005       vertex v2x v2y v2z
0006       vertex v3x v3y v3z
0007     endloop
0008   endfacet
0009   [..]
0010 endsolid name

Pretty simple (hence why it’s so attractive for 3D printers), just three vertexes (3D points) all joined together. The facet normal ni nj nk simply tells the render which side is the outside and which side is the inside.

Code

Behold, the hacks:

0011 public static MeshView loadStl(String path){

Firstly, we ignore any potential user of the code that there may be some problems in loading an STL file:

0012   warning("loadStl", "Bad method used to load `" + path + "`");
0013   TriangleMesh mesh = new TriangleMesh();

User a custom method to load the raw text file as an ArrayList<String> to parse line by line:

0014   ArrayList<String> lines = readTextFile(path);
0015   int faceCnt = 0;
0016   for(int x = 0; x < lines.size(); x++){

Make sure that we have a line with some interesting data on it:

0017     String line = lines.get(x);
0018     if(!(
0019       line == null ||
0020       line.indexOf("solid") >= 0 ||
0021       line.indexOf("outer") >= 0 ||
0022       line.indexOf("end") >= 0
0023     )){

Process a face’s normal values (very hacked - will discuss at the end):

0024       if(line.indexOf("facet") >= 0){
0025         String[] normals = line.replaceFirst("facet normal", "").trim().split(" ");
0026         for(int y = 0; y < 3; y++){
0027           for(String n : normals){
0028             /* TODO: This does not and *cannot* work correctly. */
0029             int facets = (int)Math.sqrt((lines.size() - 2) / 7);
0030             mesh.getTexCoords().addAll(((Float.parseFloat(n) + 1) / -2));
0031           }
0032         }

Process the vertex:

0033       }else{
0034         int target = x + 3;
0035         for(; x < target; x++){
0036           line = lines.get(x);
0037           String[] points = line.replaceFirst("vertex", "").trim().split(" ");
0038           for(int y = 0; y < points.length; y++){
0039             mesh.getPoints().addAll(Float.parseFloat(points[y]));
0040           }
0041         }

And finally, set the face indexes (a massive hack):

0042         mesh.getFaces().addAll(faceCnt, faceCnt, faceCnt + 1, faceCnt + 1, faceCnt + 2, faceCnt + 2);
0043         faceCnt += 3;
0044       }
0045     }
0046   }
0047   return new MeshView(mesh);
0048 }
Problems

The known problems with this code are as follows:

  • Assumes that vertexes are three in size, technically no more than this are supported in an STL file - but potentially there’s no reason why more shouldn’t be supported. A more future proof parser would at least support quad faces.
  • Cannot handle coloured STL files, this would likely break the parser if colour values were found.
  • Does not support the binary format of the STL file, which is probably the most preferable version for most use cases.
  • The STL file format has zero support for mapping a texture.
  • Setting the faces indexes is a meaningless hack in order to get JavaFX to read the object correctly.

Import OBJ

OBJ File Structure

Again, I recommend Wikipedia for understanding this, where to defines an OBJ file as:

0049 # this is a comment
0050 ...
0051 # List of geometric vertices, with (x,y,z[,w]) coordinates, w is optional and defaults to 1.0.
0052 v 0.123 0.234 0.345 1.0
0053 v ...
0054 ...
0055 # List of texture coordinates, in (u, v [,w]) coordinates, these will vary between 0 and 1, w is optional and defaults to 0.
0056 vt 0.500 1 [0]
0057 vt ...
0058 ...
0059 # List of vertex normals in (x,y,z) form; normals might not be unit vectors.
0060 vn 0.707 0.000 0.707
0061 vn ...
0062 ...
0063 # Parameter space vertices in ( u [,v] [,w] ) form; free form geometry statement ( see below )
0064 vp 0.310000 3.210000 2.100000
0065 vp ...
0066 ...
0067 # Polygonal face element (see below)
0068 f 1 2 3
0069 f 3/1 4/2 5/3
0070 f 6/4/1 3/5/3 7/6/5
0071 f 7//1 8//2 9//3
0072 f ...
0073 ...
0074 # Line element (see below)
0075 l 5 8 1 2 4 9

Over than being simpler in terms of description language, rather importantly we now get information about texture coordinates (vt) that we didn’t previously have. These allow us to apply a diffuse map to our mesh!

Code

Again, a quick overview of the hacks:

0076 public static MeshView loadObj(String path){
0077   TriangleMesh mesh = new TriangleMesh(VertexFormat.POINT_NORMAL_TEXCOORD);
0078   ArrayList<String> lines = readTextFile(path);
0079   for(int x = 0; x < lines.size(); x++){
0080     String line = lines.get(x);
0081     if(line != null){
0082       line = line.trim();

Make sure that the line has at least two characters in it, otherwise potential second character test of line could fail:

0083       if(line.length() < 2){
0084         warning("loadObj", "Not enough data to parse line " + x);
0085         continue;
0086       }
0087       switch(line.charAt(0)){

Ignore any comment lines:

0088         /* Comment */
0089         case '#' :
0090           /* Ignore comments */
0091           break;

Get face data, with Java requiring the values in the wrong order being an important aspect:

0092         /* Polygonal face element */
0093         case 'f' :
0094           String[] faces = line.replace("f", "").trim().split(" ");
0095           for(int y = 0; y < faces.length; y++){
0096             String[] temp = faces[y].split("/");
0097             /* NOTE: Java loads this in the wrong order. */
0098             mesh.getFaces().addAll(Integer.parseInt(temp[0]) - 1);
0099             mesh.getFaces().addAll(Integer.parseInt(temp[2]) - 1);
0100             mesh.getFaces().addAll(Integer.parseInt(temp[1]) - 1);
0101           }
0102           break;

Ignore grouping (unimplemented):

0103         /* Group */
0104         case 'g' :
0105           warning("loadObj", "Cannot handle group on line " + x);
0106           break;

Ignore lines (unimplemented):

0107         /* Line element */
0108         case 'l' :
0109           warning("loadObj", "Cannot handle line on line " + x);
0110           break;

Ignore objects (unimplemented):

0111         /* Object */
0112         case 'o' :
0113           warning("loadObj", "Cannot handle object on line " + x);
0114           break;

Ignore smoothing (unimplemented):

0115         /* Smoothing */
0116         case 's' :
0117           warning("loadObj", "Cannot handle smoothing on line " + x);
0118           break;
0119         case 'v' :
0120           switch(line.charAt(1)){

Get the basic vertices from the file:

0121             /* List of geometric vertices, with (x,y,z[,w]) coordinates */
0122             case ' ' :
0123               String[] verts = line.replace("v", "").trim().split(" ");
0124               for(int y = 0; y < verts.length; y++){
0125                 mesh.getPoints().addAll(Float.parseFloat(verts[y]));
0126               }
0127               break;

Get the texture coordinates from the file:

0128             /* List of texture coordinates, in (u, v [,w]) coordinates */
0129             case 't' :
0130               String[] texts = line.replace("vt", "").trim().split(" ");
0131               for(int y = 0; y < texts.length; y++){
0132                 mesh.getTexCoords().addAll(Float.parseFloat(texts[y]));
0133               }
0134               break;

Get the normals from the file:

0135             /* List of vertex normals in (x,y,z) form */
0136             case 'n' :
0137               String[] norms = line.replace("vn", "").trim().split(" ");
0138               for(int y = 0; y < norms.length; y++){
0139                 mesh.getNormals().addAll(Float.parseFloat(norms[y]));
0140               }
0141               break;

Ignore parameter space (unimplemented):

0142             /* Parameter space vertices in ( u [,v] [,w] ) form */
0143             case 'p' :
0144               warning("loadObj", "Cannot handle vertices on line " + x);
0145               break;
0146             default :
0147               warning("loadObj", "Bad vertex `" + line.charAt(1) + "`:" + x);
0148               break;
0149           }
0150           break;
0151         default :
0152           warning("loadObj", "Bad command `" + line.charAt(0) + "`:" + x);
0153           break;
0154       }
0155     }
0156   }
0157   return new MeshView(mesh);
0158 }
Problems

Still we have some problems:

  • Unable to handle quads or greater than three vertices.
  • Many unsupported features of OBJ files.

Open Source

So, this work has been open source on GitHub with the hope that someone will find this useful and help improve upon the current limitations.

Some tasks that should be completed:

Anybody who wants to help take up on this work is welcome!