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.
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.
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.
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 }
The known problems with this code are as follows:
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!
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 }
Still we have some problems:
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!