// package hplb.java3d;
package max;

import  java.io.*;
import 	java.net.*;
import  java.util.*;
import  java.awt.Component;

import  javax.media.j3d.*;
import  javax.vecmath.*;
import  com.sun.j3d.utils.image.TextureLoader;

/**
 *  .3DS format model loader for Java3D.
 */

public class Load3DS
{
    static final int        MAX_SURFACES    = 33;
    static final int        verbosity       = 1;

    int                     totalPolygons   = 0;
    int                     totalVertices   = 0;

    //
    // Root of the scene graph for the file.
    //
    BranchGroup             root            = null;

    //
    // Lookup table of instances of SharedObjects held
    // in <objectTable>, indexed by name.
    //
    Hashtable               instanceTable   = null;

    //
    // Lookup table of all SharedObjects created,
    // indexed by name.
    //
    Hashtable               objectTable     = null;

    //
    // Refers to the object currently being constructed.
    //
    SharedGroup             object          = null;

    //
    // Refers to the shape component of the object
    // currently being constructed.
    //
    Shape3D                 shape           = null;

    //
    // <geometry>     == current object's geometry.
    // <noofVertices> == number of vertices in the face list.
    // <vertices>     == vertex array.
    // <noofFaces>    == number of faces in the object.
    // <faces>        == list of all the faces in the object.
    // <sharedFaces>  == list of vertices constructing each face.
    // <surfaces>     == list of surfaces making up geometry.
    // <textureCoords>== list of geometry's texture coordinates.
    //
    TriangleArray           geometry        = null;
    int                     noofVertices    = 0;
    Point3f[]               vertices        = null;
    int                     noofFaces       = 0;
    Face[]                  faces           = null;
    Vector[]                sharedFaces     = null;
    Surface[]               surfaces        = null;
    Point2f[]               textureCoords   = null;

    //
    // Used to indicate whether surfaces were specified
    // and created.
    //
    boolean                 surfacesCreated = true;

    //
    // A lookup table of Appearance objects representing
    // the materials in the file, indexed by material name.
    //
    Hashtable               mats            = null;

    //
    // <mat> refers to 3DS material currently being constructed.
    // <matName> is that name of the 3DS material.
    // <material> refers to the counterpart Java3D Material.
    //
    Appearance              mat             = null;
    String                  matName         = null;
    Material                material        = null;

    //
    // <shininess>    == 3DS shininess percentage.
    //
    float                   shininess       = 0.0f;

    //
    // Provided by the constructor of this object, needed
    // by the TextureLoader class.
    //
    Component               component       = null;

    //
    // <nodeName>     == name of a referenced SharedObject.
    // <instanceName> == name of an instance of a SharedObject.
    //
    String                  nodeName        = null;
    String                  instanceName    = null;

    //
    // <translation>  == positional component of the objects' transform.
    // <orientation>  == rotational component of the objects' transform.
    // <scale>        == scalar component of the objects' transform.
    //
    Vector3f                translation     = null;
    Matrix4f                orientation     = null;
    Vector3f                scale           = null;

    String					path 			= null;

    /**
     * <p>Given a <i>filename</i>, Load3DS will construct an appropriate Java3D scene graph
     * - accessible from Load3DS.root().  It also constructs a table of <i>objects</i>,
     * indexed by their proper name and a table of <instances> of those objects which
     * are refered to by ther instance name.  Currently, only two <properties> are
     * returned, "polygons" and "vertices", the value of which are a java.lang.Integer
     * giving the total number of polygons and vertices in the .3DS file respectively.</p>
     * <p>Limitations of the current version:
     * <li>Lights are not created.</li>
     * <li>Interpretation of 3DS's shininess and shininess strength could be better.</li>
     * <li>Only handles those texture map image formats supported by com.sun.j3d.utils.image.TextureLoader.</li>
     * </p>
     * <p>Java3Disms:
     * <li>3DS wireframe material = PolygonAttributes.POLYGON_LINE</li>
     * <li>3DS metal-shaded material = PolygonAttributes.POLYGON_POINT</li>
     * <li>3DS constant-shaded material = PolygonAttributes.POLYGON_FILL</li>
     * <li>3DS phong-shaded material = PolygonAttributes.POLYGON_PHONG</li>
     * <li>Hidden objects don't seem to be set by 3DSMax.  Load3DS simulates this
     * by hiding objects whose names begin with '$'.</li>
     * <li>Note that only 3DS planar mapping generates correct 2D texture coords.</li>
     * <li>1 generic 3DSMax unit = 1 Java3D unit.  This loader does not handle other 3DSMax units,
     * e.g. metric.</li>
     * </p>
     * <p>Known bugs:
     * <li>Normal calculation fails for certain surfaces.</li>
     * @param filename .3DS file to load.
     * @param component parent java.awt.Component (needed by com.sun.j3d.utils.image.TextureLoader).
     * @param objects table of named objects indexed by object name.  name-value pairs
     * are a string and a SharedGroup respectively.
     * @param instances table of instanced objects indexed by instance name.  name-value
     * pairs are a string and a TransformGroup respectively.
     * @param properties information about the .3DS file.
     * @exception IOException any failure to process .3DS file.
     * @see com.sun.j3d.utils.image.TextureLoader
     */

