/** * 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: *

    *
  1. The chunk id (short) is read *
  2. The chunk length(int) is read *
  3. A subchunk is looked up from the map of publish * subchunk types of the current chunk. *
  4. If it isn't found during the lookup it is skipped. *
  5. Otherwise it is requested to {@link #pushData} *
  6. 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. *
  7. The chunk's subchunks are then loaded. *
  8. 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(); } }