just another .obj parser.
disclaimer
there are at least 3 bugs and 27 typos in the code.
it’s ment to be a starting point, not a super finished tool. it works tho
supports faces, quads, normals and texture-coords 1/2/3D. no smoothing groups, lines, materials etc. (tex-coords not tested by me)
here’s a simple .obj file:
# a geodesic sphere
#
v 0.0000000000 0.0000000000 10.0000000000
v 8.9442720413 0.0000000000 4.4721360207
v 2.7639317513 8.5065088272 4.4721360207
v -7.2360687256 5.2573103905 4.4721360207
v -7.2360677719 -5.2573122978 4.4721360207
v 2.7639331818 -8.5065078735 4.4721360207
v 7.2360677719 -5.2573118210 -4.4721360207
v 7.2360682487 5.2573113441 -4.4721360207
v -2.7639324665 8.5065078735 -4.4721360207
v -8.9442720413 -0.0000007819 -4.4721360207
v -2.7639288902 -8.5065088272 -4.4721360207
v 0.0000000000 0.0000000000 -10.0000000000
# 12 vertices
vt 0.5999999642 1.0000000000 10.0000000000
vt 0.5000000000 0.6475836635 10.0000000000
vt 0.6999999285 0.6475835443 10.0000009537
vt 0.7999999523 1.0000000000 10.0000000000
vt 0.9000000358 0.6475836635 10.0000000000
vt 0.0000000373 1.0000000000 10.0000000000
vt -0.0999999642 0.6475836635 10.0000000000
vt 0.1000000387 0.6475836635 10.0000000000
vt 0.2000000179 1.0000000000 10.0000000000
vt 0.3000000119 0.6475836635 10.0000000000
vt 0.4000000060 1.0000000000 10.0000000000
vt 0.4000000060 0.3524163961 10.0000000000
vt 0.5999999642 0.3524163961 10.0000000000
vt 0.8000000119 0.3524163961 10.0000000000
vt -0.1999999881 0.3524163961 10.0000000000
vt 0.0000000379 0.3524163961 10.0000000000
vt 0.2000000477 0.3524163961 10.0000000000
vt 0.6999999881 0.0000000000 10.0000000000
vt -0.0999999717 0.0000000000 10.0000000000
vt 0.1000000462 0.0000000000 10.0000000000
vt 0.3000000119 0.0000000000 10.0000000000
vt 0.5000000000 0.0000000000 10.0000000000
# 22 texture coords
g sphere
f 1/1 2/2 3/3
f 1/4 3/3 4/5
f 1/6 4/7 5/8
f 1/9 5/8 6/10
f 1/11 6/10 2/2
f 2/2 7/12 8/13
f 3/3 8/13 9/14
f 4/7 9/15 10/16
f 5/8 10/16 11/17
f 6/10 11/17 7/12
f 8/13 3/3 2/2
f 9/14 4/5 3/3
f 10/16 5/8 4/7
f 11/17 6/10 5/8
f 7/12 2/2 6/10
f 12/18 9/14 8/13
f 12/19 10/16 9/15
f 12/20 11/17 10/16
f 12/21 7/12 11/17
f 12/22 8/13 7/12
(save it to some.obj)
looks like this …
since it’s abstract, usage would be at least :
obj = new Simple[your_implementation]ObjFile();
obj.load("meshes/some.obj");
a minimum no-op adapter could look like :
public class SimpleOBJFileAdapter extends SimpleOBJFile
{
// vectors
public void vertex(float x, float y, float z) {}
public void normal(float x, float y, float z) {}
// these three should be indexed into the same list.
public void textureCoord(float s) {}
public void textureCoord(float x, float y){}
public void textureCoord(float x, float y, float z) {}
// indicies
// these 3 functions are grouped.
// if tex-coords or normals exists for the face,
// they're called first - followd by the actual face.
public void face(int v_a, int v_b, int v_c) {}
public void faceTextureCoords(int vt_a, int vt_b, int vt_c) {}
public void faceNormal(int vn_a, int vn_b, int vn_c) {}
public void quad(int v_a, int v_b, int v_c, int v_d) {}
public void quadTextureCoords(int vt_a, int vt_b, int vt_c, int vt_d) { }
public void quadNormal(int vn_a, int vn_b, int vn_c, int vn_d) {}
//
public void comment(String comment) {}
}
the abstract parser. whatever should happen to the data is up to the user :
package net.highteq.lws.core;
//~--- JDK imports ------------------------------------------------------------
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* spec as described here : http://www.martinreddy.net/gfx/3d/OBJ.spec.
*
* ment to be a starting point.
*
* NOT implementing all features, missing smoothing groups, lines, materials and more.
*
* @version 1.0, 2015.01.17
* @author bas
*/
@SuppressWarnings ( { "UseOfSystemOutOrSystemErr","DesignForExtension" } )
public abstract class SimpleOBJFile
{
//~--- static fields --------------------------------------------------------
private final static Pattern slashPattern = Pattern.compile("/");
private final static Pattern spacePattern = Pattern.compile(" ");
//~--- fields ---------------------------------------------------------------
private boolean flipYZ;
//~--- constructors ---------------------------------------------------------
/**
*/
public SimpleOBJFile()
{
this(false);
}
/**
* @param flipVertexYZ boolean - change if the object is rotated by 90 deg.
*/
public SimpleOBJFile(boolean flipVertexYZ)
{
this.flipYZ = flipVertexYZ;
}
//~--- methods --------------------------------------------------------------
// indices
/**
* @param x float
* @param y float
* @param z float
*/
public abstract void vertex(float x,float y,float z);
/**
* 1D texture coords
*
* @param s float
*/
public abstract void textureCoord(float s);
/**
* 2D texture coords
*
* @param x float
* @param y float
*/
public abstract void textureCoord(float x,float y);
/**
* 3D texture coords
*
* @param x float
* @param y float
* @param z float
*/
public abstract void textureCoord(float x,float y,float z);
/**
* @param x float
* @param y float
* @param z float
*/
public abstract void normal(float x,float y,float z);
// faces
/**
* plain face without normals or texture-coords
*
* @param v_a int
* @param v_b int
* @param v_c int
*/
public abstract void face(int v_a,int v_b,int v_c);
/**
* @param vt_a int
* @param vt_b int
* @param vt_c int
*/
public abstract void faceTextureCoords(int vt_a,int vt_b,int vt_c);
/**
* @param vn_a int
* @param vn_b int
* @param vn_c int
*/
public abstract void faceNormal(int vn_a,int vn_b,int vn_c);
//
// quads
//
/**
* @param v_a int
* @param v_b int
* @param v_c int
* @param v_d int
*/
public abstract void quad(int v_a,int v_b,int v_c,int v_d);
/**
* @param vt_a int
* @param vt_b int
* @param vt_c int
* @param vt_d int
*/
public abstract void quadTextureCoords(int vt_a,int vt_b,int vt_c,int vt_d);
/**
* @param vn_a int
* @param vn_b int
* @param vn_c int
* @param vn_d int
*/
public abstract void quadNormal(int vn_a,int vn_b,int vn_c,int vn_d);
/**
* @param comment String
*/
public abstract void comment(String comment);
/**
* change these to match your logging tools. here it's just a STD out.
*
* @param ex Throwable
* @param text String
*/
@SuppressWarnings ( "MethodMayBeStatic" )
protected void handleInfo(Throwable ex,String text)
{
if(text != null) System.out.println(text);
if(ex != null) ex.printStackTrace(System.out);
}
/**
* @param ex Throwable
* @param text String
*/
@SuppressWarnings ( "MethodMayBeStatic" )
protected void handleError(Throwable ex,String text)
{
if(text != null) System.err.println(text);
if(ex != null) ex.printStackTrace(System.err);
}
/**
* @param chr char
* @param line String
*/
private void unhandled(char chr,String line)
{
handleInfo(null,"unhandled type : " + chr + " line : " + line);
}
/**
* @param file String
*/
public final void load(String file)
{
try
{
load(Files.newInputStream(new File(file).toPath()));
}
catch(final Exception ex)
{
handleError(ex,"file " + file + " not loaded.");
}
}
/**
* @param from InputStream
*/
public final void load(InputStream from)
{
try (BufferedReader reader = new BufferedReader(new InputStreamReader(from,"utf-8")))
{
String line;
final float[] floats3 = new float[3];
final int[] v = new int[4];
final int[] vt = new int[4];
final int[] vn = new int[4];
int lineNo = 0;
while(( line = reader.readLine() ) != null)
{
++lineNo;
try
{
if(!line.isEmpty())
{
final char chr = line.charAt(0);
switch(chr)
{
case '#' : // comment
{
final int idx = line.length() > 1 ? Character.isWhitespace(line.charAt(1)) ? 2 : 1 : 1;
final String comment = line.substring(idx);
if(!comment.isEmpty()) comment(comment);
break;
}
case 'g' : // group, ignored
{
break;
}
case 'v' : // vertex
{
final char t = line.charAt(1);
if(Character.isWhitespace(t))
{
final String[] s = splitSpace(line);
final float a = Float.parseFloat(s[s.length - 3]);
final float b = Float.parseFloat(s[s.length - 2]);
final float c = Float.parseFloat(s[s.length - 1]);
if(flipYZ) vertex(a,-c,b);
else vertex(a,b,c);
}
else if(t == 'n') // vn normal
{
final int vertexDimensions = parseFloats(line,floats3);
if(vertexDimensions != 3)
handleError(new IllegalArgumentException(),"normal vector is off : num : " + vertexDimensions + " values : " + Arrays.toString(floats3) + " line :" + lineNo);
else normal(floats3[0],floats3[1],floats3[2]);
}
else if(t == 't') // vt, texture coords
{
final int numTextureCoords = parseFloats(line,floats3);
switch(numTextureCoords)
{
case 1 :
textureCoord(floats3[0]);
break;
case 2 :
textureCoord(floats3[0],floats3[1]);
break;
case 3 :
textureCoord(floats3[0],floats3[1],floats3[2]);
break;
default :
handleError(new IllegalArgumentException(),"texture coords are off : num : " + numTextureCoords + " values : " + Arrays.toString(floats3) + " line :" + lineNo);
}
}
else
{
handleInfo(new IllegalArgumentException(),"unhandled vertex data : 2nd char : " + t + " line : " + line);
}
break;
}
case 'f' : // face
{
final String[] s = splitSpace(line);
final boolean isQuad = s.length > 4; // some exporters support 4-vertex-faces.
boolean hasVT = false;
boolean hasVN = false;
final int numIndices = isQuad ? 4 : 3;
for(int i = 0; i < numIndices; i++)
{
final String[] e = splitSlash(s[i + 1]);
v[i] = Integer.parseInt(e[0]) - 1;
if(e.length > 1 && !e[1].isEmpty())
{
hasVT = true;
vt[i] = Integer.parseInt(e[1]) - 1;
}
if(e.length > 2 && !e[2].isEmpty())
{
hasVN = true;
vn[i] = Integer.parseInt(e[2]) - 1;
}
}
if(isQuad)
{
if(hasVT) quadTextureCoords(vt[0],vt[1],vt[2],vt[3]);
if(hasVN) quadNormal(vn[0],vn[1],vn[2],vn[3]);
quad(v[0],v[1],v[2],v[3]);
}
else
{
if(hasVT) faceTextureCoords(vt[0],vt[1],vt[2]);
if(hasVN) faceNormal(vn[0],vn[1],vn[2]);
face(v[0],v[1],v[2]);
}
break;
}
case 's' : // smoothing group, ignored.
{
break;
}
case 'u' : // usemtl, .. anything starting with u, ignored.
{
break;
}
default :
unhandled(chr,line); // pass out something to debug.
break;
}
}
}
catch(final Exception ex)
{
handleError(ex,"line : " + line);
break; // parsing errors. uncomment this if you want to continue even with errors.
}
}
}
catch(final Exception ex)
{
handleError(ex,null); // IO errors.
}
}
/**
* @param input CharSequence
* @return String[]
*/
private static String[] splitSlash(CharSequence input)
{
return slashPattern.split(input,0);
}
/**
* @param input CharSequence
* @return String[]
*/
private static String[] splitSpace(CharSequence input)
{
return spacePattern.split(input,0);
}
/**
* @param line String
* @param out float[]
* @return int
*/
@SuppressWarnings ( "ValueOfIncrementOrDecrementUsed" )
private static int parseFloats(CharSequence line,float[] out)
{
final String[] p = splitSpace(line);
int idx = 0;
for(int i = 1; i < p.length; i++)
{
if(!p[i].isEmpty()) out[idx++] = Float.parseFloat(p[i]);
}
return idx;
}
}
…