    public Load3DS( String filename, Component component,
                      Hashtable objects, Hashtable instances,
                      Properties properties )
                throws IOException
    {
	URL url;
	
	url = new URL(filename);
	URLConnection connection = url.openConnection();
	System.out.println( "Loading Data from " + filename);


        DataInputStream    in  = new DataInputStream( connection.getInputStream());

        this.component    = component;
        root              = new BranchGroup();        // Root of scene graph for this file.
        mats              = new Hashtable();

        objectTable       = objects;
        instanceTable     = instances;

        try
        {
            for ( ;; )
            {
                processChunk( in );
            }
        }
        catch ( EOFException e )
        {
        }

        properties.put( "polygons", Integer.toString( totalPolygons ) );
        properties.put( "vertices", Integer.toString( totalVertices ) );
    }


    void prepareForNewObject()
    {
        object          = null;
        shape           = null;
        geometry        = null;
        noofVertices    = 0;
        vertices        = null;
        sharedFaces     = null;
        noofFaces       = 0;
        faces           = null;
        surfaces        = null;
        surfacesCreated = false;
        textureCoords   = null;
        mat             = null;
        matName         = null;
        material        = null;
        shininess       = 0.0f;
        component       = null;
        nodeName        = null;
        instanceName    = null;
        translation     = null;
        orientation     = null;
        scale           = null;
    }


    /**
     *  Returns the root of the scene graph constructed
     *  from the given file.
     *  @return Root of constructed scene graph.
     */

    public BranchGroup root()
    {
        return root;
    }


    //
    // A .3DS file consists of a series of chunks.  Each chunk
    // starts with a tag ID and the chunks' length followed by
    // optional data.  This method handles most of those chunks
    // processed by this loader.  Those chunks that are not
    // recognised are ignored.
    //

