/*
* $RCS ObjectFile.java,v $
*
* Copyright (c) 2007 Sun Microsystems, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are
*
* - Redistribution of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistribution in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* Neither the name of Sun Microsystems, Inc. or the names of
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any
* kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
* WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
* EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
* NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
* USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
* DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
* ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
* CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
* REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
* INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGES.
*
* You acknowledge that this software is not designed, licensed or
* intended for use in the design, construction, operation or
* maintenance of any nuclear facility.
*
* $Revi 1.5 $
* $ 2007/02/020:10 $
* $S Exp $
*/
//package com.sun.j3d.loaders.objectfile;
import com.sun.j3d.loaders.Scene;
import com.sun.j3d.loaders.SceneBase;
import com.sun.j3d.loaders.Loader;
import com.sun.j3d.loaders.IncorrectFormatException;
import com.sun.j3d.loaders.ParsingErrorException;
//import com.sun.j3d.loaders.objectfile.ObjectFileParser;
//import com.sun.j3d.loaders.objectfile.ObjectFileMaterials;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.geometry.Stripifier;
import java.io.FileNotFoundException;
import java.io.StreamTokenizer;
import java.io.Reader;
import java.io.BufferedReader;
import java.io.BufferedInputStream;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import java.util.StringTokenizer;
import javax.media.j3d.*;
import javax.vecmath.Color3f;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;
import javax.vecmath.TexCoord2f;
import java.net.MalformedURLException;
/**
* The ObjectFile class implements the Loader interface for the Wavefront
* .obj file format, a standard 3D object file format created for use with
* Wavefront's Advanced Visualizer (tm) and available for purchase from
* Viewpoint DataLabs, as well as other 3D model companies. Object Files
* are text based
* files supporting both polygonal and free-form geometry (curves
* and surfaces). The Java 3D .obj file loader supports a subset of the
* file format, but it is enough to load almost all commonly available
* Object Files. Free-form geometry is not supported.
*
* The Object File tokens currently supported by this loader
* v float float float
* - A single vertex's geometric position in space. The first vertex
* listed in the file has index 1,
* and subsequent vertices are numbered sequentially.
* vn float float float
* - A normal. The first normal in the file is index 1, and
* subsequent normals are numbered sequentially.
* vt float float
* - A texture coordinate. The first texture coordinate in the file is
* index 1, and subsequent normals are numbered sequentially.
* f int int int . . .
* - or
* f int/int int/int int/int . . .
* - or
* f int/int/int int/int/int int/int/int . . .
* - A polygonal face. The numbers are indexes into the arrays of
* vertex positions, texture coordinates, and normals respectively.
* There is no maximum number of vertices that a single polygon may
* contain. The .obj file specification says that each face must
* be flat and convex, but if the TRIANGULATE flag is sent to the
* ObjectFile constructor, each face will be triangulated by the
* Java 3D Triangulator, and therefore may be concave.
* A number may be omitted if, for example, texture coordinates are
* not being defined in the model. Numbers are normally positive
* indexes, but may also be negative. An index of -1 means the last
* member added to the respective array, -2 is the one before that,
* and so on.
* g name
* - Faces defined after this token will be added to the named group.
* These geometry groups are returned as separated Shape3D objects
* attached to the parent SceneGroup. Each named Shape3D will also
* be in the Hashtable returned by Scene.getNamedObjects(). It is
* legal to add faces to a group, switch to another group, and then
* add more faces to the original group by reissuing the same name
* with the g token. If faces are added to the model before the g
* token is seen, the faces are put into the default group called
* "default."
* s int
* - or
* s off
* - If the vn token is not used in the file to specify vertex normals
* for the model, this token may be used to put faces into groups
* for normal calculation ("smoothing groups") in the same manner as
* the 'g' token
* is used to group faces geometrically. Faces in the same smoothing
* group will have their normals calculated as if they are part of
* the same smooth surface. To do this, we use the Java 3D NormalGenerator
* utility with the creaseAngle parameter set to PI (180 degrees -
* smooth shading, no creases) or to whatever the user has set the
* creaseAngle. Faces in group 0 or 'off' use a
* creaseAngle of zero, meaning there is no smoothing (the normal
* of the face is used at all vertices giving the surface a faceted
* look; there will be a
* crease, or "Hard Edge," between each face in group zero). There is
* also an implied hard edge between each smoothing group, where they
* meet each other.
*
* If neither the vn nor the s token is used in the file, then normals
* are calculated using the creaseAngle set in the contructor.
* Normals are calculated on each geometry
* group separately, meaning there will be a hard edge between each
* geometry group.
*
* usemtl name
* - The current (and subsequent) geometry groups (specified with
* the 'g' token) have applied
* to them the named material property. The following set of material
* properties are available by def
*
* amber amber_trans aqua aqua_filter
* archwhite archwhite2 bflesh black
* blondhair blue_pure bluegrey bluetint
* blugrn blutan bluteal bone
* bone1 bone2 brass brnhair
* bronze brown brownlips brownskn
* brzskin chappie charcoal deepgreen
* default dkblue dkblue_pure dkbrown
* dkdkgrey dkgreen dkgrey dkorange
* dkpurple dkred dkteal emerald
* fgreen flaqua flblack flblonde
* flblue_pure flbrown fldkblue_pure fldkdkgrey
* fldkgreen fldkgreen2 fldkgrey fldkolivegreen
* fldkpurple fldkred flesh fleshtransparent
* flgrey fllime flltbrown flltgrey
* flltolivegreen flmintgreen flmustard florange
* flpinegreen flpurple flred fltan
* flwhite flwhite1 flyellow glass
* glassblutint glasstransparent gold green
* greenskn grey hair iris
* jetflame lavendar lcdgreen lighttan
* lighttan2 lighttan3 lighttannew lightyellow
* lime lips ltbrown ltgrey
* meh metal mintgrn muscle
* navy_blue offwhite.cool offwhite.warm olivegreen
* orange pale_green pale_pink pale_yellow
* peach periwinkle pink pinktan
* plasma purple red redbrick
* redbrown redorange redwood rubber
* ruby sand_stone sapphire shadow
* ship2 silver skin sky_blue
* smoked_glass tan taupe teeth
* violet white yellow yellow_green
* yellowbrt yelloworng
*
* mtllib filename
* - Load material properties from the named file. Materials
* with the same name as the predefined materials above will override
* the default value. Any directory path information in (filename)
* is ignored. The .mtl files are assumed to be in the same directory
* as the .obj file. If they are in a different directory, use
* Loader.setBasePath() (or Loader.setBaseUrl() ). The format of the
* material properties files
* are as fol
*
newmtl name
* - Start the definition of a new named material property.
* Ka float float float
* - Ambient color.
* Kd float float float
* - Diffuse color.
* Ks float float float
* - Specular color.
* illum (0, 1, or 2)
* - 0 to disable lighting, 1 for ambient & diffuse only (specular
* color set to black), 2 for full lighting.
* Ns float
* - Shininess (clamped to 1.0 - 128.0).
* map_Kd filename
* - Texture map. Supports .rgb, .rgba, .int, .inta, .sgi, and
* .bw files in addition to those supported by
* TextureLoader.
*
*/
public class ObjectFile implements Loader
{
// 0=Input file assumed good
// 1=Input file checked for inconsistencies
// 2=path names
// 4=flags
// 8=Timing Info
// 16=Tokens
// 32=Token details (use with 16)
// 64=limits of model coordinates
private static final int DEBUG = 32;
/**
* Flag sent to constructor. The object's vertices will be changed
* so that the object is centered at (0,0,0) and the coordinate
* positions are all in the range of (-1,-1,-1) to (1,1,1).
*/
public static final int RESIZE = LOAD_SOUND_NODES << 1;
/**
* Flag sent to constructor. The Shape3D object will be created
* by using the GeometryInfo POLYGON_ARRAY primitive, causing
* them to be Triangulated by GeometryInfo. Use
* this if you suspect concave or other non-behaving polygons
* in your model.
*/
public static final int TRIANGULATE = RESIZE << 1;
/**
* Flag sent to constructor. Use if the vertices in your .obj
* file were specified with clockwise winding (Java 3D wants
* counter-clockwise) so you see the back of the polygons and
* not the front. Calls GeometryInfo.reverse().
*/
public static final int REVERSE = TRIANGULATE << 1;
/**
* Flag sent to contructor. After normals are generated the data
* will be analyzed to find triangle strips. Use this if your
* hardware supports accelerated rendering of strips.
*/
public static final int STRIPIFY = REVERSE << 1;
private static final char BACKSLASH = '\\';
private int flags;
private String basePath = null;
private URL baseUrl = null;
private boolean fromUrl = false;
private float radians;
// First, lists of points are read from the .obj file into these arrays. . .
private ArrayList coordList; // Holds Point3f
private ArrayList texList; // Holds TexCoord2f
private ArrayList normList; // Holds Vector3f
// . . . and index lists are read into these arrays.
private ArrayList coordIdxList; // Holds Integer index into coordList
private ArrayList texIdxList; // Holds Integer index into texList
private ArrayList normIdxList; // Holds Integer index into normList
// The length of each face is stored in this array.
private ArrayList stripCounts; // Holds Integer
// Each face's Geometry Group membership is kept here. . .
private HashMap groups; // key=Integer index into stripCounts
// value=String name of group
private String curGroup;
// . . . and Smoothing Group membership is kept here
private HashMap sGroups; // key=Integer index into stripCounts
// value=String name of group
private String curSgroup;
// The name of each group's "usemtl" material property is kept here
private HashMap groupMaterials; // key=String name of Group
// value=String name of material
// After reading the entire file, the faces are converted into triangles.
// The Geometry Group information is converted into these structures. . .
private HashMap triGroups; // key=String name of group
// value=ArrayList of Integer
// indices into coordIdxList
private ArrayList curTriGroup;
// . . . and Smoothing Group info is converted into these.
private HashMap triSgroups; // key=String name of group
// value=ArrayList of Integer
// indices into coordIdxList
private ArrayList curTriSgroup;
// Finally, coordList, texList, and normList are converted to arrays for
// use with GeometryInfo
private Point3f coordArray[] = null;
private Vector3f normArray[] = null;
private TexCoord2f texArray[] = null;
// Used for debugging
private long time;
private ObjectFileMaterials materials = null;
void readVertex(ObjectFileParser st) throws ParsingErrorException
{
Point3f p = new Point3f();
st.getNumber();
p.x = (float) st.nval;
st.getNumber();
p.y = (float) st.nval;
st.getNumber();
p.z = (float) st.nval;
if ((DEBUG & 32) != 0)
{
// System.out.println(" (" + p.x + "," + p.y + "," + p.z + ")");
}
st.skipToNextLine();
// Add this vertex to the array
coordList.add(p);
} // End of readVertex
/**
* readNormal
*/
void readNormal(ObjectFileParser st) throws ParsingErrorException
{
Vector3f p = new Vector3f();
st.getNumber();
p.x = (float) st.nval;
st.getNumber();
p.y = (float) st.nval;
st.getNumber();
p.z = (float) st.nval;
if ((DEBUG & 32) != 0)
{
// System.out.println(" (" + p.x + "," + p.y + "," + p.z + ")");
}
st.skipToNextLine();
// Add this vertex to the array
normList.add(p);
} // End of readNormal
/**
* readTexture
*/
void readTexture(ObjectFileParser st) throws ParsingErrorException
{
TexCoord2f p = new TexCoord2f();
st.getNumber();
p.x = (float) st.nval;
st.getNumber();
p.y = (float) st.nval;
if ((DEBUG & 32) != 0)
{
// System.out.println(" (" + p.x + "," + p.y + ")");
}
st.skipToNextLine();
// Add this vertex to the array
texList.add(p);
} // End of readTexture
/**
* readFace
*
* Adds the indices of the current face to the arrays.
*
* ViewPoint files can have up to three ar Vertex Positions,
* Texture Coordinates, and Vertex Normals. Each vertex can
* contain indices into all three arrays.
*/
void readFace(ObjectFileParser st) throws ParsingErrorException
{
int vertIndex, texIndex = 0, normIndex = 0;
int count = 0;
// There are n vertices on each line. Each vertex is comprised
// of 1-3 numbers separated by slashes ('/'). The slashes may
// be omitted if there's only one number.
st.getToken();
while (st.ttype != st.TT_EOL)
{
// First token is always a number (or EOL)
st.pushBack();
st.getNumber();
vertIndex = (int) st.nval - 1;
if (vertIndex < 0)
{
vertIndex += coordList.size() + 1;
}
coordIdxList.add(new Integer(vertIndex));
// Next token is a slash, a number, or EOL. Continue on slash
st.getToken();
if (st.ttype == '/')
{
// If there's a number after the first slash, read it
st.getToken();
if (st.ttype == st.TT_WORD)
{
// It's a number
st.pushBack();
st.getNumber();
texIndex = (int) st.nval - 1;
if (texIndex < 0)
{
texIndex += texList.size() + 1;
}
texIdxList.add(new Integer(texIndex));
st.getToken();
}
// Next token is a slash, a number, or EOL. Continue on slash
if (st.ttype == '/')
{
// There has to be a number after the 2nd slash
st.getNumber();
normIndex = (int) st.nval - 1;
if (normIndex < 0)
{
normIndex += normList.size() + 1;
}
normIdxList.add(new Integer(normIndex));
st.getToken();
}
}
if ((DEBUG & 32) != 0)
{
// System.out.println(" " + vertIndex + '/' + texIndex + '/' + normIndex);
}
count++;
}
Integer faceNum = new Integer(stripCounts.size());
stripCounts.add(new Integer(count));
// Add face to current groups
groups.put(faceNum, curGroup);
if (curSgroup != null)
{
sGroups.put(faceNum, curSgroup);
}
// In case we exited early
st.skipToNextLine();
} // End of readFace
/**
* readPartName
*/
void readPartName(ObjectFileParser st)
{
st.getToken();
// Find the Material Property of the current group
String curMat = (String) groupMaterials.get(curGroup);
// New faces will be added to the curGroup
if (st.ttype != ObjectFileParser.TT_WORD)
{
curGroup = "default";
} else
{
curGroup = st.sval;
}
if ((DEBUG & 32) != 0)
{
System.out.println(" +++Changed to group " + curGroup);
}
// See if this group has Material Properties yet
if (groupMaterials.get(curGroup) == null)
{
// It doesn't - carry over from last group
groupMaterials.put(curGroup, curMat);
}
st.skipToNextLine();
} // End of readPartName
/**
* readMaterialName
*/
void readMaterialName(ObjectFileParser st)
throws ParsingErrorException
{
st.getToken();
if (st.ttype == ObjectFileParser.TT_WORD)
{
groupMaterials.put(curGroup, new String(st.sval));
if ((DEBUG & 32) != 0)
{
System.out.println(" Material Property " + st.sval + " assigned to group " + curGroup);
}
}
st.skipToNextLine();
} // End of readMaterialName
/**
* loadMaterialFile
*
* Both types of slashes are returned as tokens from our parser,
* so we go through the line token by token and keep just the
* last token on the line. This should be the filename without
* any directory info.
*/
void loadMaterialFile(ObjectFileParser st)
throws ParsingErrorException
{
String s = null;
// Filenames are case sensitive
st.lowerCaseMode(false);
// Get name of material file (skip path)
do
{
st.getToken();
if (st.ttype == ObjectFileParser.TT_WORD)
{
s = st.sval;
}
} while (st.ttype != ObjectFileParser.TT_EOL);
materials.readMaterialFile(fromUrl, fromUrl ? baseUrl.toString() : basePath, s);
st.lowerCaseMode(true);
st.skipToNextLine();
} // End of loadMaterialFile
/**
* readSmoothingGroup
*/
void readSmoothingGroup(ObjectFileParser st)
throws ParsingErrorException
{
st.getToken();
if (st.ttype != ObjectFileParser.TT_WORD)
{
st.skipToNextLine();
return;
}
if (st.sval.equals("off"))
{
curSgroup = "0";
} else
{
curSgroup = st.sval;
}
if ((DEBUG & 32) != 0)
{
// System.out.println(" Smoothing group " + curSgroup);
}
st.skipToNextLine();
} // End of readSmoothingGroup
/**
* readFile
*
* Read the model data from the file.
*/
void readFile(ObjectFileParser st) throws ParsingErrorException
{
int t;
st.getToken();
while (st.ttype != ObjectFileParser.TT_EOF)
{
// Print out one token for each line
if ((DEBUG & 16) != 0)
{
System.out.print("Token ");
if (st.ttype == ObjectFileParser.TT_EOL)
{
System.out.println("EOL");
} else if (st.ttype == ObjectFileParser.TT_WORD)
{
System.out.println(st.sval);
} else
{
System.out.println((char) st.ttype);
}
}
if (st.ttype == ObjectFileParser.TT_WORD)
{
if (st.sval.equals("v"))
{
readVertex(st);
} else if (st.sval.equals("vn"))
{
readNormal(st);
} else if (st.sval.equals("vt"))
{
readTexture(st);
} else if (st.sval.equals("f"))
{
readFace(st);
} else if (st.sval.equals("fo"))
{ // Not sure what the dif is
readFace(st);
} else if (st.sval.equals("g"))
{
readPartName(st);
} else if (st.sval.equals("s"))
{
readSmoothingGroup(st);
} else if (st.sval.equals("p"))
{
st.skipToNextLine();
} else if (st.sval.equals("l"))
{
st.skipToNextLine();
} else if (st.sval.equals("mtllib"))
{
loadMaterialFile(st);
} else if (st.sval.equals("usemtl"))
{
if (cJME.mergeAttributes)
readPartName(st);
else
readMaterialName(st);
} else if (st.sval.equals("maplib"))
{
st.skipToNextLine();
} else if (st.sval.equals("usemap"))
{
st.skipToNextLine();
} else
{
/*throw*/ new ParsingErrorException(
"Unrecognized token: " + st.sval + ", line " + st.lineno()).printStackTrace();
}
}
st.skipToNextLine();
// Get next token
st.getToken();
}
} // End of readFile
/**
* Constructor.
*
* @param flags The constants from above or from
* com.sun.j3d.loaders.Loader, possibly "or'ed" (|) together.
* @param radians Ignored if the vn token is present in the model (user
* normals supplied). Otherwise, crease angle to use within smoothing
* groups, or within geometry groups if the s token isn't present either.
*/
public ObjectFile(int flags, float radians)
{
setFlags(flags);
this.radians = radians;
} // End of ObjectFile(int, float)
/**
* Constructor. Crease Angle set to default of
* 44 degrees (see NormalGenerator utility for details).
* @param flags The constants from above or from
* com.sun.j3d.loaders.Loader, possibly "or'ed" (|) together.
*/
public ObjectFile(int flags)
{
this(flags, -1.0f);
} // End of ObjectFile(int)
/**
* Default constructor. Crease Angle set to default of
* 44 degrees (see NormalGenerator utility for details). Flags
* set to zero (0).
*/
public ObjectFile()
{
this(0, -1.0f);
} // End of ObjectFile()
/**
* Takes a file name and sets the base path to the directory
* containing that file.
*/
private void setBasePathFromFilename(String fileName)
{
if (fileName.lastIndexOf(java.io.File.separator) == -1)
{
// No path given - current directory
setBasePath("." + java.io.File.separator);
} else
{
setBasePath(fileName.substring(0, fileName.lastIndexOf(java.io.File.separator)));
}
} // End of setBasePathFromFilename
/**
* The Object File is loaded from the .obj file specified by
* the filename.
* To attach the model to your scene, call getSceneGroup() on
* the Scene object passed back, and attach the returned
* BranchGroup to your scene graph. For an example, see
* j3d-examples/ObjLoad/ObjLoad.java.
*/
public Scene load(String filename) throws FileNotFoundException,
IncorrectFormatException, ParsingErrorException
{
if (filename.toLowerCase().endsWith(".obj"))
{
setBasePathFromFilename(filename);
Reader reader = new BufferedReader(new FileReader(filename));
return load(reader);
}
else // new 3ds loader
return new com.microcrowd.loader.java3d.max3ds.Loader3DS().load(filename);
} // End of load(String)
private void setBaseUrlFromUrl(URL url)
throws FileNotFoundException
{
String u = url.toString();
String s;
if (u.lastIndexOf('/') == -1)
{
s = url.getProtocol() + ":";
} else
{
s = u.substring(0, u.lastIndexOf('/') + 1);
}
try
{
baseUrl = new URL(s);
} catch (MalformedURLException e)
{
throw new FileNotFoundException(e.getMessage());
}
} // End of setBaseUrlFromUrl
/**
* The object file is loaded off of the web.
* To attach the model to your scene, call getSceneGroup() on
* the Scene object passed back, and attach the returned
* BranchGroup to your scene graph. For an example, see
* j3d-examples/ObjLoad/ObjLoad.java.
*/
public Scene load(URL url) throws FileNotFoundException,
IncorrectFormatException, ParsingErrorException
{
BufferedReader reader;
if (baseUrl == null)
{
setBaseUrlFromUrl(url);
}
try
{
reader = new BufferedReader(new InputStreamReader(url.openStream()));
} catch (IOException e)
{
throw new FileNotFoundException(e.getMessage());
}
fromUrl = true;
return load(reader);
} // End of load(URL)
/**
* getLimits
*
* Returns an array of Point3f which form a bounding box around the
* object. Element 0 is the low value, element 1 is the high value.
* See normalize() below for an example of how to use this method.
*/
private Point3f[] getLimits()
{
Point3f cur_vtx = new Point3f();
// Find the limits of the model
Point3f[] limit = new Point3f[2];
limit[0] = new Point3f(Float.MAX_VALUE, Float.MAX_VALUE,
Float.MAX_VALUE);
limit[1] = new Point3f(Float.MIN_VALUE, Float.MIN_VALUE,
Float.MIN_VALUE);
for (int i = 0; i < coordList.size(); i++)
{
cur_vtx = (Point3f) coordList.get(i);
// Keep track of limits for normalization
if (cur_vtx.x < limit[0].x)
{
limit[0].x = cur_vtx.x;
}
if (cur_vtx.x > limit[1].x)
{
limit[1].x = cur_vtx.x;
}
if (cur_vtx.y < limit[0].y)
{
limit[0].y = cur_vtx.y;
}
if (cur_vtx.y > limit[1].y)
{
limit[1].y = cur_vtx.y;
}
if (cur_vtx.z < limit[0].z)
{
limit[0].z = cur_vtx.z;
}
if (cur_vtx.z > limit[1].z)
{
limit[1].z = cur_vtx.z;
}
}
if ((DEBUG & 64) != 0)
{
System.out.println("Model r (" + limit[0].x + "," + limit[0].y + "," + limit[0].z + ") to (" + limit[1].x + "," + limit[1].y + "," + limit[1].z + ")");
}
return limit;
} // End of getLimits
/**
* Center the object and make it (-1,-1,-1) to (1,1,1).
*/
private void resize()
{
int i, j;
Point3f cur_vtx = new Point3f();
float biggest_dif;
Point3f[] limit = getLimits();
// Move object so it's centered on (0,0,0)
Vector3f offset = new Vector3f(-0.5f * (limit[0].x + limit[1].x), -0.5f * (limit[0].y + limit[1].y), -0.5f * (limit[0].z + limit[1].z));
if ((DEBUG & 64) != 0)
{
System.out.println("Offset am (" + offset.x + "," + offset.y + "," + offset.z + ")");
}
// Find the divide-by value for the normalization
biggest_dif = limit[1].x - limit[0].x;
if (biggest_dif < limit[1].y - limit[0].y)
{
biggest_dif = limit[1].y - limit[0].y;
}
if (biggest_dif < limit[1].z - limit[0].z)
{
biggest_dif = limit[1].z - limit[0].z;
}
biggest_dif /= 2.0f;
for (i = 0; i < coordList.size(); i++)
{
cur_vtx = (Point3f) coordList.get(i);
cur_vtx.add(cur_vtx, offset);
cur_vtx.x /= biggest_dif;
cur_vtx.y /= biggest_dif;
cur_vtx.z /= biggest_dif;
// coordList.setElementAt(cur_vtx, i);
}
} // End of resize
private int[] objectToIntArray(ArrayList inList)
{
int outList[] = new int[inList.size()];
for (int i = 0; i < inList.size(); i++)
{
outList[i] = ((Integer) inList.get(i)).intValue();
}
return outList;
} // End of objectToIntArray
private Point3f[] objectToPoint3Array(ArrayList inList)
{
Point3f outList[] = new Point3f[inList.size()];
for (int i = 0; i < inList.size(); i++)
{
outList[i] = (Point3f) inList.get(i);
}
return outList;
} // End of objectToPoint3Array
private TexCoord2f[] objectToTexCoord2Array(ArrayList inList)
{
TexCoord2f outList[] = new TexCoord2f[inList.size()];
for (int i = 0; i < inList.size(); i++)
{
outList[i] = (TexCoord2f) inList.get(i);
}
return outList;
} // End of objectToTexCoord2Array
private Vector3f[] objectToVectorArray(ArrayList inList)
{
Vector3f outList[] = new Vector3f[inList.size()];
for (int i = 0; i < inList.size(); i++)
{
outList[i] = (Vector3f) inList.get(i);
}
return outList;
} // End of objectToVectorArray
/**
* Each group is a list of indices into the model's index lists,
* indicating the starting index of each triangle in the group.
* This method converts those data structures
* into an integer array to use with GeometryInfo.
*/
private int[] groupIndices(ArrayList sourceList, ArrayList group)
{
int indices[] = new int[group.size() * 3];
for (int i = 0; i < group.size(); i++)
{
int j = ((Integer) group.get(i)).intValue();
indices[i * 3 + 0] = ((Integer) sourceList.get(j + 0)).intValue();
indices[i * 3 + 1] = ((Integer) sourceList.get(j + 1)).intValue();
indices[i * 3 + 2] = ((Integer) sourceList.get(j + 2)).intValue();
}
return indices;
} // end of groupIndices
/**
* smoothingGroupNormals
*
* Smoothing groups are groups of faces who should be grouped
* together for normal calculation purposes. The faces are
* put into a GeometryInfo object and normals are calculated
* with a 180 degree creaseAngle (no creases) or whatever the
* user has specified. The normals
* are then copied out of the GeometryInfo and back into
* ObjectFile data structures.
*/
private void smoothingGroupNormals()
{
NormalGenerator ng = new NormalGenerator(
radians == -1.0f ? Math.PI : radians);
NormalGenerator ng0 = new NormalGenerator(0.0);
normList.clear();
normIdxList = null;
int newNormIdxArray[] = new int[coordIdxList.size()];
Iterator e = triSgroups.keySet().iterator();
while (e.hasNext())
{
String curname = (String) e.next();
ArrayList triList = (ArrayList) triSgroups.get(curname);
// Check for group with no faces
if (triList.size() > 0)
{
GeometryInfo gi = new GeometryInfo(
GeometryInfo.TRIANGLE_ARRAY);
gi.setCoordinateIndices(groupIndices(coordIdxList,
triList));
gi.setCoordinates(coordArray);
if (curname.equals("0"))
{
ng0.generateNormals(gi);
} else
{
ng.generateNormals(gi);
}
// Get the generated normals and indices
Vector3f genNorms[] = gi.getNormals();
int genNormIndices[] = gi.getNormalIndices();
// Now we need to copy the generated normals into ObjectFile
// data structures (normList and normIdxList). The variable
// normIdx is the index of the index of the normal currently
// being put into the list. It takes some calculation to
// figure out the new index and where to put it.
int normIdx = 0;
// Repeat for each triangle in the smoothing group
for (int i = 0; i < triList.size(); i++)
{
// Get the coordIdxList index of the first index in this face
int idx = ((Integer) triList.get(i)).intValue();
// Repeat for each vertex in the triangle
for (int j = 0; j < 3; j++)
{
// Put the new normal's index into the index list
newNormIdxArray[idx + j] = normList.size();
// Add the vertex's normal to the normal list
normList.add(genNorms[genNormIndices[normIdx++]]);
}
}
}
}
normIdxList = new ArrayList(coordIdxList.size());
for (int i = 0; i < coordIdxList.size(); i++)
{
normIdxList.add(new Integer(newNormIdxArray[i]));
}
normArray = objectToVectorArray(normList);
} // end of smoothingGroupNormals
/**
* Each face is converted to triangles. As each face is converted,
* we look up which geometry group and smoothing group the face
* belongs to. The generated triangles are added to each of these
* groups, which are also being converted to a new triangle based format.
*
* We need to convert to triangles before normals are generated
* because of smoothing groups. The faces in a smoothing group
* are copied into a GeometryInfo to have their normals calculated,
* and then the normals are copied out of the GeometryInfo using
* GeometryInfo.getNormalIndices. As part of Normal generation,
* the geometry gets converted to Triangles. So we need to convert
* to triangles *before* Normal generation so that the normals we
* read out of the GeometryInfo match up with the vertex data
* that we sent in. If we sent in TRIANGLE_FAN data, the normal
* generator would convert it to triangles and we'd read out
* normals formatted for Triangle data. This would not match up
* with our original Fan data, so we couldn't tell which normals
* go with which vertices.
*/
private void convertToTriangles()
{
boolean triangulate = (flags & TRIANGULATE) != 0;
boolean textures = !texList.isEmpty() && !texIdxList.isEmpty() && (texIdxList.size() == coordIdxList.size());
boolean normals = !normList.isEmpty() && !normIdxList.isEmpty() && (normIdxList.size() == coordIdxList.size());
int numFaces = stripCounts.size();
boolean haveSgroups = curSgroup != null;
triGroups = new HashMap(50);
if (haveSgroups)
{
triSgroups = new HashMap(50);
}
ArrayList newCoordIdxList = null;
ArrayList newTexIdxList = null;
ArrayList newNormIdxList = null;
if (triangulate)
{
GeometryInfo gi = new GeometryInfo(
GeometryInfo.POLYGON_ARRAY);
gi.setStripCounts(objectToIntArray(stripCounts));
gi.setCoordinates(coordArray);
gi.setCoordinateIndices(objectToIntArray(coordIdxList));
if (textures)
{
gi.setTextureCoordinateParams(1, 2);
gi.setTextureCoordinates(0, texArray);
gi.setTextureCoordinateIndices(0,
objectToIntArray(texIdxList));
}
if (normals)
{
gi.setNormals(normArray);
gi.setNormalIndices(objectToIntArray(normIdxList));
}
gi.convertToIndexedTriangles();
// Data is now indexed triangles. Next step is to take the data
// out of the GeometryInfo and put into internal data structures
int coordIndicesArray[] = gi.getCoordinateIndices();
// Fix for #4366060
// Make sure triangulated geometry has the correct number of triangles
int tris = 0;
for (int i = 0; i < numFaces; i++)
{
tris += ((Integer) stripCounts.get(i)).intValue() - 2;
}
if (coordIndicesArray.length != (tris * 3))
{
// Model contains bad polygons that didn't triangulate into the
// correct number of triangles. Fall back to "simple" triangulation
triangulate = false;
} else
{
int texIndicesArray[] = gi.getTextureCoordinateIndices();
int normIndicesArray[] = gi.getNormalIndices();
// Convert index arrays to internal ArrayList format
coordIdxList.clear();
texIdxList.clear();
normIdxList.clear();
for (int i = 0; i < coordIndicesArray.length; i++)
{
coordIdxList.add(new Integer(coordIndicesArray[i]));
if (textures)
{
texIdxList.add(new Integer(texIndicesArray[i]));
}
if (normals)
{
normIdxList.add(new Integer(normIndicesArray[i]));
}
}
}
}
if (!triangulate)
{
newCoordIdxList = new ArrayList();
if (textures)
{
newTexIdxList = new ArrayList();
}
if (normals)
{
newNormIdxList = new ArrayList();
}
}
// Repeat for each face in the model - add the triangles from each
// face to the Geometry and Smoothing Groups
int baseVertex = 0;
for (int f = 0; f < numFaces; f++)
{
int faceSize = ((Integer) stripCounts.get(f)).intValue();
// Find out the name of the group to which this face belongs
Integer curFace = new Integer(f);
curGroup = (String) groups.get(curFace);
// Change to a new geometry group, create if it doesn't exist
curTriGroup = (ArrayList) triGroups.get(curGroup);
if (curTriGroup == null)
{
curTriGroup = new ArrayList();
triGroups.put(curGroup, curTriGroup);
}
// Change to a new smoothing group, create if it doesn't exist
if (haveSgroups)
{
curSgroup = (String) sGroups.get(curFace);
if (curSgroup == null)
{
// Weird case - this face has no smoothing group. Happens if the
// first 's' token comes after some faces have already been defined.
// Assume they wanted no smoothing for these faces
curSgroup = "0";
}
curTriSgroup = (ArrayList) triSgroups.get(curSgroup);
if (curTriSgroup == null)
{
curTriSgroup = new ArrayList();
triSgroups.put(curSgroup, curTriSgroup);
}
}
if (triangulate)
{
// Each polygon of n vertices is now n-2 triangles
for (int t = 0; t < faceSize - 2; t++)
{
// The groups just remember the first vertex of each triangle
Integer triBaseVertex = new Integer(baseVertex);
curTriGroup.add(triBaseVertex);
if (haveSgroups)
{
curTriSgroup.add(triBaseVertex);
}
baseVertex += 3;
}
} else
{
// Triangulate simply
for (int v = 0; v < faceSize - 2; v++)
{
// Add this triangle to the geometry group and the smoothing group
Integer triBaseVertex = new Integer(newCoordIdxList.size());
curTriGroup.add(triBaseVertex);
if (haveSgroups)
{
curTriSgroup.add(triBaseVertex);
}
newCoordIdxList.add(coordIdxList.get(baseVertex));
newCoordIdxList.add(coordIdxList.get(baseVertex + v + 1));
newCoordIdxList.add(coordIdxList.get(baseVertex + v + 2));
if (textures)
{
newTexIdxList.add(texIdxList.get(baseVertex));
newTexIdxList.add(texIdxList.get(baseVertex + v + 1));
newTexIdxList.add(texIdxList.get(baseVertex + v + 2));
}
if (normals)
{
newNormIdxList.add(normIdxList.get(baseVertex));
newNormIdxList.add(normIdxList.get(baseVertex + v + 1));
newNormIdxList.add(normIdxList.get(baseVertex + v + 2));
}
}
baseVertex += faceSize;
}
}
// No need to keep these around
stripCounts = null;
groups = null;
sGroups = null;
if (!triangulate)
{
coordIdxList = newCoordIdxList;
texIdxList = newTexIdxList;
normIdxList = newNormIdxList;
}
} // End of convertToTriangles
private SceneBase makeScene()
{
// Create Scene to pass back
SceneBase scene = new SceneBase();
BranchGroup group = new BranchGroup();
scene.setSceneGroup(group);
boolean gen_norms = normList.isEmpty() || normIdxList.isEmpty() || (normIdxList.size() != coordIdxList.size());
boolean do_tex = !texList.isEmpty() && !texIdxList.isEmpty() && (texIdxList.size() == coordIdxList.size());
// Convert ArrayLists to arrays
coordArray = objectToPoint3Array(coordList);
if (!gen_norms)
{
normArray = objectToVectorArray(normList);
}
if (do_tex)
{
texArray = objectToTexCoord2Array(texList);
}
convertToTriangles();
if ((DEBUG & 8) != 0)
{
time = System.currentTimeMillis() - time;
System.out.println("Convert to trian " + time + " ms");
time = System.currentTimeMillis();
}
if ((gen_norms) && (curSgroup != null))
{
smoothingGroupNormals();
gen_norms = false;
if ((DEBUG & 8) != 0)
{
time = System.currentTimeMillis() - time;
System.out.println("Smoothing group nor " + time + " ms");
time = System.currentTimeMillis();
}
}
NormalGenerator ng = null;
if (gen_norms)
{
ng = new NormalGenerator(radians);
}
Stripifier strippy = null;
if ((flags & STRIPIFY) != 0)
{
strippy = new Stripifier();
}
long t1 = 0, t2 = 0, t3 = 0, t4 = 0;
// Each "Group" of faces in the model will be one Shape3D
Iterator e = triGroups.keySet().iterator();
while (e.hasNext())
{
String curname = (String) e.next();
ArrayList triList = (ArrayList) triGroups.get(curname);
// Check for group with no faces
if (triList.size() > 0)
{
GeometryInfo gi = new GeometryInfo(
GeometryInfo.TRIANGLE_ARRAY);
gi.setCoordinateIndices(groupIndices(coordIdxList,
triList));
gi.setCoordinates(coordArray);
if (do_tex)
{
gi.setTextureCoordinateParams(1, 2);
gi.setTextureCoordinates(0, texArray);
gi.setTextureCoordinateIndices(0, groupIndices(
texIdxList, triList));
}
if ((DEBUG & 8) != 0)
{
time = System.currentTimeMillis();
}
if (gen_norms)
{
if ((flags & REVERSE) != 0)
{
gi.reverse();
}
ng.generateNormals(gi);
if ((DEBUG & 8) != 0)
{
t2 += System.currentTimeMillis() - time;
System.out.println("Generate nor " + t2 + " ms");
time = System.currentTimeMillis();
}
} else
{
gi.setNormalIndices(groupIndices(normIdxList,
triList));
gi.setNormals(normArray);
if ((flags & REVERSE) != 0)
{
gi.reverse();
}
}
if ((flags & STRIPIFY) != 0)
{
strippy.stripify(gi);
if ((DEBUG & 8) != 0)
{
t3 += System.currentTimeMillis() - time;
System.out.println("Stri " + t3 + " ms");
time = System.currentTimeMillis();
}
}
// Put geometry into Shape3d
Shape3D shape = new Shape3D();
shape.setGeometry(gi.getGeometryArray()); // true, true, false));
String matName = curname; // WARNING???
if (!cJME.mergeAttributes)
matName = (String) groupMaterials.get(curname);
materials.assignMaterial(matName, shape);
group.addChild(shape);
scene.addNamedObject(curname, shape);
if ((DEBUG & 8) != 0)
{
t4 += System.currentTimeMillis() - time;
System.out.println("Shap " + t4 + " ms");
time = System.currentTimeMillis();
}
}
}
return scene;
} // end of makeScene
/**
* The Object File is loaded from the already opened file.
* To attach the model to your scene, call getSceneGroup() on
* the Scene object passed back, and attach the returned
* BranchGroup to your scene graph. For an example, see
* j3d-examples/ObjLoad/ObjLoad.java.
*/
public Scene load(Reader reader) throws FileNotFoundException,
IncorrectFormatException, ParsingErrorException
{
// ObjectFileParser does lexical analysis
ObjectFileParser st = new ObjectFileParser(reader);
coordList = new ArrayList();
texList = new ArrayList();
normList = new ArrayList();
coordIdxList = new ArrayList();
texIdxList = new ArrayList();
normIdxList = new ArrayList();
groups = new HashMap(50);
curGroup = "default";
sGroups = new HashMap(50);
curSgroup = null;
stripCounts = new ArrayList();
groupMaterials = new HashMap(50);
groupMaterials.put(curGroup, "default");
materials = new ObjectFileMaterials();
time = 0L;
if ((DEBUG & 8) != 0)
{
time = System.currentTimeMillis();
}
readFile(st);
if ((DEBUG & 8) != 0)
{
time = System.currentTimeMillis() - time;
System.out.println("Read " + time + " ms");
time = System.currentTimeMillis();
}
if ((flags & RESIZE) != 0)
{
resize();
}
return makeScene();
} // End of load(Reader)
/**
* For an .obj file loaded from a URL, set the URL where associated files
* (like material properties files) will be found.
* Only needs to be called to set it to a different URL
* from that containing the .obj file.
*/
public void setBaseUrl(URL url)
{
baseUrl = url;
} // End of setBaseUrl
/**
* Return the URL where files associated with this .obj file (like
* material properties files) will be found.
*/
public URL getBaseUrl()
{
return baseUrl;
} // End of getBaseUrl
/**
* Set the path where files associated with this .obj file are
* located.
* Only needs to be called to set it to a different directory
* from that containing the .obj file.
*/
public void setBasePath(String pathName)
{
basePath = pathName;
if (basePath == null || basePath == "")
{
basePath = "." + java.io.File.separator;
}
basePath = basePath.replace('/', java.io.File.separatorChar);
basePath = basePath.replace('\\', java.io.File.separatorChar);
if (!basePath.endsWith(java.io.File.separator))
{
basePath = basePath + java.io.File.separator;
}
} // End of setBasePath
/**
* Return the path where files associated with this .obj file (like material
* files) are located.
*/
public String getBasePath()
{
return basePath;
} // End of getBasePath
/**
* Set parameters for loading the model.
* Flags defined in Loader.java are ignored by the ObjectFile Loader
* because the .obj file format doesn't include lights, fog, background,
* behaviors, views, or sounds. However, several flags are defined
* specifically for use with the ObjectFile Loader (see above).
*/
public void setFlags(int flags)
{
this.flags = flags;
if ((DEBUG & 4) != 0)
{
System.out.println("Flags = " + flags);
}
} // End of setFlags
/**
* Get the parameters currently defined for loading the model.
* Flags defined in Loader.java are ignored by the ObjectFile Loader
* because the .obj file format doesn't include lights, fog, background,
* behaviors, views, or sounds. However, several flags are defined
* specifically for use with the ObjectFile Loader (see above).
*/
public int getFlags()
{
return flags;
} // End of getFlags
} // End of class ObjectFile
// End of file ObjectFile.java