// Filename: maxToEggConverter.cxx
// Created by Corey Revilla and Ken Strickland (6/22/03)
// from mayaToEggConverter.cxx created by drose (10Nov99)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) Carnegie Mellon University.  All rights reserved.
//
// All use of this software is subject to the terms of the revised BSD
// license.  You should have received a copy of this license along
// with this source code in a file named "LICENSE."
//
////////////////////////////////////////////////////////////////////

#include "maxEgg.h"

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::Constructor
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
MaxToEggConverter::
MaxToEggConverter()
{
    reset();
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::Destructor
//       Access: Public, Virtual
//  Description: 
////////////////////////////////////////////////////////////////////
MaxToEggConverter::
~MaxToEggConverter() 
{
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::reset
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::reset() {
    _cur_tref = 0;
    _current_frame = 0;
    _textures.clear();
    _egg_data = NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::convert
//       Access: Public
//  Description: Fills up the egg_data structure according to the
//               global Max model data.  Returns true if successful,
//               false if there is an error.  If from_selection is
//               true, the converted geometry is based on that which
//               is selected; otherwise, it is the entire Max scene.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::convert(MaxEggOptions *options) {

    _options = options;

    Filename fn = Filename::from_os_specific(_options->_file_name);
    _options->_path_replace->_path_directory = fn.get_dirname();

    _egg_data = new EggData;
    if (_egg_data->get_coordinate_system() == CS_default) {
        _egg_data->set_coordinate_system(CS_zup_right);
    }
    
    // Figure out the animation parameters.
    
    // Get the start and end frames and the animation frame rate from Max
    
    Interval anim_range = _options->_max_interface->GetAnimRange();
    int start_frame = anim_range.Start()/GetTicksPerFrame();
    int end_frame = anim_range.End()/GetTicksPerFrame();
    
    if (!_options->_export_all_frames) {
        if (_options->_start_frame < start_frame) _options->_start_frame = start_frame;
        if (_options->_start_frame > end_frame)   _options->_start_frame = end_frame;
        if (_options->_end_frame < start_frame)   _options->_end_frame = start_frame;
        if (_options->_end_frame > end_frame)     _options->_end_frame = end_frame;
        if (_options->_end_frame < _options->_start_frame)  _options->_end_frame = _options->_start_frame;
        start_frame = _options->_start_frame;
        end_frame = _options->_end_frame;
    }
    
    int frame_inc = 1;
    int output_frame_rate = GetFrameRate();

    bool all_ok = true;

    if (_options->_export_whole_scene) {
        all_ok = _tree.build_complete_hierarchy(_options->_max_interface->GetRootNode(), NULL, 0);
    } else {
        all_ok = _tree.build_complete_hierarchy(_options->_max_interface->GetRootNode(), &_options->_node_list.front(), _options->_node_list.size());
    }
    
    if (all_ok) {
        switch (_options->_anim_type) {
        case MaxEggOptions::AT_pose:
            // pose: set to a specific frame, then get out the static geometry.
            // sprintf(Logger::GetLogString(), "Extracting geometry from frame #%d.", start_frame); 
            // Logger::Log( MTEC, Logger::SAT_MEDIUM_LEVEL, Logger::GetLogString() );
            // Logger::Log( MTEC, Logger::SAT_MEDIUM_LEVEL, "Converting static model." );
            _current_frame = start_frame;
            all_ok = convert_hierarchy(_egg_data);
            break;
            
        case MaxEggOptions::AT_model:
            // model: get out an animatable model with joints and vertex
            // membership.
            all_ok = convert_char_model();
            break;
            
        case MaxEggOptions::AT_chan:
            // chan: get out a series of animation tables.
            all_ok = convert_char_chan(start_frame, end_frame, frame_inc,
                                       output_frame_rate);
            break;
            
        case MaxEggOptions::AT_both:
            // both: Put a model and its animation into the same egg file.
            _options->_anim_type = MaxEggOptions::AT_model;
            if (!convert_char_model()) {
                all_ok = false;
            }
            _options->_anim_type = MaxEggOptions::AT_chan;
            if (!convert_char_chan(start_frame, end_frame, frame_inc,
                                   output_frame_rate)) {
                all_ok = false;
            }
            // Set the type back to AT_both
            _options->_anim_type = MaxEggOptions::AT_both;
            break;
          
          default:
            all_ok = false;
        };
        
        reparent_decals(_egg_data);
    }
    
    if (all_ok) {
        _egg_data->recompute_tangent_binormal_auto();
        _egg_data->remove_unused_vertices(true);
    }
    
    _options->_successful = all_ok;
    
    if (all_ok) {
        Filename fn = Filename::from_os_specific(_options->_file_name);
        return _egg_data->write_egg(fn);
    } else {
        return false;
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::convert_char_model
//       Access: Private
//  Description: Converts the file as an animatable character
//               model, with joints and vertex membership.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::
convert_char_model() {
    std::string character_name = "character";
    _current_frame = _options->_start_frame;

    EggGroup *char_node = new EggGroup(character_name);
    _egg_data->add_child(char_node);
    char_node->set_dart_type(EggGroup::DT_default);
    
    return convert_hierarchy(char_node);
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::convert_char_chan
//       Access: Private
//  Description: Converts the animation as a series of tables to apply
//               to the character model, as retrieved earlier via
//               AC_model.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::
convert_char_chan(double start_frame, double end_frame, double frame_inc,
                  double output_frame_rate) {
    std::string character_name = "character";

    EggTable *root_table_node = new EggTable();
    _egg_data->add_child(root_table_node);
    EggTable *bundle_node = new EggTable(character_name);
    bundle_node->set_table_type(EggTable::TT_bundle);
    root_table_node->add_child(bundle_node);
    EggTable *skeleton_node = new EggTable("<skeleton>");
    bundle_node->add_child(skeleton_node);

    // Set the frame rate before we start asking for anim tables to be
    // created.
    _tree._fps = output_frame_rate / frame_inc;
    _tree.clear_egg(_egg_data, NULL, skeleton_node);
    
    // Now we can get the animation data by walking through all of the
    // frames, one at a time, and getting the joint angles at each
    // frame.
    
    // This is just a temporary EggGroup to receive the transform for
    // each joint each frame.
    EggGroup* tgroup;
    
    int num_nodes = _tree.get_num_nodes();
    int i;
    
    TimeValue frame = start_frame;
    TimeValue frame_stop = end_frame;
    while (frame <= frame_stop) {
        _current_frame = frame;
        for (i = 0; i < num_nodes; i++) {
            // Find all joints in the hierarchy
            MaxNodeDesc *node_desc = _tree.get_node(i);
            if (node_desc->is_joint()) {
                tgroup = new EggGroup();
                INode *max_node = node_desc->get_max_node();
                
                if (node_desc->_parent && node_desc->_parent->is_joint()) {
                    // If this joint also has a joint as a parent, the parent's 
                    // transformation has to be divided out of this joint's TM
                    get_joint_transform(max_node, node_desc->_parent->get_max_node(), 
                                        tgroup);
                } else {
                    get_joint_transform(max_node, NULL, tgroup);
                }
                
                EggXfmSAnim *anim = _tree.get_egg_anim(node_desc);
                if (!anim->add_data(tgroup->get_transform3d())) {
                    // *** log an error
                }
                delete tgroup;
            }
        }
        
        frame += frame_inc;
    }
    
    // Now optimize all of the tables we just filled up, for no real
    // good reason, except that it makes the resulting egg file a little
    // easier to read.
    for (i = 0; i < num_nodes; i++) {
        MaxNodeDesc *node_desc = _tree.get_node(i);
        if (node_desc->is_joint()) {
            _tree.get_egg_anim(node_desc)->optimize();
        }
    }
    
    return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::convert_hierarchy
//       Access: Private
//  Description: Generates egg structures for each node in the Max
//               hierarchy.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::
convert_hierarchy(EggGroupNode *egg_root) {
    //int num_nodes = _tree.get_num_nodes();
    
    _tree.clear_egg(_egg_data, egg_root, NULL);
    for (int i = 0; i < _tree.get_num_nodes(); i++) {
        if (!process_model_node(_tree.get_node(i))) {
            return false;
        }
    }
    
    return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::process_model_node
//       Access: Private
//  Description: Converts the indicated Max node to the
//               corresponding Egg structure.  Returns true if
//               successful, false if an error was encountered.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::
process_model_node(MaxNodeDesc *node_desc) {
    if (!node_desc->has_max_node()) {
        // If the node has no Max equivalent, never mind.
        return true;
    }

    // Skip all nodes that represent joints in the geometry, but aren't 
    // the actual joints themselves
    if (node_desc->is_node_joint()) {
        return true;
    }

    TimeValue time = 0;
    INode *max_node = node_desc->get_max_node();

    ObjectState state;
    state = max_node->EvalWorldState(_current_frame * GetTicksPerFrame());

    if (node_desc->is_joint()) {
        EggGroup *egg_group = _tree.get_egg_group(node_desc);
        // Don't bother with joints unless we're getting an animatable
        // model.
        if (_options->_anim_type == MaxEggOptions::AT_model) { 
            get_joint_transform(max_node, egg_group);
        }
    } else {
        if (state.obj) {
            EggGroup *egg_group = NULL;
            TriObject *myMaxTriObject;
            Mesh max_mesh;
            //Call the correct exporter based on what type of object this is.
            switch( state.obj->SuperClassID() ){

            case GEOMOBJECT_CLASS_ID:
                egg_group = _tree.get_egg_group(node_desc);
                get_transform(max_node, egg_group);
    
                //Try converting this geometric object to a mesh we can use.
                if (!state.obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID, 0))) {
                    return false;
                } 
                //Convert our state object to a TriObject.
                myMaxTriObject = (TriObject *) state.obj->ConvertToType(time, Class_ID(TRIOBJ_CLASS_ID, 0 ));
                // *** Want to figure this problem out 
                // If actual conversion was required, then we want to delete this 
                // new mesh later to avoid mem leaks. **BROKEN. doesnt delete
            
                //Now, get the mesh.
                max_mesh = myMaxTriObject->GetMesh();
                make_polyset(max_node, &max_mesh, egg_group);
            
                if (myMaxTriObject != state.obj)
                    delete myMaxTriObject;
                break;
            
            case SHAPE_CLASS_ID:
                if (state.obj->ClassID() == EDITABLE_SURF_CLASS_ID) {
                    NURBSSet getSet;
                    if (GetNURBSSet(state.obj, time, getSet, TRUE)) {
                        NURBSObject *nObj = getSet.GetNURBSObject(0);
                        if (nObj->GetType() == kNCVCurve) {
                            //It's a CV Curve, process it
                            egg_group = _tree.get_egg_group(node_desc);
                            get_transform(max_node, egg_group);
                            make_nurbs_curve((NURBSCVCurve *)nObj, string(max_node->GetName()),
                                             time, egg_group);
                        }
                    }
                }
                break;

            case CAMERA_CLASS_ID:
                break;
          
            case LIGHT_CLASS_ID:
                break;
          
            case HELPER_CLASS_ID:
                break;

            }
        }
    }
  
    return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::get_transform
//       Access: Private
//  Description: Extracts the transform on the indicated Maya node,
//               and applies it to the corresponding Egg node.
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::
get_transform(INode *max_node, EggGroup *egg_group) {
    if (_options->_anim_type == MaxEggOptions::AT_model) {
        // When we're getting an animated model, we only get transforms
        // for joints.
        return;
    }

    if ( !egg_group ) {
        return;
    }

    // Gets the TM for this node, a matrix which encapsulates all transformations
    // it takes to get to the current node, including parent transformations.
    Matrix3 pivot = max_node->GetNodeTM(_current_frame * GetTicksPerFrame());

    //This is the Panda-flava-flav-style matrix we'll be exporting to.
    Point3 row0 = pivot.GetRow(0);
    Point3 row1 = pivot.GetRow(1);
    Point3 row2 = pivot.GetRow(2);
    Point3 row3 = pivot.GetRow(3);
    
    LMatrix4d m4d(row0.x, row0.y, row0.z, 0.0f,
                  row1.x, row1.y, row1.z, 0.0f,
                  row2.x, row2.y, row2.z, 0.0f,
                  row3.x, row3.y, row3.z, 1.0f );

    // Now here's the tricky part. I believe this command strips out the node
    // "frame" which is the sum of all transformations enacted by the parent of
    // this node. This should reduce to the transformation relative to this 
    // node's parent
    m4d = m4d * egg_group->get_node_frame_inv();
    if (!m4d.almost_equal(LMatrix4d::ident_mat(), 0.0001)) {
        egg_group->add_matrix4(m4d);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::get_object_transform
//       Access: Private
//  Description: Extracts the transform on the indicated Maya node,
//               and applies it to the corresponding Egg node.
////////////////////////////////////////////////////////////////////
LMatrix4d MaxToEggConverter::
get_object_transform(INode *max_node) {

    // Gets the TM for this node, a matrix which encapsulates all transformations
    // it takes to get to the current node, including parent transformations.
    Matrix3 pivot = max_node->GetObjectTM(_current_frame * GetTicksPerFrame());

    Point3 row0 = pivot.GetRow(0);
    Point3 row1 = pivot.GetRow(1);
    Point3 row2 = pivot.GetRow(2);
    Point3 row3 = pivot.GetRow(3);
    
    LMatrix4d m4d(row0.x, row0.y, row0.z, 0.0f,
                  row1.x, row1.y, row1.z, 0.0f,
                  row2.x, row2.y, row2.z, 0.0f,
                  row3.x, row3.y, row3.z, 1.0f );
    return m4d;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::get_joint_transform
//       Access: Private
//  Description: Extracts the transform on the indicated Maya node,
//               as appropriate for a joint in an animated character,
//               and applies it to the indicated node.  This is
//               different from get_transform() in that it does not
//               respect the _transform_type flag, and it does not
//               consider the relative transforms within the egg file.
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::
get_joint_transform(INode *max_node, EggGroup *egg_group) {

    if ( !egg_group ) {
        return;
    }

    // Gets the TM for this node, a matrix which encapsulates all transformations
    // it takes to get to the current node, including parent transformations.
    Matrix3 pivot = max_node->GetNodeTM(_current_frame * GetTicksPerFrame());
    Point3 row0 = pivot.GetRow(0);
    Point3 row1 = pivot.GetRow(1);
    Point3 row2 = pivot.GetRow(2);
    Point3 row3 = pivot.GetRow(3);

    LMatrix4d m4d(row0.x, row0.y, row0.z, 0.0f,
                  row1.x, row1.y, row1.z, 0.0f,
                  row2.x, row2.y, row2.z, 0.0f,
                  row3.x, row3.y, row3.z, 1.0f );

    // Now here's the tricky part. I believe this command strips out the node
    // "frame" which is the sum of all transformations enacted by the parent of
    // this node. This should reduce to the transformation relative to this 
    // node's parent
    m4d = m4d * egg_group->get_node_frame_inv();
    if (!m4d.almost_equal(LMatrix4d::ident_mat(), 0.0001)) {
        egg_group->add_matrix4(m4d);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::get_joint_transform
//       Access: Private
//  Description: Extracts the transform on the indicated Maya node,
//               as appropriate for a joint in an animated character,
//               and applies it to the indicated node.  This is
//               different from get_transform() in that it does not
//               respect the _transform_type flag, and it does not
//               consider the relative transforms within the egg file.
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::
get_joint_transform(INode *max_node, INode *parent_node, EggGroup *egg_group) {

    if ( !egg_group ) {
        return;
    }

    // Gets the TM for this node, a matrix which encapsulates all transformations
    // it takes to get to the current node, including parent transformations.
    Matrix3 pivot = max_node->GetNodeTM(_current_frame * GetTicksPerFrame());
    Point3 row0 = pivot.GetRow(0);
    Point3 row1 = pivot.GetRow(1);
    Point3 row2 = pivot.GetRow(2);
    Point3 row3 = pivot.GetRow(3);

    LMatrix4d m4d(row0.x, row0.y, row0.z, 0.0f,
                  row1.x, row1.y, row1.z, 0.0f,
                  row2.x, row2.y, row2.z, 0.0f,
                  row3.x, row3.y, row3.z, 1.0f );

    if (parent_node) {
        Matrix3 parent_pivot = parent_node->GetNodeTM(_current_frame * GetTicksPerFrame());
        //  parent_pivot.Invert();
        row0 = parent_pivot.GetRow(0);
        row1 = parent_pivot.GetRow(1);
        row2 = parent_pivot.GetRow(2);
        row3 = parent_pivot.GetRow(3);

        LMatrix4d pi_m4d(row0.x, row0.y, row0.z, 0.0f,
                         row1.x, row1.y, row1.z, 0.0f,
                         row2.x, row2.y, row2.z, 0.0f,
                         row3.x, row3.y, row3.z, 1.0f );

        // Now here's the tricky part. I believe this command strips out the node
        // "frame" which is the sum of all transformations enacted by the parent of
        // this node. This should reduce to the transformation relative to this 
        // node's parent
        pi_m4d.invert_in_place();
        m4d = m4d * pi_m4d;
    }
    if (!m4d.almost_equal(LMatrix4d::ident_mat(), 0.0001)) {
        egg_group->add_matrix4(m4d);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::make_nurbs_curve
//       Access: Private
//  Description: Converts the indicated Maya NURBS curve (a standalone
//               curve, not a trim curve) to a corresponding egg
//               structure and attaches it to the indicated egg group.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::
make_nurbs_curve(NURBSCVCurve *curve, const string &name,
                 TimeValue time, EggGroup *egg_group) 
{
    int degree = curve->GetOrder();
    int cvs = curve->GetNumCVs();
    int knots = curve->GetNumKnots();
    int i;

    if (knots != cvs + degree) {
        return false;
    }

    string vpool_name = name + ".cvs";
    EggVertexPool *vpool = new EggVertexPool(vpool_name);
    egg_group->add_child(vpool);

    EggNurbsCurve *egg_curve = new EggNurbsCurve(name);
    egg_group->add_child(egg_curve);
    egg_curve->setup(degree, knots);

    for (i = 0; i < knots; i++)
        egg_curve->set_knot(i, curve->GetKnot(i));

    LMatrix4d vertex_frame_inv = egg_group->get_vertex_frame_inv();

    for (i = 0; i < cvs; i++) {
        NURBSControlVertex *cv = curve->GetCV(i);
        if (!cv) {
            char buf[1024];
            sprintf(buf, "Error getting CV %d", i);
            return false;
        } else {
            EggVertex vert;
            LPoint4d p4d(0, 0, 0, 1.0);
            cv->GetPosition(time, p4d[0], p4d[1], p4d[2]);
            p4d = p4d * vertex_frame_inv;
            vert.set_pos(p4d);
            egg_curve->add_vertex(vpool->create_unique_vertex(vert));
        }
    }

    return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::make_polyset
//       Access: Private
//  Description: Converts the indicated Maya polyset to a bunch of
//               EggPolygons and parents them to the indicated egg
//               group.
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::
make_polyset(INode *max_node, Mesh *mesh,
             EggGroup *egg_group, Shader *default_shader) {

    mesh->buildNormals();

    if (mesh->getNumFaces() == 0) {
        return;
    }

    // One way to convert the mesh would be to first get out all the
    // vertices in the mesh and add them into the vpool, then when we
    // traverse the polygons we would only have to index them into the
    // vpool according to their Maya vertex index.

    // Unfortunately, since Maya may store multiple normals and/or
    // colors for each vertex according to which polygon it is in, that
    // approach won't necessarily work.  In egg, those split-property
    // vertices have to become separate vertices.  So instead of adding
    // all the vertices up front, we'll start with an empty vpool, and
    // add vertices to it on the fly.

    string vpool_name = string(max_node->GetName()) + ".verts";
    EggVertexPool *vpool = new EggVertexPool(vpool_name);
    egg_group->add_child(vpool);

    // We will need to transform all vertices from world coordinate
    // space into the vertex space appropriate to this node.  Usually,
    // this is the same thing as world coordinate space, and this matrix
    // will be identity; but if the node is under an instance
    // (particularly, for instance, a billboard) then the vertex space
    // will be different from world space.
    LMatrix4d vertex_frame = get_object_transform(max_node) * 
        egg_group->get_vertex_frame_inv();


    for ( int iFace=0; iFace < mesh->getNumFaces(); iFace++ ) {
        EggPolygon *egg_poly = new EggPolygon;
        egg_group->add_child(egg_poly);

        egg_poly->set_bface_flag(_options->_double_sided);

        Face face = mesh->faces[iFace];

        const PandaMaterial &pmat = get_panda_material(max_node->GetMtl(), face.getMatID());

        // Get the vertices for the polygon.
        for ( int iVertex=0; iVertex < 3; iVertex++ ) {
            EggVertex vert;

            // Get the vertex position
            Point3 vertex = mesh->getVert(face.v[iVertex]);
            LPoint3d p3d(vertex.x, vertex.y, vertex.z);
            p3d = p3d * vertex_frame;
            vert.set_pos(p3d);

            // Get the vertex normal
            Point3 normal = get_max_vertex_normal(mesh, iFace, iVertex);
            LVector3d n3d(normal.x, normal.y, normal.z);
            // *** Not quite sure if this transform should be applied, but it may 
            //     explain why normals were weird previously
            n3d = n3d * vertex_frame;
            vert.set_normal(n3d);

            // Get the UVs for this vertex
            for (int iChan=0; iChan<pmat._map_channels.size(); iChan++) {
                int channel = pmat._map_channels[iChan];
                ostringstream uvname;
                uvname << "m" << channel;
                UVVert uvw = get_max_vertex_texcoord(mesh, iFace, iVertex, channel);
                vert.set_uv( uvname.str(), TexCoordd(uvw.x, uvw.y));
            }

            vert.set_external_index(face.v[iVertex]);

            egg_poly->add_vertex(vpool->create_unique_vertex(vert));
        }

        //Max uses normals, not winding, to determine which way a 
        //polygon faces. Make sure the winding and that normal agree
        
        EggVertex *verts[3];
        LPoint3d points[3];
        
        for (int i = 0; i < 3; i++) {
            verts[i] = egg_poly->get_vertex(i);
            points[i] = verts[i]->get_pos3();
        }

        LVector3d realNorm = ((points[1] - points[0]).cross(points[2] - points[0]));
        Point3 maxNormTemp = mesh->getFaceNormal(iFace);
        LVector3d maxNorm = (LVector3d(maxNormTemp.x, maxNormTemp.y, maxNormTemp.z) *
                             vertex_frame);

        if (realNorm.dot(maxNorm) < 0.0) {
            egg_poly->set_vertex(0, verts[2]);
            egg_poly->set_vertex(2, verts[0]);
        }

        for (int i=0; i<pmat._texture_list.size(); i++) {
            egg_poly->add_texture(pmat._texture_list[i]);
        }
        egg_poly->set_color(pmat._color);
    }
   
    // Now that we've added all the polygons (and created all the
    // vertices), go back through the vertex pool and set up the
    // appropriate joint membership for each of the vertices.

    if (_options->_anim_type == MaxEggOptions::AT_model) {
        get_vertex_weights(max_node, vpool);
    }
}

UVVert MaxToEggConverter::get_max_vertex_texcoord(Mesh *mesh, int faceNo, int vertNo, int channel) {

    // extract the texture coordinate
    UVVert uvVert(0,0,0);
    if(mesh->mapSupport(channel)) {
        TVFace *pTVFace = mesh->mapFaces(channel);
        UVVert *pUVVert = mesh->mapVerts(channel);
        uvVert = pUVVert[pTVFace[faceNo].t[vertNo]];
    } else if(mesh->numTVerts > 0) {
        uvVert = mesh->tVerts[mesh->tvFace[faceNo].t[vertNo]];
    }
    return uvVert;
}
    
Point3 MaxToEggConverter::get_max_vertex_normal(Mesh *mesh, int faceNo, int vertNo)
{
    Face f = mesh->faces[faceNo];
    DWORD smGroup = f.smGroup;
    int vert = f.getVert(vertNo);
    RVertex *rv = mesh->getRVertPtr(vert);
  
    int numNormals;
    Point3 vertexNormal;

    // Is normal specified
    // SPCIFIED is not currently used, but may be used in future versions.
    if (rv->rFlags & SPECIFIED_NORMAL) {
        vertexNormal = rv->rn.getNormal();
    }
    // If normal is not specified it's only available if the face belongs
    // to a smoothing group
    else if ((numNormals = rv->rFlags & NORCT_MASK) && smGroup) {
        // If there is only one vertex is found in the rn member.
        if (numNormals == 1) {
            vertexNormal = rv->rn.getNormal();
        }
        else {
            // If two or more vertices are there you need to step through them
            // and find the vertex with the same smoothing group as the current face.
            // You will find multiple normals in the ern member.
            for (int i = 0; i < numNormals; i++) {
                if (rv->ern[i].getSmGroup() & smGroup) {
                    vertexNormal = rv->ern[i].getNormal();
                }
            }
        }
    }
    else {
        // Get the normal from the Face if no smoothing groups are there
        vertexNormal = mesh->getFaceNormal(faceNo);
    }
  
    return vertexNormal;
}

////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::get_vertex_weights
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::
get_vertex_weights(INode *max_node, EggVertexPool *vpool) {
    //Try to get the weights out of a physique if one exists
    Modifier *mod = FindSkinModifier(max_node, PHYSIQUE_CLASSID);
    EggVertexPool::iterator vi;

    if (mod) {
        // create a physique export interface
        IPhysiqueExport *pPhysiqueExport = (IPhysiqueExport *)mod->GetInterface(I_PHYINTERFACE);
        if (pPhysiqueExport) {
            // create a context export interface
            IPhyContextExport *pContextExport = 
                (IPhyContextExport *)pPhysiqueExport->GetContextInterface(max_node);
            if (pContextExport) {
                // set the flags in the context export interface
                pContextExport->ConvertToRigid(TRUE);
                pContextExport->AllowBlending(TRUE);
  
                for (vi = vpool->begin(); vi != vpool->end(); ++vi) {
                    EggVertex *vert = (*vi);
                    int max_vi = vert->get_external_index();

                    // get the vertex export interface
                    IPhyVertexExport *pVertexExport = 
                        (IPhyVertexExport *)pContextExport->GetVertexInterface(max_vi);
                    if (pVertexExport) {
                        int vertexType = pVertexExport->GetVertexType();

                        // handle the specific vertex type
                        if(vertexType == RIGID_TYPE) {
                            // typecast to rigid vertex
                            IPhyRigidVertex *pTypeVertex = (IPhyRigidVertex *)pVertexExport;
                            INode *bone_node = pTypeVertex->GetNode();
                            MaxNodeDesc *joint_node_desc = _tree.find_joint(bone_node);
                            if (joint_node_desc){
                                EggGroup *joint = _tree.get_egg_group(joint_node_desc);
                                if (joint != (EggGroup *)NULL)
                                    joint->ref_vertex(vert, 1.0f);
                            }
                        }
                        else if(vertexType == RIGID_BLENDED_TYPE) {
                            // typecast to blended vertex
                            IPhyBlendedRigidVertex *pTypeVertex = (IPhyBlendedRigidVertex *)pVertexExport;

                            for (int ji = 0; ji < pTypeVertex->GetNumberNodes(); ++ji) {
                                float weight = pTypeVertex->GetWeight(ji);
                                if (weight > 0.0f) {
                                    INode *bone_node = pTypeVertex->GetNode(ji);
                                    MaxNodeDesc *joint_node_desc = _tree.find_joint(bone_node);
                                    if (joint_node_desc){
                                        EggGroup *joint = _tree.get_egg_group(joint_node_desc);
                                        if (joint != (EggGroup *)NULL)
                                            joint->ref_vertex(vert, weight);
                                    }
                                }
                            }
                        }
                        //Release the vertex interface
                        pContextExport->ReleaseVertexInterface(pVertexExport);
                    }
                }
                //Release the context interface
                pPhysiqueExport->ReleaseContextInterface(pContextExport);
            }
            //Release the physique export interface
            mod->ReleaseInterface(I_PHYINTERFACE, pPhysiqueExport);
        }
    }
    else {
        //No physique, try to find a skin
        mod = FindSkinModifier(max_node, SKIN_CLASSID);
        if (mod) {
            ISkin *skin = (ISkin*)mod->GetInterface(I_SKIN);
            if (skin) {
                ISkinContextData *skinMC = skin->GetContextInterface(max_node);
                if (skinMC) {
                    for (vi = vpool->begin(); vi != vpool->end(); ++vi) {
                        EggVertex *vert = (*vi);
                        int max_vi = vert->get_external_index();
  
                        for (int ji = 0; ji < skinMC->GetNumAssignedBones(max_vi); ++ji) {
                            float weight = skinMC->GetBoneWeight(max_vi, ji);
                            if (weight > 0.0f) {
                                INode *bone_node = skin->GetBone(skinMC->GetAssignedBone(max_vi, ji));
                                MaxNodeDesc *joint_node_desc = _tree.find_joint(bone_node);
                                if (joint_node_desc){
                                    EggGroup *joint = _tree.get_egg_group(joint_node_desc);
                                    if (joint != (EggGroup *)NULL) {
                                        joint->ref_vertex(vert, weight);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}


////////////////////////////////////////////////////////////////////
//     Function: MaxToEggConverter::get_material_textures
//       Access: Private
//  Description: Converts a Max material into a set of Panda textures
//               and a primitive color.
////////////////////////////////////////////////////////////////////
const MaxToEggConverter::PandaMaterial &MaxToEggConverter::
get_panda_material(Mtl *mtl, MtlID matID) {

    MaterialMap::iterator it = _material_map.find(mtl);
    if (it != _material_map.end()) {
        return (*it).second;
    }
    
    PandaMaterial &pandaMat = _material_map[mtl];
    pandaMat._color = Colorf(1,1,1,1);
    pandaMat._any_diffuse = false;
    pandaMat._any_opacity = false;
    pandaMat._any_gloss = false;
    pandaMat._any_normal = false;

    // If it's a multi-material, dig down.
        
    while (( mtl != 0) && (mtl->ClassID() == Class_ID(MULTI_CLASS_ID, 0 ))) {
        if (matID < mtl->NumSubMtls()) {
            mtl = mtl->GetSubMtl(matID);
        } else {
            mtl = 0;
        }
    }

    // If it's a standard material, we're good.
    
    if ((mtl != 0) && (mtl->ClassID() == Class_ID(DMTL_CLASS_ID, 0 ))) {
        StdMat *maxMaterial = (StdMat*)mtl;
        analyze_diffuse_maps(pandaMat, maxMaterial->GetSubTexmap(ID_DI));
        analyze_opacity_maps(pandaMat, maxMaterial->GetSubTexmap(ID_OP));
        analyze_gloss_maps(pandaMat, maxMaterial->GetSubTexmap(ID_SP));
        if (!pandaMat._any_gloss)
            analyze_gloss_maps(pandaMat, maxMaterial->GetSubTexmap(ID_SS));
        if (!pandaMat._any_gloss)
            analyze_gloss_maps(pandaMat, maxMaterial->GetSubTexmap(ID_SH));
        analyze_glow_maps(pandaMat, maxMaterial->GetSubTexmap(ID_SI));
        analyze_normal_maps(pandaMat, maxMaterial->GetSubTexmap(ID_BU));
        for (int i=0; i<pandaMat._texture_list.size(); i++) {
            EggTexture *src = pandaMat._texture_list[i];
            pandaMat._texture_list[i] =
                _textures.create_unique_texture(*src, ~EggTexture::E_tref_name);
        }

        // The existence of a texture on either color channel completely
        // replaces the corresponding flat color.
        if (!pandaMat._any_diffuse) {
            // Get the default diffuse color of the material without the texture map
            Point3 diffuseColor = Point3(maxMaterial->GetDiffuse(0));
            pandaMat._color[0] = diffuseColor.x;
            pandaMat._color[1] = diffuseColor.y;
            pandaMat._color[2] = diffuseColor.z;
        }
        if (!pandaMat._any_opacity) {
            // *** Figure out how to actually get the opacity here
            pandaMat._color[3] = maxMaterial->GetOpacity(_current_frame * GetTicksPerFrame());
        }
        return pandaMat;
    }

    // Otherwise, it's unrecognizable. Leave result blank.
    return pandaMat;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::analyze_diffuse_maps
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::analyze_diffuse_maps(PandaMaterial &pandaMat, Texmap *mat) {
    if (mat == 0) return;
    
    if (mat->ClassID() == Class_ID(RGBMULT_CLASS_ID, 0)) {
        for (int i=0; i<mat->NumSubTexmaps(); i++) {
            analyze_diffuse_maps(pandaMat, mat->GetSubTexmap(i));
        }
        return;
    }

    if (mat->ClassID() == Class_ID(BMTEX_CLASS_ID, 0)) {
        pandaMat._any_diffuse = true;
        PT(EggTexture) tex = new EggTexture(generate_tex_name(), "");

        BitmapTex *diffuseTex = (BitmapTex *)mat;

        Filename fullpath, outpath;
        Filename filename = Filename::from_os_specific(diffuseTex->GetMapName());
        _options->_path_replace->full_convert_path(filename, get_model_path(),
                                                   fullpath, outpath);
        tex->set_filename(outpath);
        tex->set_fullpath(fullpath);

        apply_texture_properties(*tex, diffuseTex->GetMapChannel());
        add_map_channel(pandaMat, diffuseTex->GetMapChannel());
        
        Bitmap *diffuseBitmap = diffuseTex->GetBitmap(0);
        if ( diffuseBitmap && diffuseBitmap->HasAlpha()) {
            tex->set_format(EggTexture::F_rgba);
        } else {
            tex->set_format(EggTexture::F_rgb);
        }
        tex->set_env_type(EggTexture::ET_modulate);
        
        pandaMat._texture_list.push_back(tex);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::analyze_opacity_maps
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::analyze_opacity_maps(PandaMaterial &pandaMat, Texmap *mat) {
    if (mat == 0) return;
    
    if (mat->ClassID() == Class_ID(RGBMULT_CLASS_ID, 0)) {
        for (int i=0; i<mat->NumSubTexmaps(); i++) {
            analyze_opacity_maps(pandaMat, mat->GetSubTexmap(i));
        }
        return;
    }

    if (mat->ClassID() == Class_ID(BMTEX_CLASS_ID, 0)) {
        pandaMat._any_opacity = true;
        BitmapTex *transTex = (BitmapTex *)mat;

        Filename fullpath, outpath;
        Filename filename = Filename::from_os_specific(transTex->GetMapName());
        _options->_path_replace->full_convert_path(filename, get_model_path(),
                                                   fullpath, outpath);

        // See if this opacity map already showed up.
        for (int i=0; i<pandaMat._texture_list.size(); i++) {
            EggTexture *tex = pandaMat._texture_list[i];
            if ((tex->get_env_type()==EggTexture::ET_modulate)&&(tex->get_fullpath() == fullpath)) {
                tex->set_format(EggTexture::F_rgba);
                return;
            }
        }
        
        // Try to find a diffuse map to pair this with as an alpha-texture.
        std::string uvname = get_uv_name(transTex->GetMapChannel());
        for (int i=0; i<pandaMat._texture_list.size(); i++) {
            EggTexture *tex = pandaMat._texture_list[i];
            if ((tex->get_env_type()==EggTexture::ET_modulate)&&
                (tex->get_format() == EggTexture::F_rgb)&&
                (tex->get_uv_name() == uvname)) {
                tex->set_format(EggTexture::F_rgba);
                tex->set_alpha_filename(outpath);
                tex->set_alpha_fullpath(fullpath);
                return;
            }
        }
        
        // Otherwise, just create it as an alpha-texture.
        PT(EggTexture) tex = new EggTexture(generate_tex_name(), "");
        tex->set_filename(outpath);
        tex->set_fullpath(fullpath);

        apply_texture_properties(*tex, transTex->GetMapChannel());
        add_map_channel(pandaMat, transTex->GetMapChannel());
        tex->set_format(EggTexture::F_alpha);

        pandaMat._texture_list.push_back(tex);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::analyze_glow_maps
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::analyze_glow_maps(PandaMaterial &pandaMat, Texmap *mat) {
    if (mat == 0) return;
    
    if (mat->ClassID() == Class_ID(BMTEX_CLASS_ID, 0)) {
        BitmapTex *gtex = (BitmapTex *)mat;

        Filename fullpath, outpath;
        Filename filename = Filename::from_os_specific(gtex->GetMapName());
        _options->_path_replace->full_convert_path(filename, get_model_path(),
                                                   fullpath, outpath);

        // Try to find a diffuse map to pair this with as an alpha-texture.
        std::string uvname = get_uv_name(gtex->GetMapChannel());
        for (int i=0; i<pandaMat._texture_list.size(); i++) {
            EggTexture *tex = pandaMat._texture_list[i];
            if ((tex->get_env_type()==EggTexture::ET_modulate)&&
                (tex->get_format() == EggTexture::F_rgb)&&
                (tex->get_uv_name() == uvname)) {
                tex->set_env_type(EggTexture::ET_modulate_glow);
                tex->set_format(EggTexture::F_rgba);
                tex->set_alpha_filename(outpath);
                tex->set_alpha_fullpath(fullpath);
                return;
            }
        }
        
        // Otherwise, just create it as a separate glow-texture.
        PT(EggTexture) tex = new EggTexture(generate_tex_name(), "");
        tex->set_env_type(EggTexture::ET_glow);
        tex->set_filename(outpath);
        tex->set_fullpath(fullpath);
        apply_texture_properties(*tex, gtex->GetMapChannel());
        add_map_channel(pandaMat, gtex->GetMapChannel());
        tex->set_format(EggTexture::F_alpha);
        
        pandaMat._texture_list.push_back(tex);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::analyze_gloss_maps
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::analyze_gloss_maps(PandaMaterial &pandaMat, Texmap *mat) {
    if (mat == 0) return;
    
    if (mat->ClassID() == Class_ID(BMTEX_CLASS_ID, 0)) {
        pandaMat._any_gloss = true;
        BitmapTex *gtex = (BitmapTex *)mat;

        Filename fullpath, outpath;
        Filename filename = Filename::from_os_specific(gtex->GetMapName());
        _options->_path_replace->full_convert_path(filename, get_model_path(),
                                                   fullpath, outpath);

        // Try to find a diffuse map to pair this with as an alpha-texture.
        std::string uvname = get_uv_name(gtex->GetMapChannel());
        for (int i=0; i<pandaMat._texture_list.size(); i++) {
            EggTexture *tex = pandaMat._texture_list[i];
            if ((tex->get_env_type()==EggTexture::ET_modulate)&&
                (tex->get_format() == EggTexture::F_rgb)&&
                (tex->get_uv_name() == uvname)) {
                tex->set_env_type(EggTexture::ET_modulate_gloss);
                tex->set_format(EggTexture::F_rgba);
                tex->set_alpha_filename(outpath);
                tex->set_alpha_fullpath(fullpath);
                return;
            }
        }
        
        // Otherwise, just create it as a separate gloss-texture.
        PT(EggTexture) tex = new EggTexture(generate_tex_name(), "");
        tex->set_env_type(EggTexture::ET_gloss);
        tex->set_filename(outpath);
        tex->set_fullpath(fullpath);
        apply_texture_properties(*tex, gtex->GetMapChannel());
        add_map_channel(pandaMat, gtex->GetMapChannel());
        tex->set_format(EggTexture::F_alpha);
        
        pandaMat._texture_list.push_back(tex);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::analyze_normal_maps
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::analyze_normal_maps(PandaMaterial &pandaMat, Texmap *mat) {
    if (mat == 0) return;
    
    if (mat->ClassID() == Class_ID(BMTEX_CLASS_ID, 0)) {
        pandaMat._any_normal = true;
        BitmapTex *ntex = (BitmapTex *)mat;

        Filename fullpath, outpath;
        Filename filename = Filename::from_os_specific(ntex->GetMapName());
        _options->_path_replace->full_convert_path(filename, get_model_path(),
                                                   fullpath, outpath);

        PT(EggTexture) tex = new EggTexture(generate_tex_name(), "");
        tex->set_env_type(EggTexture::ET_normal);
        tex->set_filename(outpath);
        tex->set_fullpath(fullpath);
        apply_texture_properties(*tex, ntex->GetMapChannel());
        add_map_channel(pandaMat, ntex->GetMapChannel());
        tex->set_format(EggTexture::F_rgb);
        
        pandaMat._texture_list.push_back(tex);
    }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::add_map_channel
//       Access: Private
//  Description: Adds the specified map channel to the map channel
//               list, if it's not already there.
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::add_map_channel(PandaMaterial &pandaMat, int chan) {
    for (int i=0; i<pandaMat._map_channels.size(); i++) {
        if (pandaMat._map_channels[i] == chan) {
            return;
        }
    }
    pandaMat._map_channels.push_back(chan);
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::generate_tex_name
//       Access: Private
//  Description: Generates an arbitrary unused texture name.
////////////////////////////////////////////////////////////////////
std::string MaxToEggConverter::generate_tex_name() {
    ostringstream name_strm;
    name_strm << "Tex" << ++_cur_tref;
    return name_strm.str();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::get_uv_name
//       Access: Private
//  Description: Returns the UV-name of the nth map-channel.
////////////////////////////////////////////////////////////////////
std::string MaxToEggConverter::get_uv_name(int channel) {
    ostringstream uvname;
    uvname << "m" << channel;
    return uvname.str();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::apply_texture_properties
//       Access: Private
//  Description: Applies all the appropriate texture properties to the
//               EggTexture object, including wrap modes and texture
//               matrix.
////////////////////////////////////////////////////////////////////
void MaxToEggConverter::
apply_texture_properties(EggTexture &tex, int channel) {

    tex.set_uv_name(get_uv_name(channel));

    tex.set_minfilter(EggTexture::FT_linear_mipmap_linear);
    tex.set_magfilter(EggTexture::FT_linear);

    EggTexture::WrapMode wrap_u = EggTexture::WM_repeat;
    EggTexture::WrapMode wrap_v = EggTexture::WM_repeat;
  
    tex.set_wrap_u(wrap_u);
    tex.set_wrap_v(wrap_v);
}


////////////////////////////////////////////////////////////////////
//     Function: MayaShader::reparent_decals
//       Access: Private
//  Description: Recursively walks the egg hierarchy, reparenting
//               "decal" type nodes below their corresponding
//               "decalbase" type nodes, and setting the flags.
//
//               Returns true on success, false if some nodes were
//               incorrect.
////////////////////////////////////////////////////////////////////
bool MaxToEggConverter::
reparent_decals(EggGroupNode *egg_parent) {
    bool okflag = true;

    // First, walk through all children of this node, looking for the
    // one decal base, if any.
    EggGroup *decal_base = (EggGroup *)NULL;
    pvector<EggGroup *> decal_children;

    EggGroupNode::iterator ci;
    for (ci = egg_parent->begin(); ci != egg_parent->end(); ++ci) {
        EggNode *child =  (*ci);
        if (child->is_of_type(EggGroup::get_class_type())) {
            EggGroup *child_group = (EggGroup *) child;
            if (child_group->has_object_type("decalbase")) {
                if (decal_base != (EggNode *)NULL) {
                    // error
                    okflag = false;
                }
                child_group->remove_object_type("decalbase");
                decal_base = child_group;

            } else if (child_group->has_object_type("decal")) {
                child_group->remove_object_type("decal");
                decal_children.push_back(child_group);
            }
        }
    }

    if (decal_base == (EggGroup *)NULL) {
        if (!decal_children.empty()) {
            // warning
        }

    } else {
        if (decal_children.empty()) {
            // warning

        } else {
            // All the decal children get moved to be a child of decal base.
            // This usually will not affect the vertex positions, but it
            // could if the decal base has a transform and the decal child
            // is an instance node.  So don't do that.
            pvector<EggGroup *>::iterator di;
            for (di = decal_children.begin(); di != decal_children.end(); ++di) {
                EggGroup *child_group = (*di);
                decal_base->add_child(child_group);
            }

            // Also set the decal state on the base.
            decal_base->set_decal_flag(true);
        }
    }

    // Now recurse on each of the child nodes.
    for (ci = egg_parent->begin(); ci != egg_parent->end(); ++ci) {
        EggNode *child =  (*ci);
        if (child->is_of_type(EggGroupNode::get_class_type())) {
            EggGroupNode *child_group = (EggGroupNode *) child;
            if (!reparent_decals(child_group)) {
                okflag = false;
            }
        }
    }

    return okflag;
}

Modifier* MaxToEggConverter::FindSkinModifier (INode* node, const Class_ID &type)
{
    // Get object from node. Abort if no object.
    Object* pObj = node->GetObjectRef();
    if (!pObj) return NULL;

    // Is derived object ?
    while (pObj->SuperClassID() == GEN_DERIVOB_CLASS_ID) {
        // Yes -> Cast.
        IDerivedObject* pDerObj = static_cast<IDerivedObject*>(pObj);

        // Iterate over all entries of the modifier stack.
        for (int stackId = 0; stackId < pDerObj->NumModifiers(); ++stackId) {
            // Get current modifier.
            Modifier* mod = pDerObj->GetModifier(stackId);
            
            // Is this what we are looking for?
            if (mod->ClassID() == type )
                return mod;
        }

        // continue with next derived object
        pObj = pDerObj->GetObjRef();
    }

    // Not found.
    return NULL;
}