    void processChunk( DataInputStream in )
            throws IOException
    {
        int tag     = readUnsignedShort( in );
        int length  = readInt( in );

        switch ( tag )
        {
            case S3D_M3DMAGIC:          // 3DS file
            case S3D_MMAGIC:            // Editor chunks
            case S3D_MAT_ENTRY:         // 3DS material
            case S3D_HIERARCHY_NODE:    // Object instance
            case S3D_N_TRI_OBJECT:      // Object definition
                processChunk( in );
                break;

            case S3D_HIERARCHY:         // Start of the instance tree
                if ( surfacesCreated == false )
                {
                    createUnsmoothedFaces();
                    prepareForNewObject();
                }

                processChunk( in );
                break;

            case S3D_HIERARCHY_LINK:    // Details of an object instance
                processLink( in );
                break;

            case S3D_NODE_ID:           // Node ID - unused.
                processNodeID( in );
                break;

            case S3D_POS_TRACK_TAG:     // Contains object's initial position
                processPosTrackTag( in );
                break;

            case S3D_ROT_TRACK_TAG:     // Contains object's initial orientation
                processRotTrackTag( in );
                break;

            case S3D_SCL_TRACK_TAG:     // Contains object's initial scale
                processSclTrackTag( in );
                break;

            case S3D_INSTANCE_NAME:     // Name given to object instance
                instanceName = readName( in );
                break;

            case S3D_AMBIENT_LIGHT:     // Ambient light
                processAmbientLight( in );
                break;

            case S3D_MASTER_SCALE:      // Global scaling factor
                processMasterScale( in );
                break;

            case S3D_MAT_NAME:          // Start of a 3DS material
                matName     = readName( in );
                mat         = new Appearance();
                material    = new Material();
                material.setLightingEnable( true );
                mat.setMaterial( material );
                mats.put( matName, mat );

                if ( verbosity > 2 )
                {
                    System.out.println( "Processing material '" + matName + "'" );
                }
                break;

            case S3D_MAT_AMBIENT:       // Ambient colour of material
                Color3f     ambient = readColor( in );
                material.setAmbientColor( ambient );

                if ( verbosity > 2 )
                {
                    System.out.println( "==     Ambient: " + ambient );
                }
                break;

            case S3D_MAT_DIFFUSE:       // Diffuse colour of material
                Color3f     diffuse = readColor( in );
                material.setDiffuseColor( diffuse );

                if ( verbosity > 2 )
                {
                    System.out.println( "==     Diffuse: " + diffuse );
                }
                break;

            case S3D_MAT_SPECULAR:      // Specular colour of material
                material.setSpecularColor( readColor( in ) );
                break;

            case S3D_MAT_SHININESS:     // 3DS shininess percentage
                shininess = readPercentage( in );
                //System.out.println( "== Shininess: " + shininess );
                break;

            case S3D_MAT_SHININESS_STRENGTH:    // 3DS shininess strength
                float   shininessStrength = readPercentage( in );
                //System.out.println( "== Shininess strength: " + shininessStrength );
                material.setShininess( (1.0f - ((shininess + shininessStrength) / 2.0f)) * 128 );
                break;

            case S3D_MAT_TRANSPARENCY:  // Transparency percentage
                float   transparency = readPercentage( in );

                if ( verbosity > 2 )
                {
                    System.out.println( "== Transparency: " + transparency );
                }

                if ( transparency > 0.1f )
                {
                    TransparencyAttributes  ta  = new TransparencyAttributes();

                    ta.setTransparency( transparency );
                    mat.setTransparencyAttributes( ta );

                    //
                    // If we turn transparency on then we should
                    // also turn back face culling off.
                    //

                    PolygonAttributes       pa  = new PolygonAttributes();

                    pa.setCullFace( PolygonAttributes.CULL_NONE );
                    mat.setPolygonAttributes( pa );
                }
                break;

            case S3D_MAT_SHADING:       // Type of rendering
                int                 style;
                int                 mode = readUnsignedShort( in );
                PolygonAttributes   pa = mat.getPolygonAttributes();
                ColoringAttributes  ca = mat.getColoringAttributes();

                if ( pa == null )
                {
                    pa = new PolygonAttributes();
                }
                if ( ca == null )
                {
                    ca = new ColoringAttributes();
                }

                switch ( mode )
                {
                    case 0:         // Wireframe (not used in 3DSMax?)
                        style = PolygonAttributes.POLYGON_LINE;
                        break;

                    case 2:         // Metal - use this for rendering points.
                        style = PolygonAttributes.POLYGON_POINT;
                        break;

                    case 1:         // Constant
                        style = PolygonAttributes.POLYGON_FILL;
                        ca.setShadeModel( ColoringAttributes.SHADE_FLAT );
                        break;

                    case 3:         // Phong
                    default:
                        style = PolygonAttributes.POLYGON_FILL;
                        ca.setShadeModel( ColoringAttributes.NICEST );
                        break;

                }

                if ( verbosity > 2 )
                {
                    System.out.println( "== Shading: " + mode + ", style = " + style );
                }

                pa.setPolygonMode( style );
                mat.setPolygonAttributes( pa );
                mat.setColoringAttributes( ca );
                break;

            case S3D_MAT_WIRE:          // Another way of enforcing wireframe
                PolygonAttributes   pat = mat.getPolygonAttributes();
                pat.setPolygonMode( PolygonAttributes.POLYGON_LINE );

                if ( verbosity > 2 )
                {
                    System.out.println( "== Wireframe" );
                }
                break;

            case S3D_MAT_WIRESIZE:      // Wireframe line width
                float               width   = readFloat( in );
                LineAttributes      la      = mat.getLineAttributes();

                if ( la == null )
                {
                    la = new LineAttributes();
                }

                la.setLineWidth( width );
                mat.setLineAttributes( la );

                if ( verbosity > 2 )
                {
                    System.out.println( "== Wire width: " + width );
                }
                break;

            case S3D_MAT_TWO_SIDE:      // Face culling
                PolygonAttributes   pat2 = mat.getPolygonAttributes();
                pat2.setCullFace( PolygonAttributes.CULL_NONE );

                if ( verbosity > 2 )
                {
                    System.out.println( "== Two sided" );
                }
                break;

           case S3D_MAT_TEXMAP:         // Image for texture map
                float   matPercent  = readPercentage( in ) * 100;
                String  imageName   = path + readMatName( in );

                System.out.println( "Loading texture map '" + imageName + "' (" +
                                    matPercent + "%)" );

                if ( new java.io.File( imageName ).exists() == false )
                {
                    System.out.println( "** Can't find image '" + imageName + "'" );
		            imageName = null;
                }
                else
                {
	                Texture textureMap = new TextureLoader( imageName, component ).getTexture();
	                if ( textureMap == null )
	                {
	                    System.out.println( "** Texturing has been disabled" );
	                }

	                if ( verbosity > 1 )
	                {
	                    System.out.println( "== Texturing: " + textureMap.getEnable() );
    	                System.out.println( "== MipMapMode: " + textureMap.getMipMapMode() );
    	            }

                    //textureMap.setMinFilter( Texture.BASE_LEVEL_POINT );
                    //textureMap.setMagFilter( Texture.BASE_LEVEL_POINT );
	                mat.setTexture( textureMap );

	                TextureAttributes   texA = mat.getTextureAttributes();

	                if ( texA == null )
	                {
	                    texA = new TextureAttributes();
	                }

	                mat.setTextureAttributes( texA );
	            }
                break;

            case S3D_TEX_VERTS:         // 2D Texture coordinates
                processTextureCoordinates( in );
                break;

            case S3D_NAMED_OBJECT:      // Start of 3DS object
                if ( surfacesCreated == false )
                {
                    createUnsmoothedFaces();
                    prepareForNewObject();
                }

                String objectName = readName( in );

                if ( hiddenObject( objectName ) )
                {
                    skipChunk( in, length - objectName.length() - 1 );
                    System.out.println( "(Skipping hidden object '" + objectName + "')" );
                    break;
                }

                System.out.println( "Processing object '" + objectName + "'" );

                object = new SharedGroup();
                shape  = new Shape3D();
                processChunk( in );

                if ( verbosity > 1 )
                {
                    System.out.println( "== Adding shape to transform group" );
                }

                object.addChild( shape );

                if ( verbosity > 1 )
                {
                    System.out.println( "== Adding object to list of shared objects" );
                }

                objectTable.put( objectName, object );
                break;

            case S3D_POINT_ARRAY:           // Vertex list
                processPointArray( in );
                break;

            case S3D_FACE_ARRAY:            // Face list
                processFaceArray( in );
                break;

            case S3D_MSH_MAT_GROUP:         // Materials used by object
                processMaterial( in );
                break;

            case S3D_SMOOTH_GROUP:          // List of surfaces
                processSmoothGroup( in );
                prepareForNewObject();
                break;

            case S3D_MESH_MATRIX:           // Object transform
                processMeshMatrix( in );
                break;

            case S3D_POINT_FLAG_ARRAY:      // Not much use to us
            case S3D_PIVOT:                 // Unused
                skipChunk( in, length );
                break;

            default:
                if ( verbosity > 0 )
                {
                    System.out.println( "TAG: 0x" + Integer.toHexString( tag ) +
                                        "  LEN: " + length );
                }
                skipChunk( in, length );
                break;
        }
    }


