// Filename: mayaToEggConverter.cxx
// Created by:  drose (10Nov99)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc.  All rights reserved
//
// All use of this software is subject to the terms of the Panda 3d
// Software license.  You should have received a copy of this license
// along with this source code; you will also find a current copy of
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
//
// To contact the maintainers of this program write to
// panda3d-general@lists.sourceforge.net .
//
////////////////////////////////////////////////////////////////////

#include "mayaToEggConverter.h"
#include "mayaShader.h"
#include "maya_funcs.h"
#include "config_mayaegg.h"
#include "mayaEggGroupUserData.h"

#include "eggData.h"
#include "eggGroup.h"
#include "eggTable.h"
#include "eggVertex.h"
#include "eggVertexPool.h"
#include "eggNurbsSurface.h"
#include "eggNurbsCurve.h"
#include "eggPolygon.h"
#include "eggPrimitive.h"
#include "eggTexture.h"
#include "eggTextureCollection.h"
#include "eggXfmSAnim.h"
#include "eggSAnimData.h"
#include "string_utils.h"
#include "dcast.h"

#include "pre_maya_include.h"
#include <maya/MArgList.h>
#include <maya/MColor.h>
#include <maya/MDagPath.h>
#include <maya/MFnCamera.h>
#include <maya/MFnDagNode.h>
#include <maya/MFnTransform.h>
#include <maya/MFnLight.h>
#include <maya/MFnNurbsSurface.h>
#include <maya/MFnNurbsCurve.h>
#include <maya/MFnMesh.h>
#include <maya/MFnMeshData.h>
#include <maya/MFnPlugin.h>
#include <maya/MItDag.h>
#include <maya/MLibrary.h>
#include <maya/MMatrix.h>
#include <maya/MObject.h>
#include <maya/MPoint.h>
#include <maya/MPointArray.h>
#include <maya/MDoubleArray.h>
#include <maya/MIntArray.h>
#include <maya/MPxCommand.h>
#include <maya/MStatus.h>
#include <maya/MString.h>
#include <maya/MTransformationMatrix.h>
#include <maya/MVector.h>
#include <maya/MTesselationParams.h>
#include <maya/MAnimControl.h>
#include <maya/MGlobal.h>
#include <maya/MAnimUtil.h>
#include <maya/MFnSkinCluster.h>
#include <maya/MFnSingleIndexedComponent.h>
#include <maya/MFnDoubleIndexedComponent.h>
#include <maya/MFnBlendShapeDeformer.h>
#include <maya/MItDependencyGraph.h>
#include <maya/MDagPathArray.h>
#include <maya/MSelectionList.h>
#include "post_maya_include.h"

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::Constructor
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
MayaToEggConverter::
MayaToEggConverter(const string &program_name) :
  _program_name(program_name),
  _tree(this)
{
  // Make sure the library is properly initialized.
  init_libmayaegg();

  _from_selection = false;

  _polygon_output = false;
  _polygon_tolerance = 0.01;
  _respect_maya_double_sided = maya_default_double_sided;
  _always_show_vertex_color = maya_default_vertex_color;
  _transform_type = TT_model;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::Copy Constructor
//       Access: Public
//  Description: 
////////////////////////////////////////////////////////////////////
MayaToEggConverter::
MayaToEggConverter(const MayaToEggConverter &copy) :
  _program_name(copy._program_name),
  _from_selection(copy._from_selection),
  _subsets(copy._subsets),
  _ignore_sliders(copy._ignore_sliders),
  _force_joints(copy._force_joints),
  _tree(this),
  _maya(copy._maya),
  _polygon_output(copy._polygon_output),
  _polygon_tolerance(copy._polygon_tolerance),
  _respect_maya_double_sided(copy._respect_maya_double_sided),
  _always_show_vertex_color(copy._always_show_vertex_color),
  _transform_type(copy._transform_type)
{
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::Destructor
//       Access: Public, Virtual
//  Description: 
////////////////////////////////////////////////////////////////////
MayaToEggConverter::
~MayaToEggConverter() {
  close_api();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::make_copy
//       Access: Public, Virtual
//  Description: Allocates and returns a new copy of the converter.
////////////////////////////////////////////////////////////////////
SomethingToEggConverter *MayaToEggConverter::
make_copy() {
  return new MayaToEggConverter(*this);
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_name
//       Access: Public, Virtual
//  Description: Returns the English name of the file type this
//               converter supports.
////////////////////////////////////////////////////////////////////
string MayaToEggConverter::
get_name() const {
  return "Maya";
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_extension
//       Access: Public, Virtual
//  Description: Returns the common extension of the file type this
//               converter supports.
////////////////////////////////////////////////////////////////////
string MayaToEggConverter::
get_extension() const {
  return "mb";
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_additional_extensions
//       Access: Public, Virtual
//  Description: Returns a space-separated list of extension, in
//               addition to the one returned by get_extension(), that
//               are recognized by this converter.
////////////////////////////////////////////////////////////////////
string MayaToEggConverter::
get_additional_extensions() const {
  return "ma";
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::convert_file
//       Access: Public, Virtual
//  Description: Handles the reading of the input file and converting
//               it to egg.  Returns true if successful, false
//               otherwise.
//
//               This is designed to be as generic as possible,
//               generally in support of run-time loading.
//               Also see convert_maya().
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
convert_file(const Filename &filename) {
  if (!open_api()) {
    mayaegg_cat.error()
      << "Maya is not available.\n";
    return false;
  }
  
  // We must ensure our Maya pointers are cleared before we reset the
  // Maya scene, because resetting the Maya scene will invalidate all
  // the Maya pointers we are holding and cause a crash if we try to
  // free them later.
  clear();

  if (!_maya->read(filename)) {
    mayaegg_cat.error()
      << "Unable to read " << filename << "\n";
    return false;
  }

  if (_character_name.empty()) {
    _character_name = filename.get_basename_wo_extension();
  }

  return convert_maya();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::clear_subsets
//       Access: Public
//  Description: Empties the list of subset nodes added via
//               add_subset().  The entire file will once again be
//               converted.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
clear_subsets() {
  _subsets.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::add_subset
//       Access: Public
//  Description: Adds a name pattern to the list of subset nodes.  If
//               the list of subset nodes is not empty, then only a
//               subset of the nodes in the maya file will be
//               converted: those whose names match one of the
//               patterns given on this list.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
add_subset(const GlobPattern &glob) {
  _subsets.push_back(glob);
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::clear_ignore_sliders
//       Access: Public
//  Description: Empties the list of ignore_sliders added via
//               add_ignore_slider().  No sliders will be ignored.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
clear_ignore_sliders() {
  _ignore_sliders.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::add_ignore_slider
//       Access: Public
//  Description: Adds a name pattern to the list of ignore_sliders.
//               Any slider (blend shape deformer) that matches a name
//               on the list will not be converted or otherwise
//               molested by the converter.  This is occasionally
//               necessary to filter out automatically-created sliders
//               that are not intended to be used directly, but
//               instead have an indirect effect on other sliders.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
add_ignore_slider(const GlobPattern &glob) {
  _ignore_sliders.push_back(glob);
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::ignore_slider
//       Access: Public
//  Description: Returns true if the indicated name is on the list of
//               sliders to ignore, false otherwise.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
ignore_slider(const string &name) const {
  Globs::const_iterator gi;
  for (gi = _ignore_sliders.begin(); gi != _ignore_sliders.end(); ++gi) {
    if ((*gi).matches(name)) {
      return true;
    }
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::clear_force_joints
//       Access: Public
//  Description: Empties the list of force_joints added via
//               add_force_joint().  No joints will be forced.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
clear_force_joints() {
  _force_joints.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::add_force_joint
//       Access: Public
//  Description: Adds a name pattern to the list of force_joints.
//
//               Any DAG node that matches a name on the list will be
//               treated as if it were a joint during the conversion
//               process; it will receive animation and position
//               information.  Normally, a true Maya joint, as well as
//               any DAG nodes whose transforms are animated, will
//               automatically be flagged as a Panda joint.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
add_force_joint(const GlobPattern &glob) {
  _force_joints.push_back(glob);
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::force_joint
//       Access: Public
//  Description: Returns true if the indicated name is on the list of
//               DAG nodes to treat as a joint, false otherwise.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
force_joint(const string &name) const {
  Globs::const_iterator gi;
  for (gi = _force_joints.begin(); gi != _force_joints.end(); ++gi) {
    if ((*gi).matches(name)) {
      return true;
    }
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::set_from_selection
//       Access: Public
//  Description: Sets the flag that indicates whether the currently
//               selected Maya geometry will be converted.  If this is
//               true, and the selection is nonempty, then only the
//               selected geometry will be converted.  If this is
//               false, the entire file will be converted.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
set_from_selection(bool from_selection) {
  _from_selection = from_selection;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_input_units
//       Access: Public, Virtual
//  Description: This may be called after convert_file() has been
//               called and returned true, indicating a successful
//               conversion.  It will return the distance units
//               represented by the converted egg file, if known, or
//               DU_invalid if not known.
////////////////////////////////////////////////////////////////////
DistanceUnit MayaToEggConverter::
get_input_units() {
  return _maya->get_units();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::convert_maya
//       Access: Public
//  Description: Fills up the egg_data structure according to the
//               global maya model data.  Returns true if successful,
//               false if there is an error.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
convert_maya() {
  clear();
  clear_error();

  if (!open_api()) {
    mayaegg_cat.error()
      << "Maya is not available.\n";
    return false;
  }

  if (_egg_data->get_coordinate_system() == CS_default) {
    _egg_data->set_coordinate_system(_maya->get_coordinate_system());
  }

  mayaegg_cat.info()
    << "Converting from Maya.\n";

  // Figure out the animation parameters.
  double start_frame, end_frame, frame_inc, input_frame_rate, output_frame_rate;
  if (has_start_frame()) {
    start_frame = get_start_frame();
  } else {
    start_frame = MAnimControl::minTime().value();
  }
  if (has_end_frame()) {
    end_frame = get_end_frame();
  } else {
    end_frame = MAnimControl::maxTime().value();
  }
  if (has_frame_inc()) {
    frame_inc = get_frame_inc();
  } else {
    frame_inc = 1.0;
  }
  if (has_input_frame_rate()) {
    input_frame_rate = get_input_frame_rate();
  } else {
    MTime time(1.0, MTime::kSeconds);
    input_frame_rate = time.as(MTime::uiUnit());
  }
  if (has_output_frame_rate()) {
    output_frame_rate = get_output_frame_rate();
  } else {
    output_frame_rate = input_frame_rate;
  }

  frame_inc = frame_inc * input_frame_rate / output_frame_rate;

  bool all_ok = _tree.build_hierarchy();

  if (all_ok) {
    if (_from_selection) {
      all_ok = _tree.tag_selected();

    } else if (!_subsets.empty()) {
      Globs::const_iterator gi;
      for (gi = _subsets.begin(); gi != _subsets.end(); ++gi) {
        if (!_tree.tag_named(*gi)) {
          mayaegg_cat.info()
            << "No node matching " << *gi << " found.\n";
        }
      }

    } else {
      _tree.tag_all();
    }
  }

  if (all_ok) {
    switch (get_animation_convert()) {
    case AC_pose:
      // pose: set to a specific frame, then get out the static geometry.
      mayaegg_cat.info(false)
        << "frame " << start_frame << "\n";
      MGlobal::viewFrame(MTime(start_frame, MTime::uiUnit()));
      // fall through
      
    case AC_none:
      // none: just get out a static model, no animation.
      mayaegg_cat.info() << "ac_none" << endl;
      all_ok = convert_hierarchy(get_egg_data());
      break;
      
    case AC_flip:
    case AC_strobe:
      // flip or strobe: get out a series of static models, one per
      // frame, under a sequence node for AC_flip.
      all_ok = convert_flip(start_frame, end_frame, frame_inc,
                            output_frame_rate);
      break;

    case AC_model:
      // model: get out an animatable model with joints and vertex
      // membership.
      all_ok = convert_char_model();
      break;

    case AC_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 AC_both:
      // both: Put a model and its animation into the same egg file.
      _animation_convert = AC_model;
      if (!convert_char_model()) {
        all_ok = false;
      }
      _animation_convert = AC_chan;
      if (!convert_char_chan(start_frame, end_frame, frame_inc,
                             output_frame_rate)) {
        all_ok = false;
      }
      break;

    case AC_invalid:
      break;
    };

    reparent_decals(get_egg_data());
  }

  if (had_error()) {
    all_ok = false;
  }

  if (all_ok) {
    mayaegg_cat.info()
      << "Converted, no errors.\n";
  } else {
    mayaegg_cat.info()
      << "Errors encountered in conversion.\n";
  }

  return all_ok;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::open_api
//       Access: Public
//  Description: Attempts to open the Maya API if it was not already
//               open, and returns true if successful, or false if
//               there is an error.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
open_api() {
  if (_maya == (MayaApi *)NULL || !_maya->is_valid()) {
    _maya = MayaApi::open_api(_program_name);
  }
  return _maya->is_valid();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::close_api
//       Access: Public
//  Description: Closes the Maya API, if it was previously opened.
//               Caution!  Maya appears to call exit() when its API is
//               closed.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
close_api() {
  // We have to clear the shaders, at least, before we release the
  // Maya API.
  clear();
  _maya.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::clear
//       Access: Public
//  Description: Frees all of the Maya pointers kept within this
//               object, in preparation for loading a new scene or
//               releasing the Maya API.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
clear() {
  _tree.clear();
  _textures.clear();
  _shaders.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::convert_flip
//       Access: Private
//  Description: Converts the animation as a series of models that
//               cycle (flip) from one to the next at the appropriate
//               frame rate.  This is the most likely to convert
//               precisely (since we ask Maya to tell us the vertex
//               position each time) but it is the most wasteful in
//               terms of memory utilization (since a complete of the
//               model is stored for each frame).
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
convert_flip(double start_frame, double end_frame, double frame_inc,
             double output_frame_rate) {
  bool all_ok = true;

  EggGroup *sequence_node = new EggGroup(_character_name);
  get_egg_data()->add_child(sequence_node);
  if (_animation_convert == AC_flip) { 
    sequence_node->set_switch_flag(true);
    sequence_node->set_switch_fps(output_frame_rate);
  }

  MTime frame(start_frame, MTime::uiUnit());
  MTime frame_stop(end_frame, MTime::uiUnit());
  while (frame <= frame_stop) {
    mayaegg_cat.info(false)
      << "frame " << frame.value() << "\n";
    ostringstream name_strm;
    name_strm << "frame" << frame.value();
    EggGroup *frame_root = new EggGroup(name_strm.str());
    sequence_node->add_child(frame_root);

    MGlobal::viewFrame(frame);
    if (!convert_hierarchy(frame_root)) {
      all_ok = false;
    }

    frame += frame_inc;
  }

  return all_ok;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::convert_char_model
//       Access: Private
//  Description: Converts the file as an animatable character
//               model, with joints and vertex membership.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
convert_char_model() {
  if (has_neutral_frame()) {
    MTime frame(get_neutral_frame(), MTime::uiUnit());
    mayaegg_cat.info(false)
      << "neutral frame " << frame.value() << "\n";
    MGlobal::viewFrame(frame);
  }

  // It's also important for us to reset all the blend shape sliders
  // to 0 before we get out the model.  Otherwise, the model we
  // convert will have the current positions of the sliders baked in.
  _tree.reset_sliders();

  EggGroup *char_node = new EggGroup(_character_name);
  get_egg_data()->add_child(char_node);
  char_node->set_dart_type(EggGroup::DT_default);

  return convert_hierarchy(char_node);
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::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 MayaToEggConverter::
convert_char_chan(double start_frame, double end_frame, double frame_inc,
                  double output_frame_rate) {
  //  MStatus status;

  EggTable *root_table_node = new EggTable();
  get_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);
  EggTable *morph_node = new EggTable("morph");
  bundle_node->add_child(morph_node);

  // Set the frame rate before we start asking for anim tables to be
  // created.
  _tree._fps = output_frame_rate;
  _tree.clear_egg(get_egg_data(), NULL, skeleton_node, morph_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.
  PT(EggGroup) tgroup = new EggGroup;

  int num_nodes = _tree.get_num_nodes();
  int num_sliders = _tree.get_num_blend_descs();
  int i;

  MTime frame(start_frame, MTime::uiUnit());
  MTime frame_stop(end_frame, MTime::uiUnit());
  while (frame <= frame_stop) {
    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug(false)
        << "frame " << frame.value() << "\n";
    } else {
      // We have to write to cerr instead of mayaegg_cat to allow
      // flushing without writing a newline.
      cerr << "." << flush;
    }
    MGlobal::viewFrame(frame);

    for (i = 0; i < num_nodes; i++) {
      MayaNodeDesc *node_desc = _tree.get_node(i);
      if (node_desc->is_joint()) {
        if (mayaegg_cat.is_spam()) {
          mayaegg_cat.spam()
            << "joint " << node_desc->get_name() << "\n";
        }
        get_joint_transform(node_desc->get_dag_path(), tgroup);
        EggXfmSAnim *anim = _tree.get_egg_anim(node_desc);
        if (!anim->add_data(tgroup->get_transform3d())) {
          mayaegg_cat.error()
            << "Invalid transform on " << node_desc->get_name()
            << " frame " << frame.value() << ".\n";
        }
      }
    }

    for (i = 0; i < num_sliders; i++) {
      MayaBlendDesc *blend_desc = _tree.get_blend_desc(i);
      if (mayaegg_cat.is_spam()) {
        mayaegg_cat.spam()
          << "slider " << blend_desc->get_name() << "\n";
      }
      EggSAnimData *anim = _tree.get_egg_slider(blend_desc);
      anim->add_data(blend_desc->get_slider());
    }

    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++) {
    MayaNodeDesc *node_desc = _tree.get_node(i);
    if (node_desc->is_joint()) {
      _tree.get_egg_anim(node_desc)->optimize();
    }
  }

  for (i = 0; i < num_sliders; i++) {
    MayaBlendDesc *blend_desc = _tree.get_blend_desc(i);
    EggSAnimData *anim = _tree.get_egg_slider(blend_desc);
    anim->optimize();
  }
  
  mayaegg_cat.info(false)
    << "\n";

  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::convert_hierarchy
//       Access: Private
//  Description: Generates egg structures for each node in the Maya
//               hierarchy.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
convert_hierarchy(EggGroupNode *egg_root) {
  int num_nodes = _tree.get_num_nodes();

  _tree.clear_egg(get_egg_data(), egg_root, NULL, NULL);
  for (int i = 0; i < num_nodes; i++) {
    MayaNodeDesc *node = _tree.get_node(i);
    if (!process_model_node(node)) {
      return false;
    }
  }
  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::process_model_node
//       Access: Private
//  Description: Converts the indicated Maya node (given a MDagPath,
//               similar in concept to Panda's NodePath) to the
//               corresponding Egg structure.  Returns true if
//               successful, false if an error was encountered.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
process_model_node(MayaNodeDesc *node_desc) {
  if (!node_desc->has_dag_path()) {
    // If the node has no Maya equivalent, never mind.
    return true;
  }

  MDagPath dag_path = node_desc->get_dag_path();

  MStatus status;
  MFnDagNode dag_node(dag_path, &status);
  if (!status) {
    status.perror("MFnDagNode constructor");
    mayaegg_cat.error() << dag_path.fullPathName().asChar() << "\n";
    return false;
  }

  MObject node = dag_path.transform(&status);
  if (!status) {
    status.perror("dag_path.transform()");
    return false;
  }

  string path = dag_path.fullPathName().asChar();

  if (mayaegg_cat.is_debug()) {
    mayaegg_cat.debug()
      << path << ": " << dag_node.typeName().asChar();

    if (MAnimUtil::isAnimated(dag_path)) {
      mayaegg_cat.debug(false)
        << " (animated)";
    }

    mayaegg_cat.debug(false) << "\n";
  }

  if (dag_node.inUnderWorld()) {
    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug()
        << "Ignoring underworld node " << path
        << "\n";
    }

  } else if (dag_node.isIntermediateObject()) {
    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug()
        << "Ignoring intermediate object " << path
        << "\n";
    }

  } else if (dag_path.hasFn(MFn::kCamera)) {
    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug()
        << "Ignoring camera node " << path
        << "\n";
    }

  } else if (dag_path.hasFn(MFn::kLight)) {
    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug()
        << "Ignoring light node " << path
        << "\n";
    }

  } else if (dag_path.hasFn(MFn::kNurbsSurface)) {
    EggGroup *egg_group = _tree.get_egg_group(node_desc);
    get_transform(node_desc, dag_path, egg_group);

    if (node_desc->is_tagged()) {
      MFnNurbsSurface surface(dag_path, &status);
      if (!status) {
        mayaegg_cat.info()
          << "Error in node " << path
          << ":\n"
          << "  it appears to have a NURBS surface, but does not.\n";
      } else {
        make_nurbs_surface(node_desc, dag_path, surface, egg_group);
      }
    }

  } else if (dag_path.hasFn(MFn::kNurbsCurve)) {
    // Only convert NurbsCurves if we aren't making an animated model.
    // Animated models, as a general rule, don't want these sorts of
    // things in them.
    if (_animation_convert != AC_model) {
      EggGroup *egg_group = _tree.get_egg_group(node_desc);
      get_transform(node_desc, dag_path, egg_group);

      if (node_desc->is_tagged()) {
        MFnNurbsCurve curve(dag_path, &status);
        if (!status) {
          mayaegg_cat.info()
            << "Error in node " << path << ":\n"
            << "  it appears to have a NURBS curve, but does not.\n";
        } else {
          make_nurbs_curve(dag_path, curve, egg_group);
        }
      }
    }
      
  } else if (dag_path.hasFn(MFn::kMesh)) {
    if (node_desc->is_tagged()) {
      EggGroup *egg_group = _tree.get_egg_group(node_desc);
      get_transform(node_desc, dag_path, egg_group);
      MFnMesh mesh(dag_path, &status);
      if (!status) {
        mayaegg_cat.info()
          << "Error in node " << path << ":\n"
          << "  it appears to have a polygon mesh, but does not.\n";
      } else {
        make_polyset(node_desc, dag_path, mesh, egg_group);
      }
    }
    /*
    EggGroup *egg_group = _tree.get_egg_group(node_desc);
    get_transform(node_desc, dag_path, egg_group);

    if (node_desc->is_tagged()) {
      MFnMesh mesh(dag_path, &status);
      if (!status) {
        mayaegg_cat.info()
          << "Error in node " << path << ":\n"
          << "  it appears to have a polygon mesh, but does not.\n";
      } else {
        make_polyset(node_desc, dag_path, mesh, egg_group);
      }
    }
    */
  } else if (dag_path.hasFn(MFn::kLocator)) {
    if (_animation_convert == AC_none) {
      if (!node_desc->is_tagged()) {
        return true;
      }
    }
    EggGroup *egg_group = _tree.get_egg_group(node_desc);

    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug()
        << "Locator at " << path << "\n";
    }

    if (node_desc->is_tagged()) {
      // Presumably, the locator's position has some meaning to the
      // end-user, so we will implicitly tag it with the DCS flag so it
      // won't get flattened out.
      if (_animation_convert != AC_model) {
        // For now, don't set the DCS flag on locators within
        // character models, since egg-optchar doesn't understand
        // this.  Perhaps there's no reason to ever change this, since
        // locators within character models may not be meaningful.
        egg_group->set_dcs_type(EggGroup::DC_net);
      }
      get_transform(node_desc, dag_path, egg_group);
      make_locator(dag_path, dag_node, egg_group);
    }
  } else {
    // Just a generic node.
    if (_animation_convert == AC_none) {
      if (!node_desc->is_tagged()) {
        return true;
      }
    }
    EggGroup *egg_group = _tree.get_egg_group(node_desc);
    get_transform(node_desc, dag_path, egg_group);
  }

  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_transform
//       Access: Private
//  Description: Extracts the transform on the indicated Maya node,
//               and applies it to the corresponding Egg node.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
get_transform(MayaNodeDesc *node_desc, const MDagPath &dag_path, 
              EggGroup *egg_group) {
  if (_animation_convert == AC_model) {
    // When we're getting an animated model, we only get transforms
    // for joints, and they get converted in a special way.

    if (node_desc->is_joint()) { 
      get_joint_transform(dag_path, egg_group);
    }
    return;
  }

  MStatus status;
  MObject transformNode = dag_path.transform(&status);
  if (!status && status.statusCode() == MStatus::kInvalidParameter) {
    // This node has no transform - i.e., it's the world node
    return;
  }

  // Billboards always get the transform set.
  if (egg_group->get_billboard_type() == EggGroup::BT_none) {
    switch (_transform_type) {
    case TT_all:
      break;
      
    case TT_model:
      if (!egg_group->get_model_flag() && !egg_group->has_dcs_type()) {
        return;
      }
      break;
      
    case TT_dcs: 
      if (!egg_group->has_dcs_type()) {
        return;
      }
      break;
      
    case TT_none:
    case TT_invalid:
      return;
    }
  }

  // Extract the matrix from the dag path.
  MMatrix mat = dag_path.inclusiveMatrix(&status);
  if (!status) {
    status.perror("Can't get transform matrix");
    return;
  }
  LMatrix4d m4d(mat[0][0], mat[0][1], mat[0][2], mat[0][3],
                mat[1][0], mat[1][1], mat[1][2], mat[1][3],
                mat[2][0], mat[2][1], mat[2][2], mat[2][3],
                mat[3][0], mat[3][1], mat[3][2], mat[3][3]);

  // Maya has a rotate pivot, separate from its transform.  Usually we
  // care more about the rotate pivot than we do about the transform,
  // so get the rotate pivot too.
  MFnTransform transform(transformNode, &status);
  if (!status) {
    status.perror("MFnTransform constructor");
    return;
  }
  MPoint pivot = transform.rotatePivot(MSpace::kObject, &status);
  if (!status) {
    status.perror("Can't get rotate pivot");
    return;
  }
  
  // We need to convert the pivot to world coordinates.  (Maya can
  // only tell it to us in local coordinates.)
  LPoint3d p3d(pivot[0], pivot[1], pivot[2]);
  p3d = p3d * m4d;

  // Now recenter the matrix about the pivot point.
  m4d.set_row(3, p3d);

  // Convert the recentered matrix into the group's space and store
  // it.
  m4d = m4d * egg_group->get_node_frame_inv();
  if (!m4d.almost_equal(LMatrix4d::ident_mat(), 0.0001)) {
    egg_group->add_matrix4(m4d);
  }
  return;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::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 MayaToEggConverter::
get_joint_transform(const MDagPath &dag_path, EggGroup *egg_group) {
  // First, make sure there's not a transform on the group already.
  egg_group->clear_transform();

  MStatus status;
  MObject transformNode = dag_path.transform(&status);
  // This node has no transform - i.e., it's the world node
  if (!status && status.statusCode() == MStatus::kInvalidParameter) {
    return;
  }

  MFnDagNode transform(transformNode, &status);
  if (!status) {
    status.perror("MFnDagNode constructor");
    return;
  }

  MTransformationMatrix matrix(transform.transformationMatrix());

  if (mayaegg_cat.is_spam()) {
    MVector t = matrix.translation(MSpace::kWorld);
    mayaegg_cat.spam()
      << "  translation: ["
      << t[0] << ", "
      << t[1] << ", "
      << t[2] << "]\n";
    double d[3];
    MTransformationMatrix::RotationOrder rOrder;

    matrix.getRotation(d, rOrder, MSpace::kWorld);
    mayaegg_cat.spam()
      << "  rotation: ["
      << d[0] << ", "
      << d[1] << ", "
      << d[2] << "]\n";
    matrix.getScale(d, MSpace::kWorld);
    mayaegg_cat.spam()
      << "  scale: ["
      << d[0] << ", "
      << d[1] << ", "
      << d[2] << "]\n";
    matrix.getShear(d, MSpace::kWorld);
    mayaegg_cat.spam()
      << "  shear: ["
      << d[0] << ", "
      << d[1] << ", "
      << d[2] << "]\n";
  }

  MMatrix mat = matrix.asMatrix();
  MMatrix ident_mat;
  ident_mat.setToIdentity();

  if (!mat.isEquivalent(ident_mat, 0.0001)) {
    egg_group->set_transform3d
      (LMatrix4d(mat[0][0], mat[0][1], mat[0][2], mat[0][3],
                 mat[1][0], mat[1][1], mat[1][2], mat[1][3],
                 mat[2][0], mat[2][1], mat[2][2], mat[2][3],
                 mat[3][0], mat[3][1], mat[3][2], mat[3][3]));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::make_nurbs_surface
//       Access: Private
//  Description: Converts the indicated Maya NURBS surface to a
//               corresponding egg structure, and attaches it to the
//               indicated egg group.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
make_nurbs_surface(MayaNodeDesc *node_desc, const MDagPath &dag_path,
                   MFnNurbsSurface &surface, EggGroup *egg_group) {
  MStatus status;
  string name = surface.name().asChar();

  if (mayaegg_cat.is_spam()) {
    mayaegg_cat.spam()
      << "  numCVs: "
      << surface.numCVsInU()
      << " * "
      << surface.numCVsInV()
      << "\n";
    mayaegg_cat.spam()
      << "  numKnots: "
      << surface.numKnotsInU()
      << " * "
      << surface.numKnotsInV()
      << "\n";
    mayaegg_cat.spam()
      << "  numSpans: "
      << surface.numSpansInU()
      << " * "
      << surface.numSpansInV()
      << "\n";
  }

  MayaShader *shader = _shaders.find_shader_for_node(surface.object());

  if (_polygon_output) {
    // If we want polygon output only, tesselate the NURBS and output
    // that.
    MTesselationParams params;
    params.setFormatType(MTesselationParams::kStandardFitFormat);
    params.setOutputType(MTesselationParams::kQuads);
    params.setStdFractionalTolerance(_polygon_tolerance);

    // We'll create the tesselation as a sibling of the NURBS surface.
    // That way we inherit all of the transformations.
    MDagPath polyset_path = dag_path;
    MObject polyset_parent = polyset_path.node();
    MObject polyset =
      surface.tesselate(params, polyset_parent, &status);
    if (!status) {
      status.perror("MFnNurbsSurface::tesselate");
      return;
    }

    status = polyset_path.push(polyset);
    if (!status) {
      status.perror("MDagPath::push");
    }

    MFnMesh polyset_fn(polyset, &status);
    if (!status) {
      status.perror("MFnMesh constructor");
      return;
    }
    make_polyset(node_desc, polyset_path, polyset_fn, egg_group, shader);

    // Now remove the polyset we created.
    MFnDagNode parent_node(polyset_parent, &status);
    if (!status) {
      status.perror("MFnDagNode constructor");
      return;
    }
    status = parent_node.removeChild(polyset);
    if (!status) {
      status.perror("MFnDagNode::removeChild");
    }

    return;
  }

  MPointArray cv_array;
  status = surface.getCVs(cv_array, MSpace::kWorld);
  if (!status) {
    status.perror("MFnNurbsSurface::getCVs");
    return;
  }

  // Also get out all the alternate blend shapes for the surface by
  // applying each morph slider one at a time.
  pvector<MPointArray> morph_cvs;
  if (_animation_convert == AC_model) {
    int num_sliders = node_desc->get_num_blend_descs();
    morph_cvs.reserve(num_sliders);
    for (int i = 0; i < num_sliders; i++) {
      MayaBlendDesc *blend_desc = node_desc->get_blend_desc(i);

      // Temporarily push the slider up to 1.0 so we can see what the
      // surface looks like at that value.
      blend_desc->set_slider(1.0);
      MPointArray cv_array;
      status = surface.getCVs(cv_array, MSpace::kWorld);
      blend_desc->set_slider(0.0);

      if (!status) {
        status.perror("MFnNurbsSurface::getCVs");
        return;
      }
      morph_cvs.push_back(cv_array);
    }
  }

  MDoubleArray u_knot_array, v_knot_array;
  status = surface.getKnotsInU(u_knot_array);
  if (!status) {
    status.perror("MFnNurbsSurface::getKnotsInU");
    return;
  }
  status = surface.getKnotsInV(v_knot_array);
  if (!status) {
    status.perror("MFnNurbsSurface::getKnotsInV");
    return;
  }

  MFnNurbsSurface::Form u_form = surface.formInU();
  MFnNurbsSurface::Form v_form = surface.formInV();

  int u_degree = surface.degreeU();
  int v_degree = surface.degreeV();

  int u_cvs = surface.numCVsInU();
  int v_cvs = surface.numCVsInV();

  // Maya repeats CVS at the end for a periodic surface, and doesn't
  // count them in the joint weight array, below.
  int maya_u_cvs = (u_form == MFnNurbsSurface::kPeriodic) ? u_cvs - u_degree : u_cvs;
  int maya_v_cvs = (v_form == MFnNurbsSurface::kPeriodic) ? v_cvs - v_degree : v_cvs;

  int u_knots = surface.numKnotsInU();
  int v_knots = surface.numKnotsInV();

  assert(u_knots == u_cvs + u_degree - 1);
  assert(v_knots == v_cvs + v_degree - 1);

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

  EggNurbsSurface *egg_nurbs = new EggNurbsSurface(name);
  egg_nurbs->setup(u_degree + 1, v_degree + 1,
                   u_knots + 2, v_knots + 2);

  int i;

  egg_nurbs->set_u_knot(0, u_knot_array[0]);
  for (i = 0; i < u_knots; i++) {
    egg_nurbs->set_u_knot(i + 1, u_knot_array[i]);
  }
  egg_nurbs->set_u_knot(u_knots + 1, u_knot_array[u_knots - 1]);

  egg_nurbs->set_v_knot(0, v_knot_array[0]);
  for (i = 0; i < v_knots; i++) {
    egg_nurbs->set_v_knot(i + 1, v_knot_array[i]);
  }
  egg_nurbs->set_v_knot(v_knots + 1, v_knot_array[v_knots - 1]);

  LMatrix4d vertex_frame_inv = egg_group->get_vertex_frame_inv();

  for (i = 0; i < egg_nurbs->get_num_cvs(); i++) {
    int ui = egg_nurbs->get_u_index(i);
    int vi = egg_nurbs->get_v_index(i);
    int maya_vi = v_cvs * ui + vi;

    double v[4];
    status = cv_array[maya_vi].get(v);
    if (!status) {
      status.perror("MPoint::get");
    } else {
      EggVertex *vert = vpool->add_vertex(new EggVertex, i);
      LPoint4d p4d(v[0], v[1], v[2], v[3]);
      p4d = p4d * vertex_frame_inv;
      vert->set_pos(p4d);

      // Now generate the morph targets for the vertex.
      if (!morph_cvs.empty()) {
        // Morph deltas are given in 3-d space, not in 4-d homogenous
        // space.
        LPoint3d p3d(v[0] / v[3], v[1] / v[3], v[2] / v[3]);

        for (unsigned int si = 0; si < morph_cvs.size(); si++) {
          MayaBlendDesc *blend_desc = node_desc->get_blend_desc(si);
          status = morph_cvs[si][maya_vi].get(v);
          if (!status) {
            status.perror("MPoint::get");
          } else {
            LPoint3d m3d(v[0] / v[3], v[1] / v[3], v[2] / v[3]);
            LVector3d delta = m3d - p3d;
            if (!delta.almost_equal(LVector3d::zero())) {
              EggMorphVertex dxyz(blend_desc->get_name(), delta);
              vert->_dxyzs.insert(dxyz);
            }
          }
        }
      }

      egg_nurbs->add_vertex(vert);
    }
  }

  // Now consider the trim curves, if any.
  unsigned num_trims = surface.numRegions();
  int trim_curve_index = 0;
  for (unsigned ti = 0; ti < num_trims; ti++) {
    unsigned num_loops = surface.numBoundaries(ti);

    if (num_loops > 0) {
      egg_nurbs->_trims.push_back(EggNurbsSurface::Trim());
      EggNurbsSurface::Trim &egg_trim = egg_nurbs->_trims.back();

      for (unsigned li = 0; li < num_loops; li++) {
        egg_trim.push_back(EggNurbsSurface::Loop());
        EggNurbsSurface::Loop &egg_loop = egg_trim.back();
        
        MFnNurbsSurface::BoundaryType type =
          surface.boundaryType(ti, li, &status);
        bool keep_loop = false;
        
        if (!status) {
          status.perror("MFnNurbsSurface::BoundaryType");
        } else {
          keep_loop = (type == MFnNurbsSurface::kInner ||
                       type == MFnNurbsSurface::kOuter);
        }
        
        if (keep_loop) {
          unsigned num_edges = surface.numEdges(ti, li);
          for (unsigned ei = 0; ei < num_edges; ei++) {
            MObjectArray edge = surface.edge(ti, li, ei, true, &status);
            if (!status) {
              status.perror("MFnNurbsSurface::edge");
            } else {
              unsigned num_segs = edge.length();
              for (unsigned si = 0; si < num_segs; si++) {
                MObject segment = edge[si];
                if (segment.hasFn(MFn::kNurbsCurve)) {
                  MFnNurbsCurve curve(segment, &status);
                  if (!status) {
                    mayaegg_cat.error()
                      << "Trim curve appears to be a nurbs curve, but isn't.\n";
                  } else {
                    // Finally, we have a valid curve!
                    EggNurbsCurve *egg_curve =
                      make_trim_curve(curve, name, egg_group, trim_curve_index);
                    trim_curve_index++;
                    if (egg_curve != (EggNurbsCurve *)NULL) {
                      egg_loop.push_back(egg_curve);
                    }
                  }
                } else {
                  mayaegg_cat.error()
                    << "Trim curve segment is not a nurbs curve.\n";
                }
              }
            }
          }
        }
      }
    }
  }

  // We add the NURBS to the group down here, after all of the vpools
  // for the trim curves have been added.
  egg_group->add_child(egg_nurbs);

  if (shader != (MayaShader *)NULL) {
    set_shader_attributes(*egg_nurbs, *shader);
  }

  // Now try to find the skinning information for the surface.
  bool got_weights = false;

  pvector<EggGroup *> joints;
  MFloatArray weights;
  if (_animation_convert == AC_model) {
    got_weights = 
      get_vertex_weights(dag_path, surface, joints, weights);
  }

  if (got_weights && !joints.empty()) {
    int num_joints = joints.size();
    int num_weights = (int)weights.length();
    int num_verts = num_weights / num_joints;
    // The number of weights should be an even multiple of verts *
    // joints.
    nassertv(num_weights == num_verts * num_joints);

    for (i = 0; i < egg_nurbs->get_num_cvs(); i++) {
      int ui = egg_nurbs->get_u_index(i) % maya_u_cvs;
      int vi = egg_nurbs->get_v_index(i) % maya_v_cvs;

      int maya_vi = maya_v_cvs * ui + vi;
      nassertv(maya_vi < num_verts);
      EggVertex *vert = vpool->get_vertex(i);

      for (int ji = 0; ji < num_joints; ++ji) {
        float weight = weights[maya_vi * num_joints + ji];
        if (weight != 0.0f) {
          EggGroup *joint = joints[ji];
          if (joint != (EggGroup *)NULL) {
            joint->ref_vertex(vert, weight);
          }
        }
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::make_trim_curve
//       Access: Private
//  Description: Converts the indicated Maya NURBS trim curve to a
//               corresponding egg structure, and returns it, or NULL
//               if there is a problem.
////////////////////////////////////////////////////////////////////
EggNurbsCurve *MayaToEggConverter::
make_trim_curve(const MFnNurbsCurve &curve, const string &nurbs_name,
                EggGroupNode *egg_group, int trim_curve_index) {
  if (mayaegg_cat.is_spam()) {
    mayaegg_cat.spam()
      << "Trim curve:\n";
    mayaegg_cat.spam()
      << "  numCVs: "
      << curve.numCVs()
      << "\n";
    mayaegg_cat.spam()
      << "  numKnots: "
      << curve.numKnots()
      << "\n";
    mayaegg_cat.spam()
      << "  numSpans: "
      << curve.numSpans()
      << "\n";
  }

  MStatus status;

  MPointArray cv_array;
  status = curve.getCVs(cv_array, MSpace::kWorld);
  if (!status) {
    status.perror("MFnNurbsCurve::getCVs");
    return (EggNurbsCurve *)NULL;
  }
  MDoubleArray knot_array;
  status = curve.getKnots(knot_array);
  if (!status) {
    status.perror("MFnNurbsCurve::getKnots");
    return (EggNurbsCurve *)NULL;
  }

  /*
  MFnNurbsCurve::Form form = curve.form();
  */

  int degree = curve.degree();
  int cvs = curve.numCVs();
  int knots = curve.numKnots();

  assert(knots == cvs + degree - 1);

  string trim_name = "trim" + format_string(trim_curve_index);

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

  EggNurbsCurve *egg_curve = new EggNurbsCurve(trim_name);
  egg_curve->setup(degree + 1, knots + 2);

  int i;

  egg_curve->set_knot(0, knot_array[0]);
  for (i = 0; i < knots; i++) {
    egg_curve->set_knot(i + 1, knot_array[i]);
  }
  egg_curve->set_knot(knots + 1, knot_array[knots - 1]);

  for (i = 0; i < egg_curve->get_num_cvs(); i++) {
    double v[4];
    MStatus status = cv_array[i].get(v);
    if (!status) {
      status.perror("MPoint::get");
    } else {
      EggVertex vert;
      vert.set_pos(LPoint3d(v[0], v[1], v[3]));
      egg_curve->add_vertex(vpool->create_unique_vertex(vert));
    }
  }

  return egg_curve;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::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.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
make_nurbs_curve(const MDagPath &, const MFnNurbsCurve &curve,
                 EggGroup *egg_group) {
  MStatus status;
  string name = curve.name().asChar();

  if (mayaegg_cat.is_spam()) {
    mayaegg_cat.spam()
      << "  numCVs: "
      << curve.numCVs()
      << "\n";
    mayaegg_cat.spam()
      << "  numKnots: "
      << curve.numKnots()
      << "\n";
    mayaegg_cat.spam()
      << "  numSpans: "
      << curve.numSpans()
      << "\n";
  }

  MPointArray cv_array;
  status = curve.getCVs(cv_array, MSpace::kWorld);
  if (!status) {
    status.perror("MFnNurbsCurve::getCVs");
    return;
  }
  MDoubleArray knot_array;
  status = curve.getKnots(knot_array);
  if (!status) {
    status.perror("MFnNurbsCurve::getKnots");
    return;
  }

  /*
  MFnNurbsCurve::Form form = curve.form();
  */

  int degree = curve.degree();
  int cvs = curve.numCVs();
  int knots = curve.numKnots();

  assert(knots == cvs + degree - 1);

  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 + 1, knots + 2);

  int i;

  egg_curve->set_knot(0, knot_array[0]);
  for (i = 0; i < knots; i++) {
    egg_curve->set_knot(i + 1, knot_array[i]);
  }
  egg_curve->set_knot(knots + 1, knot_array[knots - 1]);

  LMatrix4d vertex_frame_inv = egg_group->get_vertex_frame_inv();

  for (i = 0; i < egg_curve->get_num_cvs(); i++) {
    double v[4];
    MStatus status = cv_array[i].get(v);
    if (!status) {
      status.perror("MPoint::get");
    } else {
      EggVertex vert;
      LPoint4d p4d(v[0], v[1], v[2], v[3]);
      p4d = p4d * vertex_frame_inv;
      vert.set_pos(p4d);
      egg_curve->add_vertex(vpool->create_unique_vertex(vert));
    }
  }

  MayaShader *shader = _shaders.find_shader_for_node(curve.object());
  if (shader != (MayaShader *)NULL) {
    set_shader_attributes(*egg_curve, *shader);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::find_uv_link
//       Access: Private
//  Description: given the texture name, find corresponding uvLink
////////////////////////////////////////////////////////////////////
string MayaToEggConverter::
find_uv_link(string match) {
  // find the index of this string in the _tex_names
  int idx = 0;
  vector_string::iterator vi;
  mayaegg_cat.spam() << "ful: looking for " << match << endl;
  for (vi = _tex_names.begin(); vi != _tex_names.end(); ++vi, ++idx) {
    mayaegg_cat.spam() << "ful:" << idx << ":stored_name is " << vi->c_str() << endl;
    if (vi->find(match) != string::npos) {
      return _uvset_names[idx];
    }
  }
  mayaegg_cat.spam() << "ful: did not find uvset-texture link called:" << match << endl;
  return "not found";
}
/*
////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::store_tex_names
//       Access: Private
//  Description: check the uvsets and see what texture these are 
//               connected to. set_shader_attribute will use this
//               information to write eggTexture
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
make_tex_names(const MFnMesh &mesh, const MObject &mesh_object) {

  // test the connection editor to gather the uvLink to the texture name
  MFnDependencyNode dn(mesh_object);
  MStringArray mresult;
  MPlugArray pla;

  _tex_names.clear();
  dn.getConnections(pla);
  mayaegg_cat.info() << "number of connections: " << pla.length() << endl;
  string uv1("uvLink -query -uvSet ");
  uv1.append(mesh.name().asChar());
  uv1.append(".uvSet[0].uvSetName");
  MGlobal::executeCommand(MString(uv1.c_str()), mresult);
  string tcat;
  for (size_t k=0; k<mresult.length(); ++k) {
    maya_cat.spam() << "found uvLink to texture: " << mresult[k].asChar() << endl;
    tcat.append(mresult[k].asChar());
  }
  mayaegg_cat.info() << "saving string to look up uvset: " << tcat << endl;
  _tex_names.push_back(tcat);
  for (size_t j=0; j<pla.length(); ++j) {
    MPlug pl = pla[j];
    mayaegg_cat.info() << pl.name() << " is(pl) " << pl.node().apiTypeStr() << endl;
    string tn;
    string ts = pl.name().asChar();
    if (ts.find("uvSetName") != string::npos) {
      string execString = "uvLink -query -uvSet " + ts;
      //mayaegg_cat.info() << "executing command: " << execString << "\n";
      MGlobal::executeCommand(MString(execString.c_str()), mresult);
      for (size_t k=0; k<mresult.length(); ++k) {
        maya_cat.info() << "found uvLink to texture: " << mresult[k].asChar() << endl;
        tcat.append(mresult[k].asChar());
      }
      // save unique of this string
      if (find_uv_link(tcat) == "not found") {
        mayaegg_cat.info() << "saving string: " << tcat << endl;
        _tex_names.push_back(tcat);
      }
    }
  } 
}
*/
////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::make_polyset
//       Access: Private
//  Description: Converts the indicated Maya polyset to a bunch of
//               EggPolygons and parents them to the indicated egg
//               group.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
make_polyset(MayaNodeDesc *node_desc, const MDagPath &dag_path, 
             const MFnMesh &mesh, EggGroup *egg_group,
             MayaShader *default_shader) {
  MStatus status;
  string name = mesh.name().asChar();

  MObject mesh_object = mesh.object();
  bool maya_double_sided = false;
  get_bool_attribute(mesh_object, "doubleSided", maya_double_sided);

  if (mayaegg_cat.is_spam()) {
    mayaegg_cat.spam()
      << "  numPolygons: "
      << mesh.numPolygons()
      << "\n";
    mayaegg_cat.spam()
      << "  numVertices: "
      << mesh.numVertices()
      << "\n";
  }

  if (mesh.numPolygons() == 0) {
    if (mayaegg_cat.is_debug()) {
      mayaegg_cat.debug()
        << "Ignoring empty mesh " << name << "\n";
    }
    return;
  }

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

  // 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.

  MObject component_obj;
  MItMeshPolygon pi(dag_path, component_obj, &status);
  if (!status) {
    status.perror("MItMeshPolygon constructor");
    return;
  }

  MObjectArray shaders;
  MIntArray poly_shader_indices;

  status = mesh.getConnectedShaders(dag_path.instanceNumber(),
                                    shaders, poly_shader_indices);
  if (!status) {
    status.perror("MFnMesh::getConnectedShaders");
  }

  // 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_inv = egg_group->get_vertex_frame_inv();

  // Save these modeling flags for the check below.
  bool egg_vertex_color = false;
  bool egg_double_sided = false;
  if (egg_group->has_user_data(MayaEggGroupUserData::get_class_type())) {
    MayaEggGroupUserData *user_data = 
      DCAST(MayaEggGroupUserData, egg_group->get_user_data());
    egg_vertex_color = user_data->_vertex_color;
    egg_double_sided = user_data->_double_sided;
  }

  bool double_sided = maya_double_sided;
  if (!_respect_maya_double_sided) {
    // If this flag is false, we respect the maya double-sided
    // settings only if the egg "double-sided" flag is also set.
    if (!egg_double_sided) {
      double_sided = false;
    }
  }

  //bool keep_all_uvsets = egg_group->has_object_type("keep-all-uvsets");
  bool keep_all_uvsets = node_desc->has_object_type("keep-all-uvsets");
  if (keep_all_uvsets)
    mayaegg_cat.info() << "will keep_all_uvsets" << endl;

  MStringArray maya_uvset_names;
  status = mesh.getUVSetNames(maya_uvset_names);
  if (!status) {
    status.perror("MFnMesh getUVSetNames not found");
  }
  MObjectArray moa;
  _tex_names.clear();
  _uvset_names.clear();
  for (size_t ui=0; ui<maya_uvset_names.length(); ++ui) {
    mayaegg_cat.spam() << "uv_set[" << ui << "] name: " << maya_uvset_names[ui].asChar() << endl;
    _uvset_names.push_back(maya_uvset_names[ui].asChar());

    // Get the tex_names that are connected to those uvnames
    mesh.getAssociatedUVSetTextures(maya_uvset_names[ui], moa);
    string t_n("");
    for (size_t ui=0; ui<moa.length(); ++ui){
      MFnDependencyNode dt(moa[ui]);
      t_n = dt.name().asChar();
      mayaegg_cat.spam() << "texture_node:" << t_n << endl;
    }
    _tex_names.push_back(t_n);
  }

  while (!pi.isDone()) {
    EggPolygon *egg_poly = new EggPolygon;
    egg_group->add_child(egg_poly);

    egg_poly->set_bface_flag(double_sided);

    // Determine the shader for this particular polygon.
    MayaShader *shader = NULL;
    int index = pi.index();
    nassertv(index >= 0 && index < (int)poly_shader_indices.length());
    int shader_index = poly_shader_indices[index];

    if (shader_index != -1) {
      nassertv(shader_index >= 0 && shader_index < (int)shaders.length());
      MObject engine = shaders[shader_index];
      shader =
        _shaders.find_shader_for_shading_engine(engine);

    } else if (default_shader != (MayaShader *)NULL) {
      shader = default_shader;
    }

    const MayaShaderColorDef *color_def = NULL;

    // And apply the shader properties to the polygon.
    if (shader != (MayaShader *)NULL) {
      set_shader_attributes(*egg_poly, *shader, &pi);
      color_def = shader->get_color_def();
    }

    // Should we extract the color from the vertices?  Normally, in
    // Maya a texture completely replaces the vertex color, so we
    // should ignore the vertex color if we have a texture.

    // However, this is an inconvenient property of Maya; sometimes we
    // really do want both vertex color and texture applied to the
    // same object.  To allow this, we define the special egg flag
    // "vertex-color", which when set indicates that we should
    // respect the vertex color anyway.

    // Furthermore, if _always_show_vertex_color is true, we pretend
    // that the "vertex-color" flag is always set.
    bool ignore_vertex_color = false;
    if ( color_def != (MayaShaderColorDef *)NULL) {
      ignore_vertex_color = color_def->_has_texture && !(egg_vertex_color || _always_show_vertex_color);
    }

    Colorf poly_color(1.0f, 1.0f, 1.0f, 1.0f);
    if (!ignore_vertex_color) {
      // If we're respecting the vertex color, then remove the color
      // specification from the polygon (so we can apply it to the
      // vertices).
      poly_color = egg_poly->get_color();
      egg_poly->clear_color();
    }

    // Get the vertices for the polygon.
    long num_verts = pi.polygonVertexCount();
    long i;
    LPoint3d centroid(0.0, 0.0, 0.0);

    if (color_def != (MayaShaderColorDef *)NULL && color_def->has_projection()) {
      // If the shader has a projection, we may need to compute the
      // polygon's centroid to avoid seams at the edges.
      for (i = 0; i < num_verts; i++) {
        MPoint p = pi.point(i, MSpace::kWorld);
        LPoint3d p3d(p[0], p[1], p[2]);
        p3d = p3d * vertex_frame_inv;
        centroid += p3d;
      }
      centroid /= (double)num_verts;
    }
    for (i = 0; i < num_verts; i++) {
      EggVertex vert;

      MPoint p = pi.point(i, MSpace::kWorld);
      LPoint3d p3d(p[0] / p[3], p[1] / p[3], p[2] / p[3]);
      p3d = p3d * vertex_frame_inv;
      vert.set_pos(p3d);

      MVector n;
      status = pi.getNormal(i, n, MSpace::kWorld);
      if (!status) {
        status.perror("MItMeshPolygon::getNormal");
      } else {
        Normald n3d(n[0], n[1], n[2]);
        n3d = n3d * vertex_frame_inv;
        vert.set_normal(n3d);
      }

      string uvset_name("");
      if (color_def != (MayaShaderColorDef *)NULL && color_def->_has_texture) {
        // Go thru all the texture references for this primitive and set uvs
        mayaegg_cat.debug() << "shader->_color.size is " << shader->_color.size() << endl;
        mayaegg_cat.debug() << "primitive->tref.size is " << egg_poly->get_num_textures() << endl;
        for (size_t ti=0; ti< _uvset_names.size(); ++ti) {
          // get the eggTexture pointer
          string colordef_uv_name = uvset_name=  _uvset_names[ti];
          mayaegg_cat.debug() << "--uvset_name :" << uvset_name << endl;

          if (uvset_name == "map1")  // this is the name to look up by in maya
            colordef_uv_name = "default";

          // get the shader color def that matches this EggTexture
          // Asad: optimizing uvset: to discard unused uvsets. This for 
          // loop figures out which ones are unused.
          // 
          bool found = false;
          for (size_t tj=0; tj< shader->_color.size(); ++tj) {
            color_def = shader->get_color_def(tj);
            if (color_def->_uvset_name == colordef_uv_name) {
              mayaegg_cat.debug() << "matched colordef idx: " << tj << endl;
              found = true;
              break;
            }
          }
          // if uvset is not used don't add it to the vertex
          if (!found && !keep_all_uvsets) {
            mayaegg_cat.spam() << "discarding unused uvset " << uvset_name << endl;
            continue;
          }

          mayaegg_cat.debug() << "color_def->uvset_name :" << color_def->_uvset_name << endl;
          if (color_def->has_projection()) {
            // If the shader has a projection, use it instead of the
            // polygon's built-in UV's.
            vert.set_uv(colordef_uv_name,
                        color_def->project_uv(p3d, centroid));
          } else {
            // Get the UV's from the polygon.
            float2 uvs;
            MString uv_mstring(uvset_name.c_str());
            if (pi.hasUVs(uv_mstring, &status)) {
              status = pi.getUV(i, uvs, &uv_mstring);
              if (!status) {
                status.perror("MItMeshPolygon::getUV");
              } else {
                vert.set_uv(colordef_uv_name, TexCoordd(uvs[0], uvs[1]));
              }
            }
          }
        }
      }

      if (!ignore_vertex_color) {
        if (pi.hasColor()) {
          MColor c;
          status = pi.getColor(c, i);
          if (!status) {
            status.perror("MItMeshPolygon::getColor");
          } else {
            // The vertex color is a color scale that modifies the
            // polygon color, not an override that replaces it.
            vert.set_color(Colorf(c.r * poly_color[0], c.g * poly_color[1],
                                  c.b * poly_color[2], c.a * poly_color[3]));
          }
        } else {
          vert.set_color(poly_color);
        }
      }

      vert.set_external_index(pi.vertexIndex(i, &status));

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

    // Also get the face normal for the polygon.
    Normald face_normal;
    bool got_face_normal = false;

    MVector n;
    status = pi.getNormal(n, MSpace::kWorld);
    if (!status) {
      status.perror("MItMeshPolygon::getNormal face");
    } else {
      face_normal.set(n[0], n[1], n[2]);
      face_normal = face_normal * vertex_frame_inv;
      got_face_normal = true;
      egg_poly->set_normal(face_normal);
    }

    // Now, check that the vertex ordering is consistent with the
    // direction of the normals.  If not, reverse the vertex ordering
    // (since we have seen cases where Maya sets this in contradiction
    // to its normals).
    Normald order_normal;
    if (got_face_normal && egg_poly->calculate_normal(order_normal)) {
      if (order_normal.dot(face_normal) < 0.0) {
        egg_poly->reverse_vertex_ordering();
        mayaegg_cat.debug()
          << "reversing polygon\n";
      }
    }
      
    pi.next();

  }
  mayaegg_cat.spam() << "done traversing polys" << endl;

  // 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.
  bool got_weights = false;

  pvector<EggGroup *> joints;
  MFloatArray weights;
  if (_animation_convert == AC_model) {
    got_weights = 
      get_vertex_weights(dag_path, mesh, joints, weights);
  }

  if (got_weights && !joints.empty()) {
    int num_joints = joints.size();
    int num_weights = (int)weights.length();
    int num_verts = num_weights / num_joints;
    // The number of weights should be an even multiple of verts *
    // joints.
    nassertv(num_weights == num_verts * num_joints);

    EggVertexPool::iterator vi;
    for (vi = vpool->begin(); vi != vpool->end(); ++vi) {
      EggVertex *vert = (*vi);
      int maya_vi = vert->get_external_index();
      nassertv(maya_vi >= 0 && maya_vi < num_verts);

      for (int ji = 0; ji < num_joints; ++ji) {
        float weight = weights[maya_vi * num_joints + ji];
        if (weight != 0.0f) {
          EggGroup *joint = joints[ji];
          if (joint != (EggGroup *)NULL) {
            joint->ref_vertex(vert, weight);
          }
        }
      }
    }
  }


  // We also need to compute the vertex morphs for the polyset, based
  // on whatever blend shapes may be present.  This is similar to the
  // code in make_nurbs_surface(), except that since we don't have a
  // one-to-one relationship of egg vertices to Maya vertices, we have
  // to get the morphs down here, after we have added all of the egg
  // vertices.

  if (_animation_convert == AC_model) {
    int num_orig_mesh_verts = mesh.numVertices();

    int num_sliders = node_desc->get_num_blend_descs();
    for (int i = 0; i < num_sliders; i++) {
      MayaBlendDesc *blend_desc = node_desc->get_blend_desc(i);

      // Temporarily push the slider up to 1.0 so we can see what the
      // surface looks like at that value.
      blend_desc->set_slider(1.0);

      // We have to get the mesh object from the dag again after
      // fiddling with the slider.
      MFnMesh blend_mesh(dag_path, &status);
      if (!status) {
        mayaegg_cat.warning()
          << name << " no longer has a mesh after applying "
          << blend_desc->get_name() << "\n";

      } else {
        if (blend_mesh.numVertices() != num_orig_mesh_verts) {
          mayaegg_cat.warning()
            << "Ignoring " << blend_desc->get_name() << " for " 
            << name << "; blend shape has " << blend_mesh.numVertices()
            << " vertices while original shape has " 
            << num_orig_mesh_verts << ".\n";
          
        } else {
          MPointArray verts;
          status = blend_mesh.getPoints(verts, MSpace::kWorld);
          if (!status) {
            status.perror("MFnMesh::getPoints");
          } else {
            int num_verts = (int)verts.length();
            EggVertexPool::iterator vi;
            for (vi = vpool->begin(); vi != vpool->end(); ++vi) {
              EggVertex *vert = (*vi);
              int maya_vi = vert->get_external_index();
              nassertv(maya_vi >= 0 && maya_vi < num_verts);
              
              const MPoint &m = verts[maya_vi];
              LPoint3d m3d(m[0] / m[3], m[1] / m[3], m[2] / m[3]);
              m3d = m3d * vertex_frame_inv;
              
              LVector3d delta = m3d - vert->get_pos3();
              if (!delta.almost_equal(LVector3d::zero())) {
                EggMorphVertex dxyz(blend_desc->get_name(), delta);
                vert->_dxyzs.insert(dxyz);
              }
            }
          }
        }
      }

      blend_desc->set_slider(0.0);
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::make_locator
//       Access: Private
//  Description: Locators are used in Maya to indicate a particular
//               position in space to the user or the modeler.  We
//               represent that in egg with an ordinary Group node,
//               which we transform by the locator's position, so that
//               the indicated point becomes the origin at this node
//               and below.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
make_locator(const MDagPath &dag_path, const MFnDagNode &dag_node,
             EggGroup *egg_group) {
  MStatus status;

  unsigned int num_children = dag_node.childCount();
  MObject locator;
  bool found_locator = false;
  for (unsigned int ci = 0; ci < num_children && !found_locator; ci++) {
    locator = dag_node.child(ci);
    found_locator = (locator.apiType() == MFn::kLocator);
  }

  if (!found_locator) {
    mayaegg_cat.error()
      << "Couldn't find locator within locator node " 
      << dag_path.fullPathName().asChar() << "\n";
    return;
  }

  LPoint3d p3d;
  if (!get_vec3d_attribute(locator, "localPosition", p3d)) {
    mayaegg_cat.error()
      << "Couldn't get position of locator " 
      << dag_path.fullPathName().asChar() << "\n";
    return;
  }

  // We need to convert the position to world coordinates.  For some
  // reason, Maya can only tell it to us in local coordinates.
  MMatrix mat = dag_path.inclusiveMatrix(&status);
  if (!status) {
    status.perror("Can't get coordinate space for locator");
    return;
  }
  LMatrix4d n2w(mat[0][0], mat[0][1], mat[0][2], mat[0][3],
                mat[1][0], mat[1][1], mat[1][2], mat[1][3],
                mat[2][0], mat[2][1], mat[2][2], mat[2][3],
                mat[3][0], mat[3][1], mat[3][2], mat[3][3]);
  p3d = p3d * n2w;

  // Now convert the locator point into the group's space.
  p3d = p3d * egg_group->get_node_frame_inv();

  egg_group->add_translate3d(p3d);
}


////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_vertex_weights
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
get_vertex_weights(const MDagPath &dag_path, const MFnMesh &mesh,
                   pvector<EggGroup *> &joints, MFloatArray &weights) {
  MStatus status;
  
  // Since we are working with a mesh the input attribute that 
  // creates the mesh is named "inMesh" 
  // 
  MObject attr = mesh.attribute("inMesh"); 
  
  // Create the plug to the "inMesh" attribute then use the 
  // DG iterator to walk through the DG, at the node level.
  // 
  MPlug history(mesh.object(), attr); 
  MItDependencyGraph it(history, MFn::kDependencyNode, 
                        MItDependencyGraph::kUpstream, 
                        MItDependencyGraph::kDepthFirst, 
                        MItDependencyGraph::kNodeLevel);

  while (!it.isDone()) {
    // We will walk along the node level of the DG until we 
    // spot a skinCluster node.
    // 
    MObject c_node = it.thisNode(); 
    if (c_node.hasFn(MFn::kSkinClusterFilter)) { 
      // We've found the cluster handle. Try to get the weight
      // data.
      // 
      MFnSkinCluster cluster(c_node, &status); 
      if (!status) {
        status.perror("MFnSkinCluster constructor");
        return false;
      }

      // Get the set of objects that influence the vertices of this
      // mesh.  Hopefully these will all be joints.
      MDagPathArray influence_objects;
      cluster.influenceObjects(influence_objects, &status); 
      if (!status) {
        status.perror("MFnSkinCluster::influenceObjects");

      } else {
        // Fill up the vector with the corresponding table of egg
        // groups for each joint.
        joints.clear();
        for (unsigned oi = 0; oi < influence_objects.length(); oi++) {
          MDagPath joint_dag_path = influence_objects[oi];
          MayaNodeDesc *joint_node_desc = _tree.build_node(joint_dag_path);
          EggGroup *joint = _tree.get_egg_group(joint_node_desc);
          joints.push_back(joint);
        }

        // Now use a component object to retrieve all of the weight
        // data in one API call.
        MFnSingleIndexedComponent sic; 
        MObject sic_object = sic.create(MFn::kMeshVertComponent); 
        sic.setCompleteData(mesh.numVertices()); 
        unsigned influence_count; 

        status = cluster.getWeights(dag_path, sic_object, 
                                    weights, influence_count); 
        if (!status) {
          status.perror("MFnSkinCluster::getWeights");
        } else {
          if (influence_count != influence_objects.length()) {
            mayaegg_cat.error()
              << "MFnSkinCluster::influenceObjects() returns " 
              << influence_objects.length()
              << " objects, but MFnSkinCluster::getWeights() reports "
              << influence_count << " objects.\n";
            
          } else {
            // We've got the weights and the set of objects.  That's all
            // we need.
            return true;
          }
        }
      }
    }

    it.next();
  }

  // The mesh was not soft-skinned.
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaToEggConverter::get_vertex_weights
//       Access: Private
//  Description: As above, for a NURBS surface instead of a polygon
//               mesh.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
get_vertex_weights(const MDagPath &dag_path, const MFnNurbsSurface &surface,
                   pvector<EggGroup *> &joints, MFloatArray &weights) {
  MStatus status;
  
  // Since we are working with a NURBS surface the input attribute that 
  // creates the surface is named "create" 
  // 
  MObject attr = surface.attribute("create"); 
  
  // Create the plug to the "create" attribute then use the 
  // DG iterator to walk through the DG, at the node level.
  // 
  MPlug history(surface.object(), attr); 
  MItDependencyGraph it(history, MFn::kDependencyNode, 
                        MItDependencyGraph::kUpstream, 
                        MItDependencyGraph::kDepthFirst, 
                        MItDependencyGraph::kNodeLevel);

  while (!it.isDone()) {
    // We will walk along the node level of the DG until we 
    // spot a skinCluster node.
    // 
    MObject c_node = it.thisNode(); 
    if (c_node.hasFn(MFn::kSkinClusterFilter)) { 
      // We've found the cluster handle. Try to get the weight
      // data.
      // 
      MFnSkinCluster cluster(c_node, &status); 
      if (!status) {
        status.perror("MFnSkinCluster constructor");
        return false;
      }

      // Get the set of objects that influence the vertices of this
      // surface.  Hopefully these will all be joints.
      MDagPathArray influence_objects;
      cluster.influenceObjects(influence_objects, &status); 
      if (!status) {
        status.perror("MFnSkinCluster::influenceObjects");

      } else {
        // Fill up the vector with the corresponding table of egg
        // groups for each joint.
        joints.clear();
        for (unsigned oi = 0; oi < influence_objects.length(); oi++) {
          MDagPath joint_dag_path = influence_objects[oi];
          MayaNodeDesc *joint_node_desc = _tree.build_node(joint_dag_path);
          EggGroup *joint = _tree.get_egg_group(joint_node_desc);
          joints.push_back(joint);
        }

        // Now use a component object to retrieve all of the weight
        // data in one API call.
        MFnDoubleIndexedComponent dic; 
        MObject dic_object = dic.create(MFn::kSurfaceCVComponent); 
        dic.setCompleteData(surface.numCVsInU(), surface.numCVsInV()); 
        unsigned influence_count; 

        status = cluster.getWeights(dag_path, dic_object, 
                                    weights, influence_count); 
        if (!status) {
          status.perror("MFnSkinCluster::getWeights");
        } else {
          if (influence_count != influence_objects.length()) {
            mayaegg_cat.error()
              << "MFnSkinCluster::influenceObjects() returns " 
              << influence_objects.length()
              << " objects, but MFnSkinCluster::getWeights() reports "
              << influence_count << " objects.\n";
            
          } else {
            // We've got the weights and the set of objects.  That's all
            // we need.
            return true;
          }
        }
      }
    }

    it.next();
  }

  // The surface was not soft-skinned.
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::set_shader_attributes
//       Access: Private
//  Description: Applies the known shader attributes to the indicated
//               egg primitive. Note: For multi-textures, Maya lists 
//               the top most texture in slot 0. But Panda puts the 
//               base texture at slot 0. Hence I parse the list of 
//               textures from last to first.
////////////////////////////////////////////////////////////////////
void MayaToEggConverter::
set_shader_attributes(EggPrimitive &primitive, const MayaShader &shader,
                      const MItMeshPolygon *pi) {

  // determine if the base texture or any of the top texture need to be rgb only
  MayaShaderColorDef *color_def = NULL;
  bool is_rgb = false;
  bool is_decal = false;
  bool is_interpolate = false;
  int i;
  // last shader is the base so lets skip it 
  for (i=0; i<(int)shader._color.size()-1; ++i) {
    color_def = shader.get_color_def(i);
    if (color_def->_has_texture) {
      if ((EggTexture::EnvType)color_def->_interpolate) {
        is_interpolate = true;
      }
      else if ((EggTexture::EnvType)color_def->_blend_type == EggTexture::ET_modulate) {
        // Maya's multiply is slightly different than panda's. Unless, _keep_alpha is set, 
        // we are dropping the alpha.
        if (!color_def->_keep_alpha)
          is_rgb = true; // modulate forces the alpha to be ignored
      }
      else if ((EggTexture::EnvType)color_def->_blend_type == EggTexture::ET_decal) {
        is_decal = true;
      }
    }
  }

  // we don't want an extra light stage for interpolate mode, it takes care of automatically
  if (is_interpolate)
    is_decal = false;
  
  // new decal mode needs an extra dummy layers of textureStage
  EggTexture *dummy_tex = (EggTexture *)NULL;
  string dummy_uvset_name;

  // In Maya, a polygon is either textured or colored.  The texture,
  // if present, replaces the color. Also now there could be multiple textures
  const MayaShaderColorDef &trans_def = shader._transparency;
  for (i=shader._color.size()-1; i>=0; --i) {
    color_def = shader.get_color_def(i);
    mayaegg_cat.spam() << "slot " << i << ":got color_def: " << color_def << endl;
    if (color_def->_has_texture || trans_def._has_texture) {
      EggTexture tex(shader.get_name(), "");
      mayaegg_cat.debug() << "got shader name:" << shader.get_name() << endl;
      mayaegg_cat.debug() << "ssa:texture name[" << i << "]: " << color_def->_texture_name << endl;

      string uvset_name = find_uv_link(color_def->_texture_name);
      mayaegg_cat.debug() << "ssa:corresponding uvset name is " << uvset_name << endl;

      if (color_def->_has_texture) {
        // If we have a texture on color, apply it as the filename.
        //mayaegg_cat.debug() << "ssa:got texture name" << color_def->_texture_filename << endl;
        Filename filename = Filename::from_os_specific(color_def->_texture_filename);
        Filename fullpath, outpath;
        _path_replace->full_convert_path(filename, get_texture_path(), fullpath, outpath);
        tex.set_filename(outpath);
        tex.set_fullpath(fullpath);
        apply_texture_properties(tex, *color_def);
        
        // If we also have a texture on transparency, apply it as the
        // alpha filename.
        if (trans_def._has_texture) {
          if (color_def->_wrap_u != trans_def._wrap_u ||
              color_def->_wrap_u != trans_def._wrap_u) {
            mayaegg_cat.warning()
              << "Shader " << shader.get_name()
              << " has contradictory wrap modes on color and texture.\n";
          }
          
          if (!compare_texture_properties(tex, trans_def)) {
            // Only report each broken shader once.
            static pset<string> bad_shaders;
            if (bad_shaders.insert(shader.get_name()).second) {
              mayaegg_cat.error()
                << "Color and transparency texture properties differ on shader "
                << shader.get_name() << "\n";
            }
          }
          //tex.set_format(EggTexture::F_rgba);
          
          // We should try to be smarter about whether the transparency
          // value is connected to the texture's alpha channel or to its
          // grayscale channel.  However, I'm not sure how to detect
          // this at the moment; rather than spending days trying to
          // figure out, for now I'll just assume that if the same
          // texture image is used for both color and transparency, then
          // the artist meant to use the alpha channel for transparency.
          if (trans_def._texture_filename == color_def->_texture_filename) {
            // That means that we don't need to do anything special: use
            // all the channels of the texture.
            
          } else {
            // Otherwise, pull the alpha channel from the other image
            // file.  Ideally, we should figure out which channel from
            // the other image supplies alpha (and specify this via
            // set_alpha_file_channel()), but for now we assume it comes
            // from the grayscale data.
            filename = Filename::from_os_specific(trans_def._texture_filename);
            _path_replace->full_convert_path(filename, get_texture_path(),
                                             fullpath, outpath);
            tex.set_alpha_filename(outpath);
            tex.set_alpha_fullpath(fullpath);
          }
        } else {
          // If there is no transparency texture specified, we don't
          // have any transparency, so tell the egg format to ignore any
          // alpha channel that might be on the color texture.
          //tex.set_format(EggTexture::F_rgb);
        }
        
        if (shader._color.size() > 1) {
          // if multi-textured, first texture in maya is on top, so
          // last shader on the list is the base one, which should always pick up the alpha
          // from the texture file. But the top textures may have to strip the alpha
          if (i!=shader._color.size()-1) {
            if (!i && is_interpolate) {
              // this is the grass path mode where alpha on this texture determines
              // whether to show layer1 or layer2. Since by now other layers are set
              // lets change those to get this effect
              tex.set_combine_mode(EggTexture::CC_rgb, EggTexture::CM_interpolate);
              tex.set_combine_source(EggTexture::CC_rgb, 0, EggTexture::CS_previous);
              tex.set_combine_operand(EggTexture::CC_rgb, 0, EggTexture::CO_src_color);
              tex.set_combine_source(EggTexture::CC_rgb, 1, EggTexture::CS_last_saved_result);
              tex.set_combine_operand(EggTexture::CC_rgb, 1, EggTexture::CO_src_color);
              tex.set_combine_source(EggTexture::CC_rgb, 2, EggTexture::CS_texture);
              tex.set_combine_operand(EggTexture::CC_rgb, 2, EggTexture::CO_src_alpha);

              tex.set_combine_mode(EggTexture::CC_alpha, EggTexture::CM_interpolate);
              tex.set_combine_source(EggTexture::CC_alpha, 0, EggTexture::CS_previous);
              tex.set_combine_operand(EggTexture::CC_alpha, 0, EggTexture::CO_src_alpha);
              tex.set_combine_source(EggTexture::CC_alpha, 1, EggTexture::CS_last_saved_result);
              tex.set_combine_operand(EggTexture::CC_alpha, 1, EggTexture::CO_src_alpha);
              tex.set_combine_source(EggTexture::CC_alpha, 2, EggTexture::CS_texture);
              tex.set_combine_operand(EggTexture::CC_alpha, 2, EggTexture::CO_src_alpha);
            }
            else {
              if (is_interpolate) {
                tex.set_combine_mode(EggTexture::CC_rgb, EggTexture::CM_modulate);
                tex.set_combine_source(EggTexture::CC_rgb, 0, EggTexture::CS_primary_color);
                tex.set_combine_operand(EggTexture::CC_rgb, 0, EggTexture::CO_src_color);
                tex.set_combine_source(EggTexture::CC_rgb, 1, EggTexture::CS_texture);
                tex.set_combine_operand(EggTexture::CC_rgb, 1, EggTexture::CO_src_color);
              }
              else {
                tex.set_env_type((EggTexture::EnvType)color_def->_blend_type);
                if (tex.get_env_type() == EggTexture::ET_modulate) {
                  if (color_def->_has_alpha_channel) {
                    // lets caution the artist that they should not be using a alpha channel on
                    // this texture. 
                    maya_cat.spam() 
                      << color_def->_texture_name 
                      << " should not have alpha channel in multiply mode: ignoring\n";
                  }
                  if (is_rgb) {
                    //tex.set_alpha_mode(EggRenderMode::AM_off);  // force alpha off
                    tex.set_format(EggTexture::F_rgb);  // Change the format to be rgb only
                  }
                }
              }
            }
          }
          else {
            if (is_interpolate) {
              // base shader need to save result
              tex.set_saved_result(true);
            }
            else if (is_decal) {
              // decal in classic time, always overwrote the base color. That causes problem
              // when the polygon wants to be lit or wants to retain vertex/polygon color
              // In the new decal mode, we achieve this with a third dummy layer
              // copy this layer to a new dummy layer
              EggTexture texDummy(shader.get_name()+".dummy", "");
              mayaegg_cat.debug() << "creating dummy shader: " << texDummy.get_name() << endl;
              texDummy.set_filename(outpath);
              texDummy.set_fullpath(fullpath);
              apply_texture_properties(texDummy, *color_def);
              texDummy.set_combine_mode(EggTexture::CC_rgb, EggTexture::CM_modulate);
              texDummy.set_combine_source(EggTexture::CC_rgb, 0, EggTexture::CS_primary_color);
              texDummy.set_combine_operand(EggTexture::CC_rgb, 0, EggTexture::CO_src_color);
              texDummy.set_combine_source(EggTexture::CC_rgb, 1, EggTexture::CS_previous);
              texDummy.set_combine_operand(EggTexture::CC_rgb, 1, EggTexture::CO_src_color);
              dummy_tex = _textures.create_unique_texture(texDummy, ~0);

              // make this layer ET_replace
              tex.set_env_type(EggTexture::ET_replace);
            }
          }
        }
      } else {  // trans_def._has_texture
        // We have a texture on transparency only.  Apply it as the
        // primary filename, and set the format accordingly.
        Filename filename = Filename::from_os_specific(trans_def._texture_filename);
        Filename fullpath,outpath;
        _path_replace->full_convert_path(filename, get_texture_path(),
                                         fullpath, outpath);
        tex.set_filename(outpath);
        tex.set_fullpath(fullpath);
        apply_texture_properties(tex, trans_def);
      }
      
      mayaegg_cat.debug() << "ssa:tref_name:" << tex.get_name() << endl;
      if (is_rgb && i == (int)shader._color.size()-1) {
        // make base layer rgb only
        tex.set_format(EggTexture::F_rgb);  // Change the format to be rgb only
      }
      EggTexture *new_tex =
        _textures.create_unique_texture(tex, ~0);
      
      if (pi) {
        // see if the uvset_name exists for this polygon, if yes, add the texture to primitive
        if (pi->hasUVs(MString(uvset_name.c_str()))) {
          primitive.add_texture(new_tex);
          if (uvset_name == "map1")  // this is the name to look up by in maya
            color_def->_uvset_name.assign("default");
          else
            color_def->_uvset_name.assign(uvset_name.c_str());
          new_tex->set_uv_name(color_def->_uvset_name);
          if (i == (int)shader._color.size()-1 && is_decal) {
            dummy_uvset_name.assign(color_def->_uvset_name);
          } 
        }
      } else {
        primitive.add_texture(new_tex);
        new_tex->set_uv_name(color_def->_uvset_name);
      }
    }
  }
  if (dummy_tex != (EggTexture *)NULL) {
    primitive.add_texture(dummy_tex);
    dummy_tex->set_uv_name(dummy_uvset_name);
  }
  // Also apply an overall color to the primitive.
  Colorf rgba = shader.get_rgba();

  // The existence of a texture on either color channel completely
  // replaces the corresponding flat color.
  if (color_def && color_def->_has_texture) {
    rgba[0] = 1.0f;
    rgba[1] = 1.0f;
    rgba[2] = 1.0f;
  }
  if (trans_def._has_texture) {
    rgba[3] = 1.0f;
  }

  // But the color gain always gets applied.
  if (color_def) {
    rgba[0] *= color_def->_color_gain[0];
    rgba[1] *= color_def->_color_gain[1];
    rgba[2] *= color_def->_color_gain[2];
    rgba[3] *= color_def->_color_gain[3];
  }

  primitive.set_color(rgba);

  mayaegg_cat.spam() << "  set_shader_attributes : end\n";
}

////////////////////////////////////////////////////////////////////
//     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 MayaToEggConverter::
apply_texture_properties(EggTexture &tex, const MayaShaderColorDef &color_def) {
  // Let's mipmap all textures by default.
  tex.set_minfilter(EggTexture::FT_linear_mipmap_linear);
  tex.set_magfilter(EggTexture::FT_linear);

  EggTexture::WrapMode wrap_u = color_def._wrap_u ? EggTexture::WM_repeat : EggTexture::WM_clamp;
  EggTexture::WrapMode wrap_v = color_def._wrap_v ? EggTexture::WM_repeat : EggTexture::WM_clamp;

  tex.set_wrap_u(wrap_u);
  tex.set_wrap_v(wrap_v);
  
  LMatrix3d mat = color_def.compute_texture_matrix();
  if (!mat.almost_equal(LMatrix3d::ident_mat())) {
    tex.set_transform2d(mat);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::compare_texture_properties
//       Access: Private
//  Description: Compares the texture properties already on the
//               texture (presumably set by a previous call to
//               apply_texture_properties()) and returns false if they
//               differ from that specified by the indicated color_def
//               object, or true if they match.
////////////////////////////////////////////////////////////////////
bool MayaToEggConverter::
compare_texture_properties(EggTexture &tex, 
                           const MayaShaderColorDef &color_def) {
  bool okflag = true;

  EggTexture::WrapMode wrap_u = color_def._wrap_u ? EggTexture::WM_repeat : EggTexture::WM_clamp;
  EggTexture::WrapMode wrap_v = color_def._wrap_v ? EggTexture::WM_repeat : EggTexture::WM_clamp;
  
  if (wrap_u != tex.determine_wrap_u()) {
    // Choose the more general of the two.
    if (wrap_u == EggTexture::WM_repeat) {
      tex.set_wrap_u(wrap_u);
    }
    okflag = false;
  }
  if (wrap_v != tex.determine_wrap_v()) {
    if (wrap_v == EggTexture::WM_repeat) {
      tex.set_wrap_v(wrap_v);
    }
    okflag = false;
  }
  
  LMatrix3d m = color_def.compute_texture_matrix();
  LMatrix4d mat4(m(0, 0), m(0, 1), 0.0, m(0, 2),
                 m(1, 0), m(1, 1), 0.0, m(1, 2),
                 0.0, 0.0, 1.0, 0.0,
                 m(2, 0), m(2, 1), 0.0, m(2, 2));
  if (!mat4.almost_equal(tex.get_transform3d())) {
    okflag = false;
  }

  return okflag;
}

////////////////////////////////////////////////////////////////////
//     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 MayaToEggConverter::
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 = DCAST(EggGroup, child);
      if (child_group->has_object_type("decalbase")) {
        if (decal_base != (EggNode *)NULL) {
          mayaegg_cat.error()
            << "Two children of " << egg_parent->get_name()
            << " both have decalbase set: " << decal_base->get_name()
            << " and " << child_group->get_name() << "\n";
          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()) {
      mayaegg_cat.warning()
        << decal_children.front()->get_name()
        << " has decal, but no sibling node has decalbase.\n";
    }

  } else {
    if (decal_children.empty()) {
      mayaegg_cat.warning()
        << decal_base->get_name()
        << " has decalbase, but no sibling nodes have decal.\n";

    } 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 = DCAST(EggGroupNode, child);
      if (!reparent_decals(child_group)) {
        okflag = false;
      }
    }
  }

  return okflag;
}

////////////////////////////////////////////////////////////////////
//     Function: MayaShader::string_transform_type
//       Access: Public, Static
//  Description: Returns the TransformType value corresponding to the
//               indicated string, or TT_invalid.
////////////////////////////////////////////////////////////////////
MayaToEggConverter::TransformType MayaToEggConverter::
string_transform_type(const string &arg) {
  if (cmp_nocase(arg, "all") == 0) {
    return TT_all;
  } else if (cmp_nocase(arg, "model") == 0) {
    return TT_model;
  } else if (cmp_nocase(arg, "dcs") == 0) {
    return TT_dcs;
  } else if (cmp_nocase(arg, "none") == 0) {
    return TT_none;
  } else {
    return TT_invalid;
  }
}
