/**
|
* Make a donation http://sourceforge.net/donate/index.php?group_id=98797
|
* Microcrowd.com
|
*
|
* This library is free software; you can redistribute it and/or
|
* modify it under the terms of the GNU Lesser General Public
|
* License as published by the Free Software Foundation; either
|
* version 2.1 of the License, or (at your option) any later version.
|
*
|
* This library is distributed in the hope that it will be useful,
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
* Lesser General Public License for more details.
|
*
|
* You should have received a copy of the GNU Lesser General Public
|
* License along with this library; if not, write to the Free Software
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
*
|
* Contact Josh DeFord jdeford@realvue.com
|
*/
|
|
package com.microcrowd.loader.java3d.max3ds;
|
|
import java.awt.Image;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.nio.BufferUnderflowException;
|
import java.nio.ByteBuffer;
|
import java.nio.ByteOrder;
|
import java.nio.channels.Channels;
|
import java.nio.channels.ReadableByteChannel;
|
import java.util.HashMap;
|
import java.util.logging.Level;
|
import java.util.logging.Logger;
|
import javax.media.j3d.Behavior;
|
import javax.media.j3d.BranchGroup;
|
import javax.media.j3d.Light;
|
import javax.media.j3d.Texture;
|
import javax.media.j3d.TransformGroup;
|
import javax.vecmath.Point3f;
|
import javax.vecmath.Vector3f;
|
import com.microcrowd.loader.java3d.max3ds.chunks.Chunk;
|
import com.microcrowd.loader.java3d.max3ds.data.KeyFramer;
|
import com.sun.j3d.loaders.SceneBase;
|
import com.sun.j3d.utils.image.TextureLoader;
|
|
/**
|
* A singleton flyweight factory responsible for chopping the
|
* data up and sending it to the corresponding
|
* chunks(which are flyweights ala the flyweight pattern)
|
* for processing.
|
* This will sequentially read a 3ds file, load or
|
* skip chunks and subchunks and initialize the data
|
* for the chunks.
|
* <p>
|
* Retrieved data may be stored as state in the ChunkChopper
|
* via {@link #pushData} for use by other chunks.
|
* <p>
|
* Features not supported; unknown chunks are skipped.
|
*/
|
public class ChunkChopper
|
{
|
private Logger logger = Logger.getLogger(ChunkChopper.class.getName());
|
|
private Loader3DS loader;
|
private BranchGroup sceneGroup;
|
private SceneBase base;
|
private HashMap dataMap;
|
private ByteBuffer chunkBuffer;
|
private Integer chunkID;
|
|
private TransformGroup currentGroup;
|
private String currentObjectName;
|
private ChunkTester chunkTester = new ChunkTester();
|
private Chunk mainChunk = new Chunk("MainChunk");
|
private ChunkMap chunkMap = new ChunkMap(mainChunk);
|
|
private KeyFramer keyFramer = new KeyFramer();
|
|
/** This should be turned on by Loader3DS to view debugging information. */
|
public static boolean debug;
|
|
/** Current chunk for which debugging info is viewed if debug == true */
|
public static Chunk debugChunk;
|
|
/**
|
* private singleton constructor.
|
*/
|
public ChunkChopper(){}
|
|
/**
|
* This sequentially parses the chunks out of the input stream and
|
* constructs the 3D entities represented within.
|
* A Chunk is a little endian data structure consists of a
|
* 6 byte header followed by subchunks and or data.
|
* The first short int(little endian) represent the id
|
* of the chunk. The next int represent the total
|
* length of the chunk(total of data, subchunks and chunk header).
|
* <p>
|
* The first chunk is the main chunk (id=4D4D) and its length
|
* is always the length of the file. It only contains sub chunks.
|
* Other chunks may contain data, subchunks or both. If the format
|
* of a chunk is unknown skipped.
|
* <p>
|
* Subclasses of chunk will all automagically load the subchunks.
|
* It is the programmers responsibility to ensure that the data
|
* preceeding the subchunks is loaded or skipped as
|
* required and that something useful is done with the data. If data from the
|
* subchunks is needed in order to initialize components then that code should be
|
* placed in {@link Chunk#initialize}. Otherwise the data may be dealt with in
|
* {@link Chunk#loadData}. Also, if a chunk has data preceeding its subchunks
|
* it communicates how many bytes long that data is by returning it from loadData.
|
* <p>
|
* This chopper reads a file in order from beginning to end
|
* @param inputStream the stream with the data to be parsed.
|
* @param loader the loader that will be configured from the data.
|
* @param modelName name of the model file for display purposes.
|
* @param modelSize size in bytes of the file to read.
|
*/
|
public synchronized SceneBase loadSceneBase(InputStream inputStream, Loader3DS loader, int modelSize)
|
{
|
this.loader = loader;
|
this.sceneGroup = new BranchGroup();
|
this.base = new SceneBase();
|
this.dataMap = new HashMap();
|
base.setSceneGroup(sceneGroup);
|
|
//FileChannel channel = null;
|
ReadableByteChannel channel = null;
|
try {
|
channel = Channels.newChannel(inputStream);
|
chunkBuffer = getByteBuffer(channel, modelSize);
|
//chunkBuffer = getDirectByteBuffer(channel, modelSize);
|
|
int mainChunkID = chunkBuffer.getShort();
|
long mainChunkLength = chunkBuffer.getInt();
|
|
long begin = System.currentTimeMillis();
|
logger.finest("\n\n\n STARTING SUBCUNKS " + (mainChunkLength - 61));
|
try {
|
loadSubChunks(mainChunk, 0);
|
}
|
catch(CannotChopException e){
|
|
}
|
logger.finest("FINISHED WITH THE SUBCHUNKS");
|
}
|
catch (Exception e) {
|
e.printStackTrace();
|
}
|
finally
|
{
|
try {
|
if(channel != null) {
|
channel.close();
|
}
|
} catch (Exception e){
|
//Just closing file.. don't care.
|
}
|
}
|
return base;
|
}
|
|
/**
|
* Allocates and loads a byte buffer from the channel
|
* @param channel the file channel to load the data from
|
* @return a direct byte buffer containing all the data of the channel at position 0
|
*/
|
public ByteBuffer getByteBuffer(ReadableByteChannel channel, int channelSize) throws IOException
|
{
|
ByteBuffer chunkBuffer = ByteBuffer.allocate(channelSize);
|
chunkBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
channel.read(chunkBuffer);
|
chunkBuffer.position(0);
|
return chunkBuffer;
|
}
|
|
|
/**
|
* The base class Chunk takes care of loading subchunks for
|
* all chunks types. It occurs as follows:
|
* <ol>
|
* <li>The chunk id (short) is read
|
* <li>The chunk length(int) is read
|
* <li>A subchunk is looked up from the map of publish
|
* subchunk types of the current chunk.
|
* <li>If it isn't found during the lookup it is skipped.
|
* <li>Otherwise it is requested to {@link #pushData}
|
* <li>The return value, if there is one, is used to determine
|
* where its next subchunk is. A return value of 0 signifies
|
* that the next subchunk is nigh.
|
* <li>The chunk's subchunks are then loaded.
|
* <li>The chunks initialize method is called.
|
* </ol>
|
*/
|
protected void loadSubChunks(Chunk parentChunk, int level) throws CannotChopException
|
{
|
level++;
|
while(chunkBuffer.hasRemaining())//hasRemaining() evaluates limit - position.
|
{
|
chunkID = new Integer(chunkBuffer.getShort());
|
Chunk chunk = parentChunk.getSubChunk(chunkID);
|
|
int currentChunkLength = chunkBuffer.getInt() - 6; //length includes this 6 byte header.
|
int finishedPosition = chunkBuffer.position() + currentChunkLength;
|
int previousLimit = chunkBuffer.limit();
|
chunkBuffer.limit(chunkBuffer.position() + currentChunkLength);
|
|
if(debug) {
|
debug(parentChunk, level, chunkID, currentChunkLength, chunkBuffer.position(), chunkBuffer.limit());
|
}
|
if(chunk != null && currentChunkLength != 0) {
|
try {
|
chunk.loadData(this);
|
}
|
catch(BufferUnderflowException e){
|
chunkBuffer.position(finishedPosition);
|
chunkBuffer.limit(previousLimit);
|
throw new CannotChopException(" tried to read too much data from the buffer. Trying to recover.", e);
|
}
|
try {
|
if(chunkBuffer.hasRemaining()) {
|
loadSubChunks(chunk, level);
|
}
|
chunk.initialize(this);
|
}
|
catch(CannotChopException e){
|
logger.log(Level.SEVERE, chunk.toString() + "Trying to continue");
|
}
|
}
|
|
chunkBuffer.position(finishedPosition);
|
chunkBuffer.limit(previousLimit);
|
}
|
}
|
|
/**
|
* Gets the key framer chunk
|
* These should be their own objects instead of chunks.
|
*/
|
public KeyFramer getKeyFramer()
|
{
|
return keyFramer;
|
}
|
|
/**
|
* Adds a group to the choppers scene group
|
* and sets the current name and group.
|
* @param the name of the object to add which
|
* will also be the current name of the object
|
* the chopper is working with.
|
* @param group the current group that the chopper
|
* will be adding things too.
|
*/
|
public void addObject(String name, TransformGroup group)
|
{
|
sceneGroup.addChild(group);
|
currentGroup = group;
|
currentObjectName = name;
|
base.addNamedObject(name, group);
|
}
|
|
|
|
/**
|
* Gets the name of the current object
|
* the chopper is working with. The value returned
|
* from this generally is either set by a NamedObjectChunk
|
* and is the name of the name of the last object added
|
* or is the name set by setObjectName.
|
* @return the name of the current object being
|
* constructed.
|
*/
|
public String getObjectName()
|
{
|
return currentObjectName;
|
}
|
|
|
/**
|
* Sets the name of the current object.
|
* The name of the current object can also be set
|
* with {@link #addObject}
|
* @param name the name that the current object should be set to.
|
*/
|
public void setObjectName(String name)
|
{
|
currentObjectName = name;
|
}
|
|
/**
|
* Gets the group for the current object
|
* the chopper is working with. The value returned
|
* from this generally gets set by a NamedObjectChunk
|
* and is the name of the last object added.
|
* @return the group for the current object being
|
* constructed.
|
*/
|
public TransformGroup getGroup()
|
{
|
return currentGroup;
|
}
|
|
/**
|
* Used to store data that may later be used
|
* by another chunk.
|
* @param key the look up key.
|
* @param data the data to store.
|
*/
|
public void pushData(Object key, Object data)
|
{
|
dataMap.put(key, data);
|
}
|
|
/**
|
* Gets a datum that had been retrieved and stored
|
* via {@link #pushData} earlier and removes it.
|
* @param key the key used to store the datum earlier.
|
*/
|
public Object popData(Object key)
|
{
|
Object retVal = dataMap.remove(key);
|
return retVal;
|
}
|
|
/**
|
* Sets a named object in the loader.
|
* @param key the key name of the object
|
* @param value the named Object.
|
*/
|
public void setNamedObject(String key, Object value)
|
{
|
base.addNamedObject(key, value);
|
}
|
|
/**
|
* Returns true if there have been lights loaded.
|
* @return true if there are lights.
|
*/
|
public boolean hasLights()
|
{
|
return (base.getLightNodes() != null && base.getLightNodes().length > 0);
|
}
|
|
/**
|
* Adds a behavior to the scene base.
|
* @param behavior the behavior to add to the scene base.
|
*/
|
public void addBehaviorNode(Behavior behavior)
|
{
|
base.addBehaviorNode(behavior);
|
}
|
|
|
/**
|
* Adds a light to the scene base.
|
* @param light the light to add to the scene base.
|
*/
|
public void addLightNode(Light light)
|
{
|
base.addLightNode(light);
|
}
|
|
|
/**
|
* Adds a camera transform to the scene base.
|
* @param viewGroup the transform group to add as a view.
|
*/
|
public void addViewGroup(TransformGroup viewGroup)
|
{
|
base.addViewGroup(viewGroup);
|
}
|
|
/**
|
* Sets a named Object in the loader.
|
* @param key the key used as the name for which the object will be returned
|
*/
|
public Object getNamedObject(String key)
|
{
|
if(key == null)
|
return null;
|
return base.getNamedObjects().get(key);
|
}
|
|
/**
|
* Gets and cast the named object for the
|
* key provided. Its an error if its not
|
* a transform group.
|
*/
|
public TransformGroup getNamedTransformGroup(String key)
|
{
|
Object object = getNamedObject(key);
|
if(object instanceof TransformGroup)
|
{
|
return (TransformGroup)object;
|
}
|
else if (object != null)
|
{
|
logger.log(Level.INFO, "Retrieving " + key + " which is a named object but not useable because "+
|
" its not a transform group. Its a " + object.getClass().getName());
|
}
|
return null;
|
}
|
|
|
/**
|
* Gets a long from the chunk Buffer
|
*/
|
public long getLong()
|
{
|
return chunkBuffer.getLong();
|
}
|
|
/**
|
* Reads a short and returns it as a signed
|
* int.
|
*/
|
public int getShort()
|
{
|
return chunkBuffer.getShort();
|
}
|
|
/**
|
* Reads a short and returns it as an unsigned
|
* int.
|
*/
|
public int getUnsignedShort()
|
{
|
return chunkBuffer.getShort()&0xFFFF;
|
}
|
|
/**
|
* reads a float from the chunkBuffer.
|
*/
|
public float getFloat()
|
{
|
return chunkBuffer.getFloat();
|
}
|
|
/**
|
* Reads 3 floats x,z,y from the chunkbuffer.
|
* Since 3ds has z as up and y as pointing in whereas
|
* java3d has z as pointing forward and y as pointing up;
|
* this returns new Vector3f(x,-z,y)
|
*
|
*/
|
public Vector3f getVector()
|
{
|
return new Vector3f(getPoint());
|
}
|
/**
|
* Reads 3 floats x,z,y from the chunkbuffer.
|
* Since 3ds has z as up and y as pointing in whereas
|
* java3d has z as pointing forward and y as pointing up;
|
* this returns new Point3f(x,-z,y)
|
*/
|
public Point3f getPoint()
|
{
|
float x = chunkBuffer.getFloat();
|
float z = -chunkBuffer.getFloat();
|
float y = chunkBuffer.getFloat();
|
return new Point3f(x,y,z);
|
}
|
|
/**
|
* Reads an int and returns it
|
* @return the int read
|
*/
|
public int getInt()
|
{
|
return chunkBuffer.getInt();
|
}
|
|
/**
|
* Reads an int and returns it
|
* unsigned, any ints greater than MAX_INT
|
* will break.
|
*/
|
public int getUnsignedInt()
|
{
|
return chunkBuffer.getInt()&0xFFFFFFFF;
|
}
|
|
/**
|
* Reads a byte, unsigns it, returns the corresponding int.
|
* @return the unsigned int corresponding to the read byte.
|
*/
|
public int getUnsignedByte()
|
{
|
return chunkBuffer.get()&0xFF;
|
}
|
|
/**
|
* Reads a number of bytes corresponding to the
|
* number of bytes left in the current chunk and returns an array
|
* containing them.
|
* @return an array containing all the bytes for the current chunk.
|
*/
|
public byte[] getChunkBytes()
|
{
|
byte[] retVal = new byte[chunkBuffer.limit() - chunkBuffer.position()];
|
get(retVal);
|
return retVal;
|
}
|
|
/**
|
* Fills bytes with data from the chunk buffer.
|
* @param bytes the array to fill with data.
|
*/
|
public void get(byte[] bytes)
|
{
|
chunkBuffer.get(bytes);
|
}
|
|
|
/**
|
* Sets the data map used to store values
|
* that chunks may need to retrieve later.
|
* @param dataMap the hashmap that will be used to store
|
* and retrieve values for use by chunks.
|
*/
|
public void setDataMap(HashMap dataMap)
|
{
|
this.dataMap = dataMap;
|
}
|
|
|
/**
|
* This reads bytes until it gets 0x00 and returns
|
* the corresponding string.
|
*/
|
public String getString()
|
{
|
StringBuffer stringBuffer = new StringBuffer();
|
char charIn = (char)chunkBuffer.get();
|
while(charIn != 0x00)
|
{
|
stringBuffer.append(charIn);
|
charIn = (char)chunkBuffer.get();
|
}
|
return stringBuffer.toString();
|
}
|
|
/**
|
* Gets the id of the current chunk.
|
* @return id of the current chunk as read
|
* from the chunkBuffer. It will be a signed <code>short</code>.
|
*/
|
public Integer getID()
|
{
|
return chunkID;
|
}
|
|
/**
|
* Loads the image to server as a texture.
|
* @param textureImageName name of the image that
|
* is going to be set to be the texture.
|
*/
|
public Texture createTexture(String textureImageName)
|
{
|
Image image = loader.getTextureImage(textureImageName);
|
if(image == null)
|
{
|
System.err.println("Cannot load texture image " + textureImageName +
|
". Make sure it is in the directory with the model file. " +
|
"If its a bmp make sure JAI is installed.");
|
return null;
|
}
|
try
|
{
|
TextureLoader textureLoader = new TextureLoader(image, null);
|
return textureLoader.getTexture();
|
}
|
catch(Exception e){
|
e.printStackTrace();
|
}
|
return null;
|
}
|
|
/**
|
* prints some handy information... the chunk hierarchy.
|
*/
|
protected void debug(Chunk parentChunk, int level, Integer chunkID, long chunkLength, int position, long limit)
|
{
|
try {
|
for(int i=0; i<level; i++)
|
{
|
System.out.print(" ");
|
}
|
Object child = parentChunk.getSubChunk(chunkID);
|
int id = ((short)chunkID.intValue()) & 0xFFFF;
|
System.out.println(parentChunk + " is " +
|
(child==null?"skipping":"LOADING")+
|
": [id=" + Integer.toHexString(id) +
|
", object= <" + parentChunk.getSubChunk(chunkID) +
|
">, chunkLength=" + chunkLength +
|
", position=" + position +
|
" limit=" + limit + "]");
|
}
|
catch(Exception e){
|
//We're debugging.. its ok
|
e.printStackTrace();
|
}
|
}
|
|
/**
|
* Prints an exception and exits.
|
*/
|
private void exceptAndExit(Throwable exception)
|
{
|
logger.log(Level.SEVERE, "\nThe chunk for loadData method read too much or not enough data from the stream." +
|
" It needs be skipped or adjusted to read more or less data.");
|
exception.printStackTrace();
|
System.exit(3);
|
}
|
|
/**
|
* Convert the integer to an unsigned number.
|
* @param i the integer to convert.
|
*/
|
private static String byteString(int i)
|
{
|
final char[] digits = {
|
'0' , '1' , '2' , '3' , '4' , '5' ,
|
'6' , '7' , '8' , '9' , 'a' , 'b' ,
|
'c' , 'd' , 'e' , 'f' };
|
|
char[] buf = new char[2];
|
buf[1] = digits[i & 0xF];
|
i >>>= 4;
|
buf[0] = digits[i & 0xF];
|
|
return "0x" + new String(buf).toUpperCase();
|
}
|
}
|