    //
    // Skip over the current chunk, i.e. we are not interested in it.
    //

    void skipChunk( DataInputStream in, int length )
                throws IOException
    {
        int bytesToSkip = length - 6;

        if ( bytesToSkip > 0 )
        {
            in.skipBytes( bytesToSkip );
        }
    }


    //
    // S3D_OBJ_HIDDEN doesn't seem to be set by 3DSMax, so
    // objects that have names beginning with '$' are taken
    // as hidden.
    //

    boolean hiddenObject( String name )
    {
        return name.charAt( 0 ) == '$';
    }


    //
    // Defines an instance of an object.
    //

    void processLink( DataInputStream in )
                throws IOException
    {
        nodeName = readName( in );

        int     flags1  = readUnsignedShort( in );
        int     flags2  = readUnsignedShort( in );
        int     dummy   = readUnsignedShort( in );

        if ( verbosity > 2 )
        {
            System.out.println( "== Link for object '" + nodeName + "': 0x" +
                                Integer.toHexString( flags1 ) + ", 0x" +
                                Integer.toHexString( flags2 ) + ", 0x" +
                                Integer.toHexString( dummy ) );
        }
    }


    //
    // Processed out of curiosity!
    //

    void processNodeID( DataInputStream in )
                throws IOException
    {
        int id = readUnsignedShort( in );

        if ( verbosity > 1 )
        {
            System.out.println( "== NodeID: " + id );
        }
    }


    //
    // Lookup an object by <name>.
    //

    SharedGroup findObject( String name )
    {
        return (SharedGroup)objectTable.get( name );
    }


    //
    // Take the last position specified in this keyframe list
    // as the initial position of the object.
    //

    void processPosTrackTag( DataInputStream in )
                throws IOException
    {
        int dummy, keys, i;

        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        keys  = readUnsignedShort( in );
        dummy = readUnsignedShort( in );

        for ( i = 0; i < keys; i++ )
        {
            dummy = readUnsignedShort( in );
            dummy = readInt( in );

            //
            // Reverse the Y and Z coordinates, negate Z coordinates
            //

            float   x = readFloat( in ), y = readFloat( in ), z = readFloat( in );

            translation = new Vector3f( x, z, -y );
//            translation = new Vector3f( x, y, z );

            if ( verbosity > 0 )
            {
                System.out.println( "    Position: " + translation );
            }
        }
    }


    //
    // Take the last orientation specified in this keyframe list
    // as the initial orientation of the object.
    //

    void processRotTrackTag( DataInputStream in )
                throws IOException
    {
        int dummy, keys, i;

        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        keys  = readUnsignedShort( in );
        dummy = readUnsignedShort( in );

        for ( i = 0; i < keys; i++ )
        {
            dummy = readUnsignedShort( in );
            dummy = readInt( in );
            float   rot = readFloat( in );
            float   x = readFloat( in );
            float   y = readFloat( in );
            float   z = readFloat( in );

            //
            // Convert the orientation between 3DS and
            // Java3D coordinate systems.
            //

            AxisAngle4f axes    = new AxisAngle4f( x, y, z, -rot );
            Matrix4f    m       = new Matrix4f();
            Matrix4f    rm      = new Matrix4f( );

            m.set( axes );
            rm.rotX( (float)-Math.PI / 2 );
            orientation = new Matrix4f();
            orientation.mul( rm, m );

            if ( verbosity > 0 )
            {
                System.out.println( "    Rotation: " + orientation );
            }
        }
    }


    //
    // Take the last scale specified in this keyframe list
    // as the initial scale of the object.  Also take this
    // as the queue to finish instancing the object.
    //

    void processSclTrackTag( DataInputStream in )
                throws IOException
    {
        int dummy, keys, i;

        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        dummy = readUnsignedShort( in );
        keys = readUnsignedShort( in );
        dummy = readUnsignedShort( in );

        for ( i = 0; i < keys; i++ )
        {
            dummy = readUnsignedShort( in );
            dummy = readInt( in );


            //
            // Reverse the Y and Z coordinates
            //

            float   x = readFloat( in ), y = readFloat( in ), z = readFloat( in );

            scale = new Vector3f( x, z, y );

            if ( verbosity > 0 )
            {
                System.out.println( "    Scale   : " + scale );
            }
        }

        if ( hiddenObject( nodeName ) )
        {
            return;
        }

        Matrix4f        m           = new Matrix4f();
        m.set( orientation );
        m.setTranslation( translation );

        Transform3D     transform   = new Transform3D( m );
        TransformGroup  instance    = new TransformGroup( transform );
        SharedGroup     shared      = null;

        if ( instanceName == null )     // Use the node name.
        {
            System.out.println( "Instancing '" + nodeName + "'" );
            instanceTable.put( nodeName, instance );
        }
        else
        {
            System.out.println( "Instancing '" + instanceName  + "' (->'" +
                                nodeName + "')" );
            instanceTable.put( instanceName, instance );
        }

        shared = findObject( nodeName );

        if ( shared == null )
        {
            throw new IOException( "Can't locate referenced object." );
        }

        Link    link        = new Link( shared );

        instance.addChild( link );
        root.addChild( instance );
        instanceName = null;
    }


