/**
* 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.
*
* Retrieved data may be stored as state in the ChunkChopper
* via {@link #pushData} for use by other chunks.
*
* 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).
*
* 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.
*
* 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.
*
* 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:
*
* - The chunk id (short) is read
*
- The chunk length(int) is read
*
- A subchunk is looked up from the map of publish
* subchunk types of the current chunk.
*
- If it isn't found during the lookup it is skipped.
*
- Otherwise it is requested to {@link #pushData}
*
- 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.
*
- The chunk's subchunks are then loaded.
*
- The chunks initialize method is called.
*
*/
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 short
.
*/
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, 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();
}
}