Coffee Space


Listen:

JavaFX - Import STL and OBJ

Preview Image

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:

solid name
  facet normal ni nj nk
    outer loop
      vertex v1x v1y v1z
      vertex v2x v2y v2z
      vertex v3x v3y v3z
    endloop
  endfacet
  [..]
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:

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:

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

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

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

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

    String line = lines.get(x);
    if(!(
      line == null ||
      line.indexOf("solid") >= 0 ||
      line.indexOf("outer") >= 0 ||
      line.indexOf("end") >= 0
    )){

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

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

Process the vertex:

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

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

        mesh.getFaces().addAll(faceCnt, faceCnt, faceCnt + 1, faceCnt + 1, faceCnt + 2, faceCnt + 2);
        faceCnt += 3;
      }
    }
  }
  return new MeshView(mesh);
}
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:

# this is a comment
...
# List of geometric vertices, with (x,y,z[,w]) coordinates, w is optional and defaults to 1.0.
v 0.123 0.234 0.345 1.0
v ...
...
# List of texture coordinates, in (u, v [,w]) coordinates, these will vary between 0 and 1, w is optional and defaults to 0.
vt 0.500 1 [0]
vt ...
...
# List of vertex normals in (x,y,z) form; normals might not be unit vectors.
vn 0.707 0.000 0.707
vn ...
...
# Parameter space vertices in ( u [,v] [,w] ) form; free form geometry statement ( see below )
vp 0.310000 3.210000 2.100000
vp ...
...
# Polygonal face element (see below)
f 1 2 3
f 3/1 4/2 5/3
f 6/4/1 3/5/3 7/6/5
f 7//1 8//2 9//3
f ...
...
# Line element (see below)
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:

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

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

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

Ignore any comment lines:

        /* Comment */
        case '#' :
          /* Ignore comments */
          break;

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

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

Ignore grouping (unimplemented):

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

Ignore lines (unimplemented):

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

Ignore objects (unimplemented):

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

Ignore smoothing (unimplemented):

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

Get the basic vertices from the file:

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

Get the texture coordinates from the file:

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

Get the normals from the file:

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

Ignore parameter space (unimplemented):

            /* Parameter space vertices in ( u [,v] [,w] ) form */
            case 'p' :
              warning("loadObj", "Cannot handle vertices on line " + x);
              break;
            default :
              warning("loadObj", "Bad vertex `" + line.charAt(1) + "`:" + x);
              break;
          }
          break;
        default :
          warning("loadObj", "Bad command `" + line.charAt(0) + "`:" + x);
          break;
      }
    }
  }
  return new MeshView(mesh);
}
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!