    //
    // Processed out of curiosity.
    //

    void processMasterScale( DataInputStream in )
                throws IOException
    {
        float   scale = readFloat( in );

        if ( verbosity > 2 )
        {
            System.out.println( "== Master scale: " + scale );
        }
    }


    //
    // Read in the list of vertices.
    //

    void processPointArray( DataInputStream in )
                throws IOException
    {
        int i;

        noofVertices = readUnsignedShort( in );
        vertices     = new Point3f[noofVertices];
        sharedFaces  = new Vector[noofVertices];

        System.out.println( "    " + noofVertices + " vertices." );
        totalVertices += noofVertices;

        for ( i = 0; i < noofVertices; i++ )
        {

            vertices[i] = new Point3f( readFloat( in ), readFloat( in ),
                                       readFloat( in ) );

/*
            // Reverse the Y and Z coordinates, negate Z coordinates

            float temp = vertices[i].z;

            vertices[i].z = -vertices[i].y;
            vertices[i].y = temp;
*/

            sharedFaces[i] = new Vector();      // Filled in processFaceArray()
        }
    }


    //
    // Read in the list of polygons and allocate a TriangleArray
    // big enough to contain them.  Don't fill it in yet though,
    // we want to one surface at a time.
    //

    void processFaceArray( DataInputStream in )
                throws IOException
    {
        int i;
        int vertexFormat = GeometryArray.COORDINATES | GeometryArray.NORMALS;

        if ( textureCoords != null )
        {
            if ( verbosity > 1 )
            {
                System.out.println( "    Object is TEXTURED" );
            }
            vertexFormat |= GeometryArray.TEXTURE_COORDINATE_2;
        }

        noofFaces   = readUnsignedShort( in );
        faces       = new Face[noofFaces];
        geometry    = new TriangleArray( noofFaces * 3, vertexFormat );

        System.out.println( "    " + noofFaces + " faces." );
        totalPolygons   += noofFaces;

        for ( i = 0; i < noofFaces; i++ )
        {
            int a       = readUnsignedShort( in );
            int b       = readUnsignedShort( in );
            int c       = readUnsignedShort( in );
            int flags   = readUnsignedShort( in );

            faces[i]    = new Face( a, b, c );
        }

        if ( verbosity > 1 )
        {
            System.out.println( "== Adding geometry to shape" );
        }

        shape.setGeometry( geometry );
    }


    //
    // In 3DS a face is constructed by listing vertices in
    // anti-clockwise order.
    //

    Vector3f calculateFaceNormal( int a, int b, int c )
    {
        Vector3f    vertexA = new Vector3f( vertices[a].x, vertices[a].y,
                                            vertices[a].z );
        Vector3f    vertexB = new Vector3f( vertices[b].x, vertices[b].y,
                                            vertices[b].z );
        Vector3f    vertexC = new Vector3f( vertices[c].x, vertices[c].y,
                                            vertices[c].z );
        Vector3f    normal  = new Vector3f();

        vertexB.sub( vertexA );
        vertexC.sub( vertexA );

        normal.cross( vertexB, vertexC );
        normal.normalize();

        return normal;
    }


    //
    // Read in the transformation matrix and inverse transform
    // the vertex coordinates so they are back where they were
    // when 3DS initially created the object.  They are
    // transformed back again with the information found in
    // the position, rotation and scale keyframe tracks.
    //

    void processMeshMatrix( DataInputStream in )
                throws IOException
    {
        Matrix4f    m = new Matrix4f();
        int         x, y;

        for ( y = 0; y < 4; y++ )
        {
            for ( x = 0; x < 3; x++ )
            {
                if ( y == 3 )
                {
                    m.setElement( x, y, readFloat( in ) );
                }
                else
                {
                    m.setElement( x, y, readFloat( in ) );
                }
            }
        }

        m.setElement( 3, 3, 1.0f );

        float   transY = m.getElement( 1, 3 );
        float   transZ = m.getElement( 2, 3 );

/*
        //
        // Reverse the Y and Z coordinates, negate Z coordinates
        //


//        m.setElement( 1, 3, transZ );
//        m.setElement( 2, 3, -transY );
*/

        Transform3D t = new Transform3D( m );

        if ( verbosity > 1 )
        {
            System.out.println( "    Transform: " + t );
        }

        t.invert();

        for ( x = 0; x < noofVertices; x++ )
        {
            t.transform( vertices[x] );
        }
    }


    //
    // Associate material with the object.
    //

    void processMaterial( DataInputStream in )
                throws IOException
    {
        String      name    = readName( in );
        Appearance  m       = (Appearance)mats.get( name );

        if ( m == null )
        {
            System.err.println( "** Can't find referenced material" );
            return;
        }

        System.out.println( "    Attaching material '" + name + "'" );
        if ( verbosity > 0 )
        {
            System.out.println( "        " + m.getMaterial() );
        }

        shape.setAppearance( m );

        int         faceCount = readUnsignedShort( in );
        int         i;

        for ( i = 0; i < faceCount; i++ )
        {
            int dummy = readUnsignedShort( in );
        }
    }


    //
    // Contains a list of smoothed surfaces.  Construct each surface
    // at a time, specifying vertex normals and texture coordinates
    // (if present).
    //

    void processSmoothGroup( DataInputStream in )
                throws IOException
    {
        double  log2    = Math.log( 2 );
        int     i;

        surfaces = new Surface[MAX_SURFACES];

        for ( i = 0; i < MAX_SURFACES; i++ )
        {
            surfaces[i] = new Surface();
        }

        for ( i = 0; i < noofFaces; i++ )
        {
            int     group   = readInt( in );
            long    b = 0x1;
            int     index;

            for ( index = 0; index < 32; index++ )
            {
                if ( (group & b) > 0 )
                {
                    break;
                }
            }

            //System.out.println( "==   group " + Long.toHexString( group ) + ", index = " + index );

            faces[i].group = (int)group;
            surfaces[index].add( faces[i] );
        }

        int surface;

        i = 0;

        for ( surface = 0; surface < MAX_SURFACES; surface++ )
        {
            if ( verbosity > 1 && surfaces[surface].noofFaces() > 0 )
            {
                System.out.println( "    Constructing surface " + surface );
            }

            Enumeration     iter = surfaces[surface].faces();

            while ( iter.hasMoreElements() )
            {
                Face   f = (Face)iter.nextElement();


                Vector3f    normalA = calculateVertexNormal( f.a, f, surfaces[surface] );
                Vector3f    normalB = calculateVertexNormal( f.b, f, surfaces[surface] );
                Vector3f    normalC = calculateVertexNormal( f.c, f, surfaces[surface] );

                //System.out.println( "Face [" + f.a + "] " + f.a() + ", [" + f.b + "] " +
                //                    f.b() + ", [" + f.c + "] " + f.c() );
                //System.out.println( "Norm " + normalA + ", " + normalB + ", " +
                //                    normalC );

                geometry.setCoordinate( i * 3, f.a() );
                geometry.setCoordinate( (i * 3) + 1, f.b() );
                geometry.setCoordinate( (i * 3) + 2, f.c() );

                geometry.setNormal( i * 3, normalA );
                geometry.setNormal( (i * 3) + 1, normalB );
                geometry.setNormal( (i * 3) + 2, normalC );

                if ( textureCoords != null )
                {
                    //System.out.println( "==     Tex Coords: " + textureCoords[f.a] +
                    //                    ", " + textureCoords[f.b] + ", " +
                    //                    textureCoords[f.c] );
                    geometry.setTextureCoordinate( i * 3, textureCoords[f.a] );
                    geometry.setTextureCoordinate( (i * 3) + 1, textureCoords[f.b] );
                    geometry.setTextureCoordinate( (i * 3) + 2, textureCoords[f.c] );
                }

                i++;
            }
        }

        surfacesCreated = true;
    }


    //
    // Fill the TriangleArray with the raw polygon information,
    // don't calculate vertex normals but do give texture coordinates
    // if present.
    //

    void createUnsmoothedFaces()
    {
        int     i;

        for ( i = 0; i < noofFaces; i++ )
        {
            Face   f = faces[i];

            //System.out.println( "Face [" + f.a + "], [" + f.b + "], [" + f.c + "]" );

            geometry.setCoordinate( i * 3, f.a() );
            geometry.setCoordinate( (i * 3) + 1, f.b() );
            geometry.setCoordinate( (i * 3) + 2, f.c() );

            geometry.setNormal( i * 3, f.normal );
            geometry.setNormal( (i * 3) + 1, f.normal );
            geometry.setNormal( (i * 3) + 2, f.normal );

            if ( textureCoords != null )
            {
                //System.out.println( "==     Tex Coords: " + textureCoords[f.a] +
                //                    ", " + textureCoords[f.b] + ", " +
                //                    textureCoords[f.c] );
                geometry.setTextureCoordinate( i * 3, textureCoords[f.a] );
                geometry.setTextureCoordinate( (i * 3) + 1, textureCoords[f.b] );
                geometry.setTextureCoordinate( (i * 3) + 2, textureCoords[f.c] );
            }
        }
    }


    //
    // Checks if <vertex> is used by <face>.
    //

    boolean sharesVertex( Face face, int vertex )
    {
        return face.a == vertex || face.b == vertex || face.c == vertex;
    }


    //
    // Calculates a normalised vertex normal for <vertex> in
    // <thisFace> within <surface>.
    //

    Vector3f calculateVertexNormal( int vertex, Face thisFace, Surface surface )
    {
        Enumeration otherFaces  = surface.faces();
        Vector3f    normal      = new Vector3f( thisFace.normal );
        int         noofNormals = 1;

        while ( otherFaces.hasMoreElements() )
        {
            Face    otherFace = (Face)otherFaces.nextElement();

            if ( sharesVertex( otherFace, vertex ) )
            {
                normal.add( otherFace.normal );
                noofNormals++;
            }
        }

        if ( noofNormals != 1 )
        {
            normal.x /= noofNormals;
            normal.y /= noofNormals;
            normal.z /= noofNormals;
        }

        normal.normalize();

        return normal;
    }


    //
    // Read in the 2D texture coordinates - note these are
    // only valid if planar mapping was used in 3DS.
    //

    void processTextureCoordinates( DataInputStream in )
                throws IOException
    {
        int     vertexCount = readUnsignedShort( in );
        int     i;

        if ( vertexCount != noofVertices )
        {
            System.out.println( "** Number of texture vertices = #model vertices" );
            return;
        }

        if ( verbosity > 1 )
        {
            System.out.println( "    Texture coordinates: #" + vertexCount );
        }

        textureCoords = new Point2f[vertexCount];

        for ( i = 0; i < vertexCount; i++ )
        {
            textureCoords[i] = new Point2f( readFloat( in ), readFloat( in ) );
            //System.out.println( "==     " + textureCoords[i] );
        }
    }


    //
    // Read in the definition of the ambient light.
    //

    void processAmbientLight( DataInputStream in )
                throws IOException
    {
        Color3f ambient = readColor( in );

        System.out.println( "Ambient Light: " + ambient );
    }


    //
    // Read in a colour, either in the 3 * float format or
    // the 3 * byte format.
    //

    Color3f readColor( DataInputStream in )
                throws IOException
    {
        int tag     = readUnsignedShort( in );
        int length  = readInt( in );

        switch ( tag )
        {
            case S3D_COLOR_F:
                return new Color3f( readFloat( in ), readFloat( in ), readFloat( in ) );

            case S3D_COLOR_24:
                return new Color3f( (float)in.readUnsignedByte() / 255,
                                    (float)in.readUnsignedByte() / 255,
                                    (float)in.readUnsignedByte() / 255 );

            default:
                throw new IOException( "COLOR_F/COLOR_24 expected" );
        }
    }


    //
    // Read in a float or int percentage and return it
    // as a number between 0.0 and 1.0
    //

    float readPercentage( DataInputStream in )
                throws IOException
    {
        int tag     = readUnsignedShort( in );
        int length  = readInt( in );

        switch ( tag )
        {
            case S3D_INT_PERCENTAGE:
                return (float)readUnsignedShort( in ) / 100;

            case S3D_FLOAT_PERCENTAGE:
                return readFloat( in );

            default:
                throw new IOException( "INT_PERCENTAGE/FLOAT_PERCENTAGE expected" );
        }
    }


    //
    // Read in material name.
    //

    String readMatName( DataInputStream in )
                throws IOException
    {
        int tag     = readUnsignedShort( in );
        int length  = readInt( in );

        return readName( in );
    }


    //
    // Read in the string used to specify a name in
    // many different chunks.
    //

    String readName( DataInputStream in )
                throws IOException
    {
        StringBuffer    buf = new StringBuffer();
        char            c;

        while ( (c = (char)in.readUnsignedByte()) != '\0' )
        {
            buf.append( c );
        }

        return buf.toString();
    }


    //
    // Read in an unsigned short (16 bits).
    //

    int readUnsignedShort( DataInputStream in )
            throws IOException
    {
        int num = in.readUnsignedShort();

        return ((num << 8) & 0xFF00) | ((num >> 8) & 0x00FF);
    }


    //
    // Read in a 32 bit integer (unsigned).
    //

    int readInt( DataInputStream in )
            throws IOException
    {
        int num = in.readInt();

        return ((num << 24) & 0xFF000000) |
               ((num << 8)  & 0x00FF0000) |
               ((num >> 8)  & 0x0000FF00) |
               ((num >> 24) & 0x000000FF);
    }


    //
    // Read in a 32 bit floating point number.
    //

    float readFloat( DataInputStream in )
            throws IOException
    {
        return Float.intBitsToFloat( readInt( in ) );
    }


    //
    // Internal data structure representing a polygon and
    // when constructed updates a list of those faces
    // sharing any given vertex.
    //

    class Face
    {
        int         a, b, c;
        Vector3f    normal  = null;
        int         group;

        public Face( int vertexA, int vertexB, int vertexC )
        {
            a       = vertexA;
            b       = vertexB;
            c       = vertexC;
            normal  = calculateFaceNormal( a, b, c );

            sharedFaces[a].addElement( this );
            sharedFaces[b].addElement( this );
            sharedFaces[c].addElement( this );
        }


        public Point3f a()
        {
            return vertices[a];
        }


        public Point3f b()
        {
            return vertices[b];
        }


        public Point3f c()
        {
            return vertices[c];
        }
    };


    //
    // Internal data structure for representing a surface
    // as a list of faces.
    //

    class Surface
    {
        Vector  faces = new Vector();

        public Surface()
        {
        }


        public void add( Face f )
        {
            faces.addElement( f );
        }


        public Enumeration faces()
        {
            return faces.elements();
        }


        public int noofFaces()
        {
            return faces.size();
        }
    }


    //
    // List of chunks contained in a .3DS file that we
    // are interested in.
    //


    // .3DS file magic number

    static final int S3D_M3DMAGIC 		= 0x4d4d;


    // Tag IDs

    static final int S3D_MMAGIC 		= 0x3d3d;
    static final int S3D_MESH_VERSION	= 0x0001;
    static final int S3D_M3D_VERSION		= 0x0002;

    static final int S3D_COLOR_F		= 0x0010;
    static final int S3D_COLOR_24		= 0x0011;
    static final int S3D_INT_PERCENTAGE	= 0x0030;
    static final int S3D_FLOAT_PERCENTAGE	= 0x0031;

    static final int S3D_MASTER_SCALE	= 0x0100;

    static final int S3D_BIT_MAP		= 0x1100;
    static final int S3D_USE_BIT_MAP		= 0x1101;
    static final int S3D_SOLID_BGND		= 0x1200;
    static final int S3D_USE_SOLID_BGND	= 0x1201;
    static final int S3D_V_GRADIENT		= 0x1300;
    static final int S3D_USE_V_GRADIENT	= 0x1301;

    static final int S3D_LO_SHADOW_BIAS	= 0x1400;
    static final int S3D_HI_SHADOW_BIAS	= 0x1410;
    static final int S3D_SHADOW_MAP_SIZE	= 0x1420;
    static final int S3D_SHADOW_SAMPLES	= 0x1430;
    static final int S3D_SHADOW_RANGE	= 0x1440;

    static final int S3D_AMBIENT_LIGHT	= 0x2100;

    static final int S3D_FOG			= 0x2200;
    static final int S3D_USE_FOG		= 0x2201;
    static final int S3D_FOG_BGND		= 0x2210;
    static final int S3D_DISTANCE_CUE	= 0x2300;
    static final int S3D_USE_DISTANCE_CUE	= 0x2301;
    static final int S3D_DCUE_BGND		= 0x2310;

    static final int S3D_DEFAULT_VIEW	= 0x3000;
    static final int S3D_VIEW_TOP		= 0x3010;
    static final int S3D_VIEW_BOTTOM		= 0x3020;
    static final int S3D_VIEW_LEFT		= 0x3030;
    static final int S3D_VIEW_RIGHT		= 0x3040;
    static final int S3D_VIEW_FRONT		= 0x3050;
    static final int S3D_VIEW_BACK		= 0x3060;
    static final int S3D_VIEW_USER		= 0x3070;
    static final int S3D_VIEW_CAMERA		= 0x3080;
    static final int S3D_VIEW_WINDOW		= 0x3090;

    static final int S3D_NAMED_OBJECT	= 0x4000;
    static final int S3D_OBJ_HIDDEN		= 0x4010;
    static final int S3D_OBJ_VIS_LOFTER	= 0x4011;
    static final int S3D_OBJ_DOESNT_CAST	= 0x4012;
    static final int S3D_OBJ_MATTE		= 0x4013;

    static final int S3D_N_TRI_OBJECT	= 0x4100;

    static final int S3D_POINT_ARRAY		= 0x4110;
    static final int S3D_POINT_FLAG_ARRAY	= 0x4111;
    static final int S3D_FACE_ARRAY		= 0x4120;
    static final int S3D_MSH_MAT_GROUP	= 0x4130;
    static final int S3D_TEX_VERTS		= 0x4140;
    static final int S3D_SMOOTH_GROUP	= 0x4150;
    static final int S3D_MESH_MATRIX		= 0x4160;

    static final int S3D_N_DIRECT_LIGHT	= 0x4600;
    static final int S3D_DL_SPOTLIGHT	= 0x4610;
    static final int S3D_DL_OFF		= 0x4620;
    static final int S3D_DL_SHADOWED		= 0x4630;

    static final int S3D_N_CAMERA		= 0x4700;


    // Material file Chunk IDs

    static final int S3D_MAT_ENTRY		= 0xafff;
    static final int S3D_MAT_NAME		= 0xa000;
    static final int S3D_MAT_AMBIENT		= 0xa010;
    static final int S3D_MAT_DIFFUSE		= 0xa020;
    static final int S3D_MAT_SPECULAR	= 0xa030;
    static final int S3D_MAT_SHININESS	= 0xa040;
    static final int S3D_MAT_SHININESS_STRENGTH = 0xa041;
    static final int S3D_MAT_TRANSPARENCY	= 0xa050;
    static final int S3D_MAT_WIRE       = 0xa085;
    static final int S3D_MAT_WIRESIZE   = 0xa087;
    static final int S3D_MAT_SELF_ILLUM	= 0xa080;
    static final int S3D_MAT_TWO_SIDE	= 0xa081;
    static final int S3D_MAT_DECAL		= 0xa082;
    static final int S3D_MAT_ADDITIVE	= 0xa083;

    static final int S3D_MAT_SHADING		= 0xa100;


    static final int S3D_MAT_TEXMAP		= 0xa200;
    static final int S3D_MAT_OPACMAP		= 0xa210;
    static final int S3D_MAT_REFLMAP		= 0xa220;
    static final int S3D_MAT_BUMPMAP		= 0xa230;

    static final int S3D_MAT_MAPNAME		= 0xa300;


    // Reverse engineered hierarchy information

    static final int S3D_HIERARCHY		= 0xb000;
    static final int S3D_HIERARCHY_NODE	= 0xb002;
    static final int S3D_HIERARCHY_LINK	= 0xb010;
    static final int S3D_INSTANCE_NAME      = 0xb011;
    static final int S3D_PIVOT              = 0xb013;
    static final int S3D_POS_TRACK_TAG      = 0xb020;
    static final int S3D_ROT_TRACK_TAG      = 0xb021;
    static final int S3D_SCL_TRACK_TAG      = 0xb022;
    static final int S3D_NODE_ID	        = 0xb030;
    static final int S3D_OBJECT_LINK_NULL   = 0xffff;


    // Dummy Chunk ID

    static final int S3D_DUMMY_CHUNK		= 0xffff;


    // These chunks are found in the .PRJ file (only as far as I know)

    static final int S3D_PROJECT_FILE 	= 0xc23d;
    static final int S3D_MAPPING_RETILE	= 0xc4b0;
    static final int S3D_MAPPING_CENTRE	= 0xc4c0;
    static final int S3D_MAPPING_SCALE 	= 0xc4d0;
    static final int S3D_MAPPING_ORIENTATION	= 0xc4e1;
}
