// Filename: nodePath.cxx
// Created by:  drose (25Feb02)
//
////////////////////////////////////////////////////////////////////
//
// 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 "nodePath.h"
#include "nodePathCollection.h"
#include "findApproxPath.h"
#include "findApproxLevelEntry.h"
#include "internalNameCollection.h"
#include "config_pgraph.h"
#include "colorAttrib.h"
#include "colorScaleAttrib.h"
#include "cullBinAttrib.h"
#include "textureAttrib.h"
#include "texMatrixAttrib.h"
#include "texGenAttrib.h"
#include "materialAttrib.h"
#include "materialCollection.h"
#include "lightAttrib.h"
#include "clipPlaneAttrib.h"
#include "polylightEffect.h"
#include "fogAttrib.h"
#include "renderModeAttrib.h"
#include "cullFaceAttrib.h"
#include "alphaTestAttrib.h"
#include "depthTestAttrib.h"
#include "depthWriteAttrib.h"
#include "shaderAttrib.h"
#include "billboardEffect.h"
#include "compassEffect.h"
#include "showBoundsEffect.h"
#include "transparencyAttrib.h"
#include "antialiasAttrib.h"
#include "texProjectorEffect.h"
#include "texturePool.h"
#include "planeNode.h"
#include "lensNode.h"
#include "materialPool.h"
#include "look_at.h"
#include "plist.h"
#include "boundingSphere.h"
#include "geomNode.h"
#include "sceneGraphAnalyzer.h"
#include "sceneGraphReducer.h"
#include "textureCollection.h"
#include "textureStageCollection.h"
#include "globPattern.h"
#include "shader.h"
#include "shaderInput.h"
#include "config_gobj.h"
#include "bamFile.h"
#include "preparedGraphicsObjects.h"
#include "dcast.h"

// stack seems to overflow on Intel C++ at 7000.  If we need more than 
// 7000, need to increase stack size.
int NodePath::_max_search_depth = 7000; 
TypeHandle NodePath::_type_handle;

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_num_nodes
//       Access: Published
//  Description: Returns the number of nodes in the path.
////////////////////////////////////////////////////////////////////
int NodePath::
get_num_nodes(Thread *current_thread) const {
  if (is_empty()) {
    return 0;
  }
  int pipeline_stage = current_thread->get_pipeline_stage();
  return _head->get_length(pipeline_stage, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_node
//       Access: Published
//  Description: Returns the nth node of the path, where 0 is the
//               referenced (bottom) node and get_num_nodes() - 1 is
//               the top node.  This requires iterating through the
//               path.
//
//               Also see node(), which is a convenience function to
//               return the same thing as get_node(0) (since the
//               bottom node is the most important node in the
//               NodePath, and is the one most frequently referenced).
////////////////////////////////////////////////////////////////////
PandaNode *NodePath::
get_node(int index, Thread *current_thread) const {
  nassertr(index >= 0 && index < get_num_nodes(), NULL);

  int pipeline_stage = current_thread->get_pipeline_stage();

  NodePathComponent *comp = _head;
  while (index > 0) {
    // If this assertion fails, the index was out of range; the
    // component's length must have been invalid.
    nassertr(comp != (NodePathComponent *)NULL, NULL);
    comp = comp->get_next(pipeline_stage, current_thread);
    index--;
  }

  // If this assertion fails, the index was out of range; the
  // component's length must have been invalid.
  nassertr(comp != (NodePathComponent *)NULL, NULL);
  return comp->get_node();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_top
//       Access: Published
//  Description: Returns a singleton NodePath that represents the top
//               of the path, or empty NodePath if this path is empty.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
get_top(Thread *current_thread) const {
  if (is_empty()) {
    return *this;
  }

  int pipeline_stage = current_thread->get_pipeline_stage();

  NodePathComponent *comp = _head;
  while (!comp->is_top_node(pipeline_stage, current_thread)) {
    comp = comp->get_next(pipeline_stage, current_thread);
    nassertr(comp != (NodePathComponent *)NULL, NULL);
  }

  NodePath top;
  top._head = comp;
  return top;
}


////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_children
//       Access: Published
//  Description: Returns the set of all child nodes of the referenced
//               node.
////////////////////////////////////////////////////////////////////
NodePathCollection NodePath::
get_children(Thread *current_thread) const {
  NodePathCollection result;
  nassertr_always(!is_empty(), result);

  PandaNode *bottom_node = node();

  int pipeline_stage = current_thread->get_pipeline_stage();

  PandaNode::Children cr = bottom_node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    NodePath child;
    child._head = PandaNode::get_component(_head, cr.get_child(i),
					   pipeline_stage, current_thread);
    result.add_path(child);
  }

  return result;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_stashed_children
//       Access: Published
//  Description: Returns the set of all child nodes of the referenced
//               node that have been stashed.  These children are not
//               normally visible on the node, and do not appear in
//               the list returned by get_children().
////////////////////////////////////////////////////////////////////
NodePathCollection NodePath::
get_stashed_children(Thread *current_thread) const {
  NodePathCollection result;
  nassertr_always(!is_empty(), result);

  PandaNode *bottom_node = node();

  int pipeline_stage = current_thread->get_pipeline_stage();

  int num_stashed = bottom_node->get_num_stashed();
  for (int i = 0; i < num_stashed; i++) {
    NodePath stashed;
    stashed._head = PandaNode::get_component(_head, bottom_node->get_stashed(i),
					     pipeline_stage, current_thread);
    result.add_path(stashed);
  }

  return result;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_sort
//       Access: Published
//  Description: Returns the sort value of the referenced node within
//               its parent; that is, the sort number passed on the
//               last reparenting operation for this node.  This will
//               control the position of the node within its parent's
//               list of children.
////////////////////////////////////////////////////////////////////
int NodePath::
get_sort(Thread *current_thread) const {
  if (!has_parent()) {
    return 0;
  }

  int pipeline_stage = current_thread->get_pipeline_stage();

  PandaNode *parent = _head->get_next(pipeline_stage, current_thread)->get_node();
  PandaNode *child = node();
  nassertr(parent != (PandaNode *)NULL && child != (PandaNode *)NULL, 0);
  int child_index = parent->find_child(child);
  if (child_index != -1) {
    return parent->get_child_sort(child_index);
  }

  child_index = parent->find_stashed(child);
  if (child_index != -1) {
    return parent->get_stashed_sort(child_index);
  }

  nassertr(false, 0);
  return 0;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find
//       Access: Published
//  Description: Searches for a node below the referenced node that
//               matches the indicated string.  Returns the shortest
//               match found, if any, or an empty NodePath if no match
//               can be found.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
find(const string &path) const {
  nassertr_always(!is_empty(), fail());

  NodePathCollection col;
  find_matches(col, path, 1);

  if (col.is_empty()) {
    return NodePath::not_found();
  }

  return col.get_path(0);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_path_to
//       Access: Published
//  Description: Searches for the indicated node below this node and
//               returns the shortest NodePath that connects them.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
find_path_to(PandaNode *node) const {
  nassertr_always(!is_empty(), fail());
  nassertr(node != (PandaNode *)NULL, fail());

  NodePathCollection col;
  FindApproxPath approx_path;
  approx_path.add_match_many(0);
  approx_path.add_match_pointer(node, 0);
  find_matches(col, approx_path, 1);

  if (col.is_empty()) {
    return NodePath::not_found();
  }

  return col.get_path(0);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_matches
//       Access: Published
//  Description: Returns the complete set of all NodePaths that begin
//               with this NodePath and can be extended by
//               path.  The shortest paths will be listed
//               first.
////////////////////////////////////////////////////////////////////
NodePathCollection NodePath::
find_all_matches(const string &path) const {
  NodePathCollection col;
  nassertr_always(!is_empty(), col);
  nassertr(verify_complete(), col);
  find_matches(col, path, -1);
  return col;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_paths_to
//       Access: Published
//  Description: Returns the set of all NodePaths that extend from
//               this NodePath down to the indicated node.  The
//               shortest paths will be listed first.
////////////////////////////////////////////////////////////////////
NodePathCollection NodePath::
find_all_paths_to(PandaNode *node) const {
  NodePathCollection col;
  nassertr_always(!is_empty(), col);
  nassertr(verify_complete(), col);
  nassertr(node != (PandaNode *)NULL, col);
  FindApproxPath approx_path;
  approx_path.add_match_many(0);
  approx_path.add_match_pointer(node, 0);
  find_matches(col, approx_path, -1);
  return col;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::reparent_to
//       Access: Published
//  Description: Removes the referenced node of the NodePath from its
//               current parent and attaches it to the referenced node
//               of the indicated NodePath.  
//
//               If the destination NodePath is empty, this is the
//               same thing as detach_node().
////////////////////////////////////////////////////////////////////
void NodePath::
reparent_to(const NodePath &other, int sort, Thread *current_thread) {
  nassertv(verify_complete());
  nassertv(other.verify_complete());
  nassertv_always(!is_empty());
  nassertv(other._error_type == ET_ok);

  // Reparenting implicitly resets the delta vector.
  node()->reset_prev_transform();

  int pipeline_stage = current_thread->get_pipeline_stage();
  bool reparented = PandaNode::reparent(other._head, _head, sort, false,
					pipeline_stage, current_thread);
  nassertv(reparented);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::wrt_reparent_to
//       Access: Published
//  Description: This functions identically to reparent_to(), except
//               the transform on this node is also adjusted so that
//               the node remains in the same place in world
//               coordinates, even if it is reparented into a
//               different coordinate system.
////////////////////////////////////////////////////////////////////
void NodePath::
wrt_reparent_to(const NodePath &other, int sort, Thread *current_thread) {
  nassertv(verify_complete(current_thread));
  nassertv(other.verify_complete(current_thread));
  nassertv_always(!is_empty());
  nassertv(other._error_type == ET_ok);

  if (get_transform(current_thread) == get_prev_transform(current_thread)) {
    set_transform(get_transform(other, current_thread), current_thread);
    node()->reset_prev_transform(current_thread);
  } else {
    set_transform(get_transform(other, current_thread), current_thread);
    set_prev_transform(get_prev_transform(other, current_thread), current_thread);
  }

  reparent_to(other, sort, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::instance_to
//       Access: Published
//  Description: Adds the referenced node of the NodePath as a child
//               of the referenced node of the indicated other
//               NodePath.  Any other parent-child relations of the
//               node are unchanged; in particular, the node is not
//               removed from its existing parent, if any.
//
//               If the node already had an existing parent, this
//               method will create a new instance of the node within
//               the scene graph.
//
//               This does not change the NodePath itself, but does
//               return a new NodePath that reflects the new instance
//               node.
//
//               If the destination NodePath is empty, this creates a
//               new instance which is not yet parented to any node.
//               A new instance of this sort cannot easily be
//               differentiated from other similar instances, but it
//               is nevertheless a different instance and it will
//               return a different get_id() value.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
instance_to(const NodePath &other, int sort, Thread *current_thread) const {
  nassertr(verify_complete(), NodePath::fail());
  nassertr(other.verify_complete(), NodePath::fail());
  nassertr_always(!is_empty(), NodePath::fail());
  nassertr(other._error_type == ET_ok, NodePath::fail());

  NodePath new_instance;

  // First, we'll attach to NULL, to guarantee we get a brand new
  // instance.
  int pipeline_stage = current_thread->get_pipeline_stage();
  new_instance._head = PandaNode::attach(NULL, node(), sort, pipeline_stage,
                                         current_thread);

  // Now, we'll reparent the new instance to the target node.
  bool reparented = PandaNode::reparent(other._head, new_instance._head,
                                        sort, false, pipeline_stage,
                                        current_thread);
  nassertr(reparented, new_instance);

  // instance_to() doesn't reset the velocity delta, unlike most of
  // the other reparenting operations.  The reasoning is that
  // instance_to() is not necessarily a reparenting operation, since
  // it doesn't change the original instance.

  return new_instance;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::instance_under_node
//       Access: Published
//  Description: Behaves like instance_to(), but implicitly creates a
//               new node to instance the geometry under, and returns a
//               NodePath to that new node.  This allows the
//               programmer to set a unique state and/or transform on
//               this instance.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
instance_under_node(const NodePath &other, const string &name, int sort,
                    Thread *current_thread) const {
  NodePath new_node = other.attach_new_node(name, sort, current_thread);
  NodePath instance = instance_to(new_node, 0, current_thread);
  if (instance.is_empty()) {
    new_node.remove_node(current_thread);
    return instance;
  }
  return new_node;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::copy_to
//       Access: Published
//  Description: Functions like instance_to(), except a deep
//               copy is made of the referenced node and all of its
//               descendents, which is then parented to the indicated
//               node.  A NodePath to the newly created copy is
//               returned.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
copy_to(const NodePath &other, int sort, Thread *current_thread) const {
  nassertr(verify_complete(current_thread), fail());
  nassertr(other.verify_complete(current_thread), fail());
  nassertr_always(!is_empty(), fail());
  nassertr(other._error_type == ET_ok, fail());

  PandaNode *source_node = node();
  PT(PandaNode) copy_node = source_node->copy_subgraph(current_thread);
  nassertr(copy_node != (PandaNode *)NULL, fail());

  copy_node->reset_prev_transform(current_thread);

  return other.attach_new_node(copy_node, sort, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::attach_new_node
//       Access: Published
//  Description: Attaches a new node, with or without existing
//               parents, to the scene graph below the referenced node
//               of this NodePath.  This is the preferred way to add
//               nodes to the graph.
//
//               If the node was already a child of the parent, this
//               returns a NodePath to the existing child.
//
//               This does *not* automatically extend the current
//               NodePath to reflect the attachment; however, a
//               NodePath that does reflect this extension is
//               returned.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
attach_new_node(PandaNode *node, int sort, Thread *current_thread) const {
  nassertr(verify_complete(current_thread), NodePath::fail());
  nassertr(_error_type == ET_ok, NodePath::fail());
  nassertr(node != (PandaNode *)NULL, NodePath::fail());

  NodePath new_path(*this);
  int pipeline_stage = current_thread->get_pipeline_stage();
  new_path._head = PandaNode::attach(_head, node, sort, pipeline_stage,
                                     current_thread);
  return new_path;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::remove_node
//       Access: Published
//  Description: Disconnects the referenced node from the scene graph.
//               This will also delete the node if there are no other
//               pointers to it.
//
//               Normally, this should be called only when you are
//               really done with the node.  If you want to remove a
//               node from the scene graph but keep it around for
//               later, you should probably use detach_node() instead.
//
//               In practice, the only difference between
//               remove_node() and detach_node() is that remove_node()
//               also resets the NodePath to empty, which will cause
//               the node to be deleted immediately if there are no
//               other references.  On the other hand, detach_node()
//               leaves the NodePath referencing the node, which will
//               keep at least one reference to the node for as long
//               as the NodePath exists.
////////////////////////////////////////////////////////////////////
void NodePath::
remove_node(Thread *current_thread) {
  nassertv(_error_type != ET_not_found);

  // If we have no parents, remove_node() is just a do-nothing
  // operation; if we have no nodes, maybe we were already removed.
  // In either case, quietly do nothing except to ensure the
  // NodePath is clear.
  if (!is_empty() && !is_singleton(current_thread)) {
    node()->reset_prev_transform(current_thread);
    int pipeline_stage = current_thread->get_pipeline_stage();
    PandaNode::detach(_head, pipeline_stage, current_thread);
  }

  if (is_empty() || _head->has_key()) {
    // Preserve the key we had on the node before we removed it.
    int key = get_key();
    (*this) = NodePath::removed();
    _backup_key = key;

  } else {
    // We didn't have a key; just clear the NodePath.
    (*this) = NodePath::removed();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::detach_node
//       Access: Published
//  Description: Disconnects the referenced node from its parent, but
//               does not immediately delete it.  The NodePath retains
//               a pointer to the node, and becomes a singleton
//               NodePath.
//
//               This should be called to detach a node from the scene
//               graph, with the option of reattaching it later to the
//               same parent or to a different parent.
//
//               In practice, the only difference between
//               remove_node() and detach_node() is that remove_node()
//               also resets the NodePath to empty, which will cause
//               the node to be deleted immediately if there are no
//               other references.  On the other hand, detach_node()
//               leaves the NodePath referencing the node, which will
//               keep at least one reference to the node for as long
//               as the NodePath exists.
////////////////////////////////////////////////////////////////////
void NodePath::
detach_node(Thread *current_thread) {
  nassertv(_error_type != ET_not_found);
  if (!is_empty() && !is_singleton()) {
    node()->reset_prev_transform();
    int pipeline_stage = current_thread->get_pipeline_stage();
    PandaNode::detach(_head, pipeline_stage, current_thread);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::output
//       Access: Published
//  Description: Writes a sensible description of the NodePath to the
//               indicated output stream.
////////////////////////////////////////////////////////////////////
void NodePath::
output(ostream &out) const {
  switch (_error_type) {
  case ET_not_found:
    out << "**not found**";
    return;
  case ET_removed:
    out << "**removed**";
    return;
  case ET_fail:
    out << "**error**";
    return;
  default:
    break;
  }

  if (_head == (NodePathComponent *)NULL) {
    out << "(empty)";
  } else {
    _head->output(out);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_state
//       Access: Published
//  Description: Returns the complete state object set on this node.
////////////////////////////////////////////////////////////////////
const RenderState *NodePath::
get_state(Thread *current_thread) const {
  // This method is declared non-inline to avoid a compiler bug in
  // gcc-3.4 and gcc-4.0.
  nassertr_always(!is_empty(), RenderState::make_empty());
  return node()->get_state(current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_state
//       Access: Published
//  Description: Returns the state changes that must be made to
//               transition to the render state of this node from the
//               render state of the other node.
////////////////////////////////////////////////////////////////////
CPT(RenderState) NodePath::
get_state(const NodePath &other, Thread *current_thread) const {
  nassertr(_error_type == ET_ok && other._error_type == ET_ok, RenderState::make_empty());

  if (other.is_empty()) {
    return get_net_state(current_thread);
  }
  if (is_empty()) {
    return other.get_net_state(current_thread)->invert_compose(RenderState::make_empty());
  }
    
  nassertr(verify_complete(current_thread), RenderState::make_empty());
  nassertr(other.verify_complete(current_thread), RenderState::make_empty());

  int a_count, b_count;
  if (find_common_ancestor(*this, other, a_count, b_count, current_thread) == (NodePathComponent *)NULL) {
    if (allow_unrelated_wrt) {
      pgraph_cat.debug()
        << *this << " is not related to " << other << "\n";
    } else {
      pgraph_cat.error()
        << *this << " is not related to " << other << "\n";
      nassertr(false, RenderState::make_empty());
    }
  }

  CPT(RenderState) a_state = r_get_partial_state(_head, a_count, current_thread);
  CPT(RenderState) b_state = r_get_partial_state(other._head, b_count, current_thread);
  return b_state->invert_compose(a_state);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_state
//       Access: Published
//  Description: Sets the state object on this node, relative to
//               the other node.  This computes a new state object
//               that will have the indicated value when seen from the
//               other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_state(const NodePath &other, const RenderState *state,
          Thread *current_thread) {
  nassertv(_error_type == ET_ok && other._error_type == ET_ok);
  nassertv_always(!is_empty());

  // First, we perform a wrt to the parent, to get the conversion.
  CPT(RenderState) rel_state;
  if (has_parent()) {
    rel_state = other.get_state(get_parent(current_thread), current_thread);
  } else {
    rel_state = other.get_state(NodePath(), current_thread);
  }

  CPT(RenderState) new_state = rel_state->compose(state);
  set_state(new_state, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_transform
//       Access: Published
//  Description: Returns the complete transform object set on this node.
////////////////////////////////////////////////////////////////////
const TransformState *NodePath::
get_transform(Thread *current_thread) const {
  // This method is declared non-inline to avoid a compiler bug in
  // gcc-3.4 and gcc-4.0.
  nassertr_always(!is_empty(), TransformState::make_identity());
  return node()->get_transform(current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_transform
//       Access: Published
//  Description: Returns the relative transform to this node from the
//               other node; i.e. the transformation of this node
//               as seen from the other node.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
get_transform(const NodePath &other, Thread *current_thread) const {
  nassertr(_error_type == ET_ok && other._error_type == ET_ok, TransformState::make_identity());

  if (other.is_empty()) {
    return get_net_transform(current_thread);
  }
  if (is_empty()) {
    return other.get_net_transform(current_thread)->invert_compose(TransformState::make_identity());
  }
    
  nassertr(verify_complete(current_thread), TransformState::make_identity());
  nassertr(other.verify_complete(current_thread), TransformState::make_identity());

  int a_count, b_count;
  if (find_common_ancestor(*this, other, a_count, b_count, current_thread) == (NodePathComponent *)NULL) {
    if (allow_unrelated_wrt) {
      if (pgraph_cat.is_debug()) {
        pgraph_cat.debug()
          << *this << " is not related to " << other << "\n";
      }
    } else {
      pgraph_cat.error()
        << *this << " is not related to " << other << "\n";
      nassertr(false, TransformState::make_identity());
    }
  }

  CPT(TransformState) a_transform, b_transform;

  a_transform = r_get_partial_transform(_head, a_count, current_thread);
  if (a_transform != (TransformState *)NULL) {
    b_transform = r_get_partial_transform(other._head, b_count, current_thread);
  }
  if (b_transform == (TransformState *)NULL) {
    // If either path involved a node with a net_transform
    // RenderEffect applied, we have to go all the way up to the root
    // to get the right answer.
    a_transform = r_get_net_transform(_head, current_thread);
    b_transform = r_get_net_transform(other._head, current_thread);
  }
  return b_transform->invert_compose(a_transform);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_transform
//       Access: Published
//  Description: Sets the transform object on this node, relative to
//               the other node.  This computes a new transform object
//               that will have the indicated value when seen from the
//               other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_transform(const NodePath &other, const TransformState *transform,
              Thread *current_thread) {
  nassertv(_error_type == ET_ok && other._error_type == ET_ok);
  nassertv_always(!is_empty());

  // First, we perform a wrt to the parent, to get the conversion.
  CPT(TransformState) rel_trans;
  if (has_parent()) {
    rel_trans = other.get_transform(get_parent(current_thread), current_thread);
  } else {
    rel_trans = other.get_transform(NodePath(), current_thread);
  }

  CPT(TransformState) new_trans = rel_trans->compose(transform);
  set_transform(new_trans, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_prev_transform
//       Access: Published
//  Description: Returns the transform that has been set as this
//               node's "previous" position.  See
//               set_prev_transform().
////////////////////////////////////////////////////////////////////
const TransformState *NodePath::
get_prev_transform(Thread *current_thread) const {
  // This method is declared non-inline to avoid a compiler bug in
  // gcc-3.4 and gcc-4.0.
  nassertr_always(!is_empty(), TransformState::make_identity());
  return node()->get_prev_transform(current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_prev_transform
//       Access: Published
//  Description: Returns the relative "previous" transform to this
//               node from the other node; i.e. the position of this
//               node in the previous frame, as seen by the other node
//               in the previous frame.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
get_prev_transform(const NodePath &other, Thread *current_thread) const {
  nassertr(_error_type == ET_ok && other._error_type == ET_ok, TransformState::make_identity());

  if (other.is_empty()) {
    return get_net_prev_transform(current_thread);
  }
  if (is_empty()) {
    return other.get_net_prev_transform(current_thread)->invert_compose(TransformState::make_identity());
  }
    
  nassertr(verify_complete(current_thread), TransformState::make_identity());
  nassertr(other.verify_complete(current_thread), TransformState::make_identity());

  int a_count, b_count;
  if (find_common_ancestor(*this, other, a_count, b_count, current_thread) == (NodePathComponent *)NULL) {
    if (allow_unrelated_wrt) {
      pgraph_cat.debug()
        << *this << " is not related to " << other << "\n";
    } else {
      pgraph_cat.error()
        << *this << " is not related to " << other << "\n";
      nassertr(false, TransformState::make_identity());
    }
  }

  CPT(TransformState) a_prev_transform = r_get_partial_prev_transform(_head, a_count, current_thread);
  CPT(TransformState) b_prev_transform = r_get_partial_prev_transform(other._head, b_count, current_thread);
  return b_prev_transform->invert_compose(a_prev_transform);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_prev_transform
//       Access: Published
//  Description: Sets the "previous" transform object on this node,
//               relative to the other node.  This computes a new
//               transform object that will have the indicated value
//               when seen from the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_prev_transform(const NodePath &other, const TransformState *transform,
                   Thread *current_thread) {
  nassertv(_error_type == ET_ok && other._error_type == ET_ok);
  nassertv_always(!is_empty());

  // First, we perform a wrt to the parent, to get the conversion.
  CPT(TransformState) rel_trans;
  if (has_parent(current_thread)) {
    rel_trans = other.get_prev_transform(get_parent(current_thread), current_thread);
  } else {
    rel_trans = other.get_prev_transform(NodePath(), current_thread);
  }

  CPT(TransformState) new_trans = rel_trans->compose(transform);
  set_prev_transform(new_trans, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos
//       Access: Published
//  Description: Sets the translation component of the transform,
//               leaving rotation and scale untouched.  This also
//               resets the node's "previous" position, so that the
//               collision system will see the node as having suddenly
//               appeared in the new position, without passing any
//               points in between.
//     See Also: NodePath::set_fluid_pos
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos(const LVecBase3f &pos) {
  nassertv_always(!is_empty());
  set_transform(get_transform()->set_pos(pos));
  node()->reset_prev_transform();
}

void NodePath::
set_x(float x) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos();
  pos[0] = x;
  set_pos(pos);
}

void NodePath::
set_y(float y) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos();
  pos[1] = y;
  set_pos(pos);
}

void NodePath::
set_z(float z) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos();
  pos[2] = z;
  set_pos(pos);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_fluid_pos
//       Access: Published
//  Description: Sets the translation component, without changing the
//               "previous" position, so that the collision system
//               will see the node as moving fluidly from its previous
//               position to its new position.
//     See Also: NodePath::set_pos
////////////////////////////////////////////////////////////////////
void NodePath::
set_fluid_pos(const LVecBase3f &pos) {
  nassertv_always(!is_empty());
  set_transform(get_transform()->set_pos(pos));
}

void NodePath::
set_fluid_x(float x) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos();
  pos[0] = x;
  set_fluid_pos(pos);
}

void NodePath::
set_fluid_y(float y) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos();
  pos[1] = y;
  set_fluid_pos(pos);
}

void NodePath::
set_fluid_z(float z) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos();
  pos[2] = z;
  set_fluid_pos(pos);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_pos
//       Access: Published
//  Description: Retrieves the translation component of the transform.
////////////////////////////////////////////////////////////////////
LPoint3f NodePath::
get_pos() const {
  nassertr_always(!is_empty(), LPoint3f(0.0f, 0.0f, 0.0f));
  return get_transform()->get_pos();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_pos_delta
//       Access: Published
//  Description: Returns the delta vector from this node's position in
//               the previous frame (according to
//               set_prev_transform(), typically set via the use of
//               set_fluid_pos()) and its position in the current
//               frame.  This is the vector used to determine
//               collisions.  Generally, if the node was last
//               repositioned via set_pos(), the delta will be zero;
//               if it was adjusted via set_fluid_pos(), the delta
//               will represent the change from the previous frame's
//               position.
////////////////////////////////////////////////////////////////////
LVector3f NodePath::
get_pos_delta() const {
  nassertr_always(!is_empty(), LPoint3f(0.0f, 0.0f, 0.0f));
  return get_transform()->get_pos() - get_prev_transform()->get_pos();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_hpr
//       Access: Published
//  Description: Sets the rotation component of the transform,
//               leaving translation and scale untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_hpr(const LVecBase3f &hpr) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  nassertv(transform->has_hpr());
  set_transform(transform->set_hpr(hpr));
}

void NodePath::
set_h(float h) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  nassertv(transform->has_hpr());
  LVecBase3f hpr = transform->get_hpr();
  hpr[0] = h;
  set_transform(transform->set_hpr(hpr));
}

void NodePath::
set_p(float p) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  nassertv(transform->has_hpr());
  LVecBase3f hpr = transform->get_hpr();
  hpr[1] = p;
  set_transform(transform->set_hpr(hpr));
}

void NodePath::
set_r(float r) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  nassertv(transform->has_hpr());
  LVecBase3f hpr = transform->get_hpr();
  hpr[2] = r;
  set_transform(transform->set_hpr(hpr));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_hpr
//       Access: Published
//  Description: Retrieves the rotation component of the transform.
////////////////////////////////////////////////////////////////////
LVecBase3f NodePath::
get_hpr() const {
  nassertr_always(!is_empty(), LVecBase3f(0.0f, 0.0f, 0.0f));
  CPT(TransformState) transform = get_transform();
  nassertr(transform->has_hpr(), LVecBase3f(0.0f, 0.0f, 0.0f));
  return transform->get_hpr();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_quat
//       Access: Published
//  Description: Sets the rotation component of the transform,
//               leaving translation and scale untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_quat(const LQuaternionf &quat) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  set_transform(transform->set_quat(quat));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_quat
//       Access: Published
//  Description: Retrieves the rotation component of the transform.
////////////////////////////////////////////////////////////////////
LQuaternionf NodePath::
get_quat() const {
  nassertr_always(!is_empty(), LQuaternionf::ident_quat());
  CPT(TransformState) transform = get_transform();
  return transform->get_quat();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_scale
//       Access: Published
//  Description: Sets the scale component of the transform,
//               leaving translation and rotation untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_scale(const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  set_transform(transform->set_scale(scale));
}

void NodePath::
set_sx(float sx) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  LVecBase3f scale = transform->get_scale();
  scale[0] = sx;
  set_transform(transform->set_scale(scale));
}

void NodePath::
set_sy(float sy) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  LVecBase3f scale = transform->get_scale();
  scale[1] = sy;
  set_transform(transform->set_scale(scale));
}

void NodePath::
set_sz(float sz) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  LVecBase3f scale = transform->get_scale();
  scale[2] = sz;
  set_transform(transform->set_scale(scale));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_scale
//       Access: Published
//  Description: Retrieves the scale component of the transform.
////////////////////////////////////////////////////////////////////
LVecBase3f NodePath::
get_scale() const {
  nassertr_always(!is_empty(), LVecBase3f(0.0f, 0.0f, 0.0f));
  CPT(TransformState) transform = get_transform();
  return transform->get_scale();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shear
//       Access: Published
//  Description: Sets the shear component of the transform,
//               leaving translation and rotation untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_shear(const LVecBase3f &shear) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  set_transform(transform->set_shear(shear));
}

void NodePath::
set_shxy(float shxy) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  LVecBase3f shear = transform->get_shear();
  shear[0] = shxy;
  set_transform(transform->set_shear(shear));
}

void NodePath::
set_shxz(float shxz) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  LVecBase3f shear = transform->get_shear();
  shear[1] = shxz;
  set_transform(transform->set_shear(shear));
}

void NodePath::
set_shyz(float shyz) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  LVecBase3f shear = transform->get_shear();
  shear[2] = shyz;
  set_transform(transform->set_shear(shear));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_shear
//       Access: Published
//  Description: Retrieves the shear component of the transform.
////////////////////////////////////////////////////////////////////
LVecBase3f NodePath::
get_shear() const {
  nassertr_always(!is_empty(), LVecBase3f(0.0f, 0.0f, 0.0f));
  CPT(TransformState) transform = get_transform();
  return transform->get_shear();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_hpr
//       Access: Published
//  Description: Sets the translation and rotation component of the
//               transform, leaving scale untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_hpr(const LVecBase3f &pos, const LVecBase3f &hpr) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  transform = TransformState::make_pos_hpr_scale_shear
    (pos, hpr, transform->get_scale(), transform->get_shear());
  set_transform(transform);
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_quat
//       Access: Published
//  Description: Sets the translation and rotation component of the
//               transform, leaving scale untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_quat(const LVecBase3f &pos, const LQuaternionf &quat) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  transform = TransformState::make_pos_quat_scale_shear
    (pos, quat, transform->get_scale(), transform->get_shear());
  set_transform(transform);
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_hpr_scale
//       Access: Published
//  Description: Sets the rotation and scale components of the
//               transform, leaving translation untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_hpr_scale(const LVecBase3f &hpr, const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  transform = TransformState::make_pos_hpr_scale_shear
    (transform->get_pos(), hpr, scale, transform->get_shear());
  set_transform(transform);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_quat_scale
//       Access: Published
//  Description: Sets the rotation and scale components of the
//               transform, leaving translation untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_quat_scale(const LQuaternionf &quat, const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform();
  transform = TransformState::make_pos_quat_scale_shear
    (transform->get_pos(), quat, scale, transform->get_shear());
  set_transform(transform);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_hpr_scale
//       Access: Published
//  Description: Replaces the translation, rotation, and scale
//               components, implicitly setting shear to 0.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_hpr_scale(const LVecBase3f &pos, const LVecBase3f &hpr,
                  const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  set_transform(TransformState::make_pos_hpr_scale
                (pos, hpr, scale));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_quat_scale
//       Access: Published
//  Description: Replaces the translation, rotation, and scale
//               components, implicitly setting shear to 0.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_quat_scale(const LVecBase3f &pos, const LQuaternionf &quat,
                   const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  set_transform(TransformState::make_pos_quat_scale
                (pos, quat, scale));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_hpr_scale_shear
//       Access: Published
//  Description: Completely replaces the transform with new
//               translation, rotation, scale, and shear components.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_hpr_scale_shear(const LVecBase3f &pos, const LVecBase3f &hpr,
                        const LVecBase3f &scale, const LVecBase3f &shear) {
  nassertv_always(!is_empty());
  set_transform(TransformState::make_pos_hpr_scale_shear
                (pos, hpr, scale, shear));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_quat_scale_shear
//       Access: Published
//  Description: Completely replaces the transform with new
//               translation, rotation, scale, and shear components.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_quat_scale_shear(const LVecBase3f &pos, const LQuaternionf &quat,
                         const LVecBase3f &scale, const LVecBase3f &shear) {
  nassertv_always(!is_empty());
  set_transform(TransformState::make_pos_quat_scale_shear
                (pos, quat, scale, shear));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_mat
//       Access: Published
//  Description: Directly sets an arbitrary 4x4 transform matrix.
////////////////////////////////////////////////////////////////////
void NodePath::
set_mat(const LMatrix4f &mat) {
  nassertv_always(!is_empty());
  set_transform(TransformState::make_mat(mat));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::look_at
//       Access: Published
//  Description: Sets the hpr on this NodePath so that it
//               rotates to face the indicated point in space.
////////////////////////////////////////////////////////////////////
void NodePath::
look_at(const LPoint3f &point, const LVector3f &up) {
  nassertv_always(!is_empty());

  LPoint3f pos = get_pos();

  LQuaternionf quat;
  ::look_at(quat, point - pos, up);
  set_quat(quat);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::heads_up
//       Access: Published
//  Description: Behaves like look_at(), but with a strong preference
//               to keeping the up vector oriented in the indicated
//               "up" direction.
////////////////////////////////////////////////////////////////////
void NodePath::
heads_up(const LPoint3f &point, const LVector3f &up) {
  nassertv_always(!is_empty());

  LPoint3f pos = get_pos();

  LQuaternionf quat;
  ::heads_up(quat, point - pos, up);
  set_quat(quat);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos
//       Access: Published
//  Description: Sets the translation component of the transform,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos(const NodePath &other, const LVecBase3f &pos) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other three components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_hpr = orig_transform->get_hpr();
    const LVecBase3f &orig_scale = orig_transform->get_scale();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    set_transform(other, rel_transform->set_pos(pos));
    set_pos_hpr_scale_shear(get_transform()->get_pos(), orig_hpr, orig_scale, orig_shear);

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, rel_transform->set_pos(pos));
  }
  node()->reset_prev_transform();
}

void NodePath::
set_x(const NodePath &other, float x) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos(other);
  pos[0] = x;
  set_pos(other, pos);
}

void NodePath::
set_y(const NodePath &other, float y) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos(other);
  pos[1] = y;
  set_pos(other, pos);
}

void NodePath::
set_z(const NodePath &other, float z) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos(other);
  pos[2] = z;
  set_pos(other, pos);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_fluid_pos
//       Access: Published
//  Description: Sets the translation component of the transform,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_fluid_pos(const NodePath &other, const LVecBase3f &pos) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other three components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_hpr = orig_transform->get_hpr();
    const LVecBase3f &orig_scale = orig_transform->get_scale();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    // Use the relative set_transform() to compute the relative pos, and
    // then reset all of the other components back to the way they were.
    set_transform(other, rel_transform->set_pos(pos));
    set_transform(TransformState::make_pos_hpr_scale_shear
                  (get_transform()->get_pos(), orig_hpr, orig_scale, orig_shear));

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, rel_transform->set_pos(pos));
  }
}

void NodePath::
set_fluid_x(const NodePath &other, float x) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos(other);
  pos[0] = x;
  set_fluid_pos(other, pos);
}

void NodePath::
set_fluid_y(const NodePath &other, float y) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos(other);
  pos[1] = y;
  set_fluid_pos(other, pos);
}

void NodePath::
set_fluid_z(const NodePath &other, float z) {
  nassertv_always(!is_empty());
  LPoint3f pos = get_pos(other);
  pos[2] = z;
  set_fluid_pos(other, pos);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_pos
//       Access: Published
//  Description: Returns the relative position of the referenced node
//               as seen from the other node.
////////////////////////////////////////////////////////////////////
LPoint3f NodePath::
get_pos(const NodePath &other) const {
  nassertr_always(!is_empty(), LPoint3f(0.0f, 0.0f, 0.0f));
  return get_transform(other)->get_pos();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_pos_delta
//       Access: Published
//  Description: Returns the delta vector from this node's position in
//               the previous frame (according to
//               set_prev_transform(), typically set via the use of
//               set_fluid_pos()) and its position in the current
//               frame, as seen in the indicated node's coordinate
//               space.  This is the vector used to determine
//               collisions.  Generally, if the node was last
//               repositioned via set_pos(), the delta will be zero;
//               if it was adjusted via set_fluid_pos(), the delta
//               will represent the change from the previous frame's
//               position.
////////////////////////////////////////////////////////////////////
LVector3f NodePath::
get_pos_delta(const NodePath &other) const {
  nassertr_always(!is_empty(), LPoint3f(0.0f, 0.0f, 0.0f));
  return get_transform(other)->get_pos() - get_prev_transform(other)->get_pos();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_hpr
//       Access: Published
//  Description: Sets the rotation component of the transform,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_hpr(const NodePath &other, const LVecBase3f &hpr) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);
  nassertv(rel_transform->has_hpr());

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other three components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_pos = orig_transform->get_pos();
    const LVecBase3f &orig_scale = orig_transform->get_scale();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    set_transform(other, rel_transform->set_hpr(hpr));
    const TransformState *new_transform = get_transform();
    if (new_transform->has_components()) {
      set_transform(TransformState::make_pos_hpr_scale_shear
                    (orig_pos, new_transform->get_hpr(), orig_scale, orig_shear));
    }

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, rel_transform->set_hpr(hpr));
  }
}

void NodePath::
set_h(const NodePath &other, float h) {
  nassertv_always(!is_empty());
  LVecBase3f hpr = get_hpr(other);
  hpr[0] = h;
  set_hpr(other, hpr);
}

void NodePath::
set_p(const NodePath &other, float p) {
  nassertv_always(!is_empty());
  LVecBase3f hpr = get_hpr(other);
  hpr[1] = p;
  set_hpr(other, hpr);
}

void NodePath::
set_r(const NodePath &other, float r) {
  nassertv_always(!is_empty());
  LVecBase3f hpr = get_hpr(other);
  hpr[2] = r;
  set_hpr(other, hpr);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_hpr
//       Access: Published
//  Description: Returns the relative orientation of the bottom node
//               as seen from the other node.
////////////////////////////////////////////////////////////////////
LVecBase3f NodePath::
get_hpr(const NodePath &other) const {
  nassertr_always(!is_empty(), LVecBase3f(0.0f, 0.0f, 0.0f));
  CPT(TransformState) transform = get_transform(other);
  nassertr(transform->has_hpr(), LVecBase3f(0.0f, 0.0f, 0.0f));
  return transform->get_hpr();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_quat
//       Access: Published
//  Description: Sets the rotation component of the transform,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_quat(const NodePath &other, const LQuaternionf &quat) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other three components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_pos = orig_transform->get_pos();
    const LVecBase3f &orig_scale = orig_transform->get_scale();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    set_transform(other, rel_transform->set_quat(quat));
    const TransformState *new_transform = get_transform();
    if (new_transform->has_components()) {
      set_transform(TransformState::make_pos_quat_scale_shear
                    (orig_pos, new_transform->get_quat(), orig_scale, orig_shear));
    }

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, rel_transform->set_quat(quat));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_quat
//       Access: Published
//  Description: Returns the relative orientation of the bottom node
//               as seen from the other node.
////////////////////////////////////////////////////////////////////
LQuaternionf NodePath::
get_quat(const NodePath &other) const {
  nassertr_always(!is_empty(), LQuaternionf::ident_quat());
  CPT(TransformState) transform = get_transform(other);
  return transform->get_quat();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_scale
//       Access: Published
//  Description: Sets the scale component of the transform,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_scale(const NodePath &other, const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other three components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_pos = orig_transform->get_pos();
    const LVecBase3f &orig_hpr = orig_transform->get_hpr();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    set_transform(other, rel_transform->set_scale(scale));
    const TransformState *new_transform = get_transform();
    if (new_transform->has_components()) {
      set_transform(TransformState::make_pos_hpr_scale_shear
                    (orig_pos, orig_hpr, new_transform->get_scale(), orig_shear));
    }

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, rel_transform->set_scale(scale));
  }
}

void NodePath::
set_sx(const NodePath &other, float sx) {
  nassertv_always(!is_empty());
  LVecBase3f scale = get_scale(other);
  scale[0] = sx;
  set_scale(other, scale);
}

void NodePath::
set_sy(const NodePath &other, float sy) {
  nassertv_always(!is_empty());
  LVecBase3f scale = get_scale(other);
  scale[1] = sy;
  set_scale(other, scale);
}

void NodePath::
set_sz(const NodePath &other, float sz) {
  nassertv_always(!is_empty());
  LVecBase3f scale = get_scale(other);
  scale[2] = sz;
  set_scale(other, scale);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_scale
//       Access: Published
//  Description: Returns the relative scale of the bottom node
//               as seen from the other node.
////////////////////////////////////////////////////////////////////
LVecBase3f NodePath::
get_scale(const NodePath &other) const {
  nassertr_always(!is_empty(), LVecBase3f(0.0f, 0.0f, 0.0f));
  CPT(TransformState) transform = get_transform(other);
  return transform->get_scale();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shear
//       Access: Published
//  Description: Sets the shear component of the transform,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_shear(const NodePath &other, const LVecBase3f &shear) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other three components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_pos = orig_transform->get_pos();
    const LVecBase3f &orig_hpr = orig_transform->get_hpr();
    const LVecBase3f &orig_scale = orig_transform->get_scale();

    set_transform(other, rel_transform->set_shear(shear));
    const TransformState *new_transform = get_transform();
    if (new_transform->has_components()) {
      set_transform(TransformState::make_pos_hpr_scale_shear
                    (orig_pos, orig_hpr, orig_scale, new_transform->get_shear()));
    }

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, rel_transform->set_shear(shear));
  }
}

void NodePath::
set_shxy(const NodePath &other, float shxy) {
  nassertv_always(!is_empty());
  LVecBase3f shear = get_shear(other);
  shear[0] = shxy;
  set_shear(other, shear);
}

void NodePath::
set_shxz(const NodePath &other, float shxz) {
  nassertv_always(!is_empty());
  LVecBase3f shear = get_shear(other);
  shear[1] = shxz;
  set_shear(other, shear);
}

void NodePath::
set_shyz(const NodePath &other, float shyz) {
  nassertv_always(!is_empty());
  LVecBase3f shear = get_shear(other);
  shear[2] = shyz;
  set_shear(other, shear);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_shear
//       Access: Published
//  Description: Returns the relative shear of the bottom node
//               as seen from the other node.
////////////////////////////////////////////////////////////////////
LVecBase3f NodePath::
get_shear(const NodePath &other) const {
  nassertr_always(!is_empty(), LVecBase3f(0.0f, 0.0f, 0.0f));
  CPT(TransformState) transform = get_transform(other);
  return transform->get_shear();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_hpr
//       Access: Published
//  Description: Sets the translation and rotation component of the
//               transform, relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_hpr(const NodePath &other, const LVecBase3f &pos,
            const LVecBase3f &hpr) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);

  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other two components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_scale = orig_transform->get_scale();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    set_transform(other, TransformState::make_pos_hpr_scale_shear
                  (pos, hpr, rel_transform->get_scale(), rel_transform->get_shear()));
    const TransformState *new_transform = get_transform();
    if (new_transform->has_components()) {
      set_pos_hpr_scale_shear(new_transform->get_pos(), new_transform->get_hpr(),
                              orig_scale, orig_shear);
    }

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, TransformState::make_pos_hpr_scale_shear
                  (pos, hpr, rel_transform->get_scale(), rel_transform->get_shear()));
    node()->reset_prev_transform();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_quat
//       Access: Published
//  Description: Sets the translation and rotation component of the
//               transform, relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_quat(const NodePath &other, const LVecBase3f &pos,
             const LQuaternionf &quat) {
  nassertv_always(!is_empty());
  CPT(TransformState) rel_transform = get_transform(other);
  
  CPT(TransformState) orig_transform = get_transform();
  if (orig_transform->has_components()) {
    // If we had a componentwise transform before we started, we
    // should be careful to preserve the other two components.  We
    // wouldn't need to do this, except for the possibility of
    // numerical error or decompose ambiguity.
    const LVecBase3f &orig_scale = orig_transform->get_scale();
    const LVecBase3f &orig_shear = orig_transform->get_shear();

    set_transform(other, TransformState::make_pos_quat_scale_shear
                  (pos, quat, rel_transform->get_scale(), rel_transform->get_shear()));
    const TransformState *new_transform = get_transform();
    if (new_transform->has_components()) {
      set_pos_quat_scale_shear(new_transform->get_pos(), new_transform->get_quat(),
                               orig_scale, orig_shear);
    }

  } else {
    // If we didn't have a componentwise transform already, never
    // mind.
    set_transform(other, TransformState::make_pos_quat_scale_shear
                  (pos, quat, rel_transform->get_scale(), rel_transform->get_shear()));
    node()->reset_prev_transform();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_hpr_scale
//       Access: Published
//  Description: Sets the rotation and scale components of the
//               transform, leaving translation untouched.  This, or
//               set_pos_hpr_scale, is the preferred way to update a
//               transform when both hpr and scale are to be changed.
////////////////////////////////////////////////////////////////////
void NodePath::
set_hpr_scale(const NodePath &other, const LVecBase3f &hpr, const LVecBase3f &scale) {
  // We don't bother trying very hard to preserve pos across this
  // operation, unlike the work we do above to preserve hpr or scale,
  // since it generally doesn't matter that much if pos is off by a
  // few thousandths.
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform(other);
  transform = TransformState::make_pos_hpr_scale_shear
    (transform->get_pos(), hpr, scale, transform->get_shear());
  set_transform(other, transform);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_quat_scale
//       Access: Published
//  Description: Sets the rotation and scale components of the
//               transform, leaving translation untouched.  This, or
//               set_pos_quat_scale, is the preferred way to update a
//               transform when both quat and scale are to be changed.
////////////////////////////////////////////////////////////////////
void NodePath::
set_quat_scale(const NodePath &other, const LQuaternionf &quat, 
               const LVecBase3f &scale) {
  // We don't bother trying very hard to preserve pos across this
  // operation, unlike the work we do above to preserve quat or scale,
  // since it generally doesn't matter that much if pos is off by a
  // few thousandths.
  nassertv_always(!is_empty());
  CPT(TransformState) transform = get_transform(other);
  transform = TransformState::make_pos_quat_scale_shear
    (transform->get_pos(), quat, scale, transform->get_shear());
  set_transform(other, transform);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_hpr_scale
//       Access: Published
//  Description: Completely replaces the transform with new
//               translation, rotation, and scale components, relative
//               to the other node, implicitly setting shear to 0.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_hpr_scale(const NodePath &other,
                  const LVecBase3f &pos, const LVecBase3f &hpr,
                  const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  set_transform(other, TransformState::make_pos_hpr_scale
                (pos, hpr, scale));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_quat_scale
//       Access: Published
//  Description: Completely replaces the transform with new
//               translation, rotation, and scale components, relative
//               to the other node, implicitly setting shear to 0.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_quat_scale(const NodePath &other,
                   const LVecBase3f &pos, const LQuaternionf &quat,
                   const LVecBase3f &scale) {
  nassertv_always(!is_empty());
  set_transform(other, TransformState::make_pos_quat_scale
                (pos, quat, scale));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_hpr_scale_shear
//       Access: Published
//  Description: Completely replaces the transform with new
//               translation, rotation, scale, and shear components,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_hpr_scale_shear(const NodePath &other,
                        const LVecBase3f &pos, const LVecBase3f &hpr,
                        const LVecBase3f &scale, const LVecBase3f &shear) {
  nassertv_always(!is_empty());
  set_transform(other, TransformState::make_pos_hpr_scale_shear
                (pos, hpr, scale, shear));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_pos_quat_scale_shear
//       Access: Published
//  Description: Completely replaces the transform with new
//               translation, rotation, scale, and shear components,
//               relative to the other node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_pos_quat_scale_shear(const NodePath &other,
                         const LVecBase3f &pos, const LQuaternionf &quat,
                         const LVecBase3f &scale, const LVecBase3f &shear) {
  nassertv_always(!is_empty());
  set_transform(other, TransformState::make_pos_quat_scale_shear
                (pos, quat, scale, shear));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_mat
//       Access: Published
//  Description: Returns the matrix that describes the coordinate
//               space of the bottom node, relative to the other
//               path's bottom node's coordinate space.
////////////////////////////////////////////////////////////////////
LMatrix4f NodePath::
get_mat(const NodePath &other) const {
  CPT(TransformState) transform = get_transform(other);
  // We can't safely return a reference to the matrix, because we
  // can't assume the transform won't go away when the function
  // returns.  If the transform was partially modified by, say, a
  // CompassEffect, it won't be stored in the cache, and thus we might
  // have the only reference to it.
  return transform->get_mat();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_mat
//       Access: Published
//  Description: Converts the indicated matrix from the other's
//               coordinate space to the local coordinate space, and
//               applies it to the node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_mat(const NodePath &other, const LMatrix4f &mat) {
  nassertv_always(!is_empty());
  set_transform(other, TransformState::make_mat(mat));
  node()->reset_prev_transform();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_relative_point
//       Access: Published
//  Description: Given that the indicated point is in the coordinate
//               system of the other node, returns the same point in
//               this node's coordinate system.
////////////////////////////////////////////////////////////////////
LPoint3f NodePath::
get_relative_point(const NodePath &other, const LVecBase3f &point) const {
  CPT(TransformState) transform = other.get_transform(*this);
  LPoint3f rel_point = LPoint3f(point) * transform->get_mat();
  return rel_point;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_relative_vector
//       Access: Published
//  Description: Given that the indicated vector is in the coordinate
//               system of the other node, returns the same vector in
//               this node's coordinate system.
////////////////////////////////////////////////////////////////////
LVector3f NodePath::
get_relative_vector(const NodePath &other, const LVecBase3f &vec) const {
  CPT(TransformState) transform = other.get_transform(*this);
  LVector3f rel_vector = LVector3f(vec) * transform->get_mat();
  return rel_vector;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::look_at
//       Access: Published
//  Description: Sets the transform on this NodePath so that it
//               rotates to face the indicated point in space, which
//               is relative to the other NodePath.
////////////////////////////////////////////////////////////////////
void NodePath::
look_at(const NodePath &other, const LPoint3f &point, const LVector3f &up) {
  nassertv_always(!is_empty());

  CPT(TransformState) transform = other.get_transform(get_parent());
  LPoint3f rel_point = point * transform->get_mat();

  LPoint3f pos = get_pos();

  LQuaternionf quat;
  ::look_at(quat, rel_point - pos, up);
  set_quat(quat);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::heads_up
//       Access: Published
//  Description: Behaves like look_at(), but with a strong preference
//               to keeping the up vector oriented in the indicated
//               "up" direction.
////////////////////////////////////////////////////////////////////
void NodePath::
heads_up(const NodePath &other, const LPoint3f &point, const LVector3f &up) {
  nassertv_always(!is_empty());

  CPT(TransformState) transform = other.get_transform(get_parent());
  LPoint3f rel_point = point * transform->get_mat();

  LPoint3f pos = get_pos();

  LQuaternionf quat;
  ::heads_up(quat, rel_point - pos, up);
  set_quat(quat);
}


////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_color
//       Access: Published
//  Description: Applies a scene-graph color to the referenced node.
//               This color will apply to all geometry at this level
//               and below (that does not specify a new color or a
//               set_color_off()).
////////////////////////////////////////////////////////////////////
void NodePath::
set_color(float r, float g, float b, float a,
          int priority) {
  set_color(Colorf(r, g, b, a), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_color
//       Access: Published
//  Description: Applies a scene-graph color to the referenced node.
//               This color will apply to all geometry at this level
//               and below (that does not specify a new color or a
//               set_color_off()).
////////////////////////////////////////////////////////////////////
void NodePath::
set_color(const Colorf &color, int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(ColorAttrib::make_flat(color), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_color_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using the geometry color.  This is normally the
//               default, but it may be useful to use this to
//               contradict set_color() at a higher node level (or,
//               with a priority, to override a set_color() at a lower
//               level).
////////////////////////////////////////////////////////////////////
void NodePath::
set_color_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(ColorAttrib::make_vertex(), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_color
//       Access: Published
//  Description: Completely removes any color adjustment from the node.
//               This allows the natural color of the geometry, or
//               whatever color transitions might be otherwise
//               affecting the geometry, to show instead.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_color() {
  nassertv_always(!is_empty());
  node()->clear_attrib(ColorAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_color
//       Access: Published
//  Description: Returns true if a color has been applied to the given
//               node, false otherwise.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_color() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(ColorAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_color
//       Access: Published
//  Description: Returns the color that has been assigned to the node,
//               or black if no color has been assigned.
////////////////////////////////////////////////////////////////////
Colorf NodePath::
get_color() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(ColorAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ColorAttrib *ca = DCAST(ColorAttrib, attrib);
    if (ca->get_color_type() == ColorAttrib::T_flat) {
      return ca->get_color();
    }
  }

  pgraph_cat.warning()
    << "get_color() called on " << *this << " which has no color set.\n";

  return Colorf(1.0f, 1.0f, 1.0f, 1.0f);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_color_scale
//       Access: Published
//  Description: Returns true if a color scale has been applied
//               to the referenced node, false otherwise.  It is still
//               possible that color at this node might have been
//               scaled by an ancestor node.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_color_scale() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(ColorScaleAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_color_scale
//       Access: Published
//  Description: Completely removes any color scale from the
//               referenced node.  This is preferable to simply
//               setting the color scale to identity, as it also
//               removes the overhead associated with having a color
//               scale at all.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_color_scale() {
  nassertv_always(!is_empty());
  node()->clear_attrib(ColorScaleAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_color_scale
//       Access: Published
//  Description: Sets the color scale component of the transform,
//               leaving translation and rotation untouched.
////////////////////////////////////////////////////////////////////
void NodePath::
set_color_scale(const LVecBase4f &scale, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ColorScaleAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(ColorScaleAttrib::get_class_type()));
    const ColorScaleAttrib *csa = DCAST(ColorScaleAttrib, attrib);

    // Modify the existing ColorScaleAttrib to add the indicated
    // colorScale.
    node()->set_attrib(csa->set_scale(scale), priority);

  } else {
    // Create a new ColorScaleAttrib for this node.
    node()->set_attrib(ColorScaleAttrib::make(scale), priority);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_color_scale_off
//       Access: Published
//  Description: Disables any color scale attribute inherited from
//               above.  This is not the same thing as
//               clear_color_scale(), which undoes any previous
//               set_color_scale() operation on this node; rather,
//               this actively disables any set_color_scale() that
//               might be inherited from a parent node.  This also
//               disables set_alpha_scale() at the same time.
//
//               It is legal to specify a new color scale on the same
//               node with a subsequent call to set_color_scale() or
//               set_alpha_scale(); this new scale will apply to lower
//               geometry.
////////////////////////////////////////////////////////////////////
void NodePath::
set_color_scale_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(ColorScaleAttrib::make_off(), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_alpha_scale
//       Access: Published
//  Description: Sets the alpha scale component of the transform
//               without (much) affecting the color scale.  Note that
//               any priority specified will also apply to the color
//               scale.
////////////////////////////////////////////////////////////////////
void NodePath::
set_alpha_scale(float scale, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ColorScaleAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(ColorScaleAttrib::get_class_type()));
    const ColorScaleAttrib *csa = DCAST(ColorScaleAttrib, attrib);

    // Modify the existing ColorScaleAttrib to add the indicated
    // colorScale.
    const LVecBase4f &sc = csa->get_scale();
    node()->set_attrib(csa->set_scale(LVecBase4f(sc[0], sc[1], sc[2], scale)), priority);

  } else {
    // Create a new ColorScaleAttrib for this node.
    node()->set_attrib(ColorScaleAttrib::make(LVecBase4f(1.0f, 1.0f, 1.0f, scale)), priority);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_all_color_scale
//       Access: Published
//  Description: Scales all the color components of the object by the
//               same amount, darkening the object, without (much)
//               affecting alpha.  Note that any priority specified
//               will also apply to the alpha scale.
////////////////////////////////////////////////////////////////////
void NodePath::
set_all_color_scale(float scale, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ColorScaleAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(ColorScaleAttrib::get_class_type()));
    const ColorScaleAttrib *csa = DCAST(ColorScaleAttrib, attrib);

    // Modify the existing ColorScaleAttrib to add the indicated
    // colorScale.
    const LVecBase4f &sc = csa->get_scale();
    node()->set_attrib(csa->set_scale(LVecBase4f(scale, scale, scale, sc[3])), priority);

  } else {
    // Create a new ColorScaleAttrib for this node.
    node()->set_attrib(ColorScaleAttrib::make(LVecBase4f(scale, scale, scale, 1.0f)), priority);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_color_scale
//       Access: Published
//  Description: Returns the complete color scale vector that has been
//               applied to this node via a previous call to
//               set_color_scale() and/or set_alpha_scale(), or all
//               1's (identity) if no scale has been applied to this
//               particular node.
////////////////////////////////////////////////////////////////////
const LVecBase4f &NodePath::
get_color_scale() const {
  static const LVecBase4f ident_scale(1.0f, 1.0f, 1.0f, 1.0f);
  nassertr_always(!is_empty(), ident_scale);
  const RenderAttrib *attrib =
    node()->get_attrib(ColorScaleAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ColorScaleAttrib *csa = DCAST(ColorScaleAttrib, attrib);
    return csa->get_scale();
  }

  return ident_scale;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_light
//       Access: Published
//  Description: Adds the indicated Light or PolylightNode to the list
//               of lights that illuminate geometry at this node and
//               below.  The light itself should be parented into the
//               scene graph elsewhere, to represent the light's
//               position in space; but until set_light() is called it
//               will illuminate no geometry.
////////////////////////////////////////////////////////////////////
void NodePath::
set_light(const NodePath &light, int priority) {
  nassertv_always(!is_empty());
  if (!light.is_empty()) {
    Light *light_obj = light.node()->as_light();
    if (light_obj != (Light *)NULL) {
      // It's an actual Light object.
      const RenderAttrib *attrib =
        node()->get_attrib(LightAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        priority = max(priority,
                       node()->get_state()->get_override(LightAttrib::get_class_type()));
        const LightAttrib *la = DCAST(LightAttrib, attrib);
        
        // Modify the existing LightAttrib to add the indicated
        // light.
        node()->set_attrib(la->add_on_light(light), priority);
        
      } else {
        // Create a new LightAttrib for this node.
        CPT(LightAttrib) la = DCAST(LightAttrib, LightAttrib::make());
        node()->set_attrib(la->add_on_light(light), priority);
      }
      return;

    } else if (light.node()->is_of_type(PolylightNode::get_class_type())) {
      // It's a Polylight object.
      if (priority != 0) {
        // PolylightEffects can't have a priority, since they're just
        // an effect to be applied immediately.
        pgraph_cat.warning()
          << "Ignoring priority on set_light(" << light << ")\n";
      }

      const RenderEffect *effect =
        node()->get_effect(PolylightEffect::get_class_type());
      if (effect != (const RenderEffect *)NULL) {
        const PolylightEffect *ple = DCAST(PolylightEffect, effect);
        
        // Modify the existing PolylightEffect to add the indicated
        // light.
        node()->set_effect(ple->add_light(light));
        
      } else {
        // Create a new PolylightEffect for this node.
        CPT(PolylightEffect) ple = DCAST(PolylightEffect, PolylightEffect::make());
        node()->set_effect(ple->add_light(light));
      }
      return;
    }
  }
  nassert_raise("Not a Light object.");
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_light_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using no lights at all.  This is different
//               from not specifying a light; rather, this
//               specifically contradicts set_light() at a higher
//               node level (or, with a priority, overrides a
//               set_light() at a lower level).
//
//               If no lights are in effect on a particular piece of
//               geometry, that geometry is rendered with lighting
//               disabled.
////////////////////////////////////////////////////////////////////
void NodePath::
set_light_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(LightAttrib::make_all_off(), priority);
  node()->clear_effect(PolylightEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_light_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               without using the indicated Light.  This is different
//               from not specifying the Light; rather, this
//               specifically contradicts set_light() at a higher node
//               level (or, with a priority, overrides a set_light()
//               at a lower level).
//
//               This interface does not support PolylightNodes, which
//               cannot be turned off at a lower level.
////////////////////////////////////////////////////////////////////
void NodePath::
set_light_off(const NodePath &light, int priority) {
  nassertv_always(!is_empty());

  if (!light.is_empty()) {
    Light *light_obj = light.node()->as_light();
    if (light_obj != (Light *)NULL) {
      const RenderAttrib *attrib =
        node()->get_attrib(LightAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        priority = max(priority,
                       node()->get_state()->get_override(LightAttrib::get_class_type()));
        const LightAttrib *la = DCAST(LightAttrib, attrib);
        
        // Modify the existing LightAttrib to add the indicated light
        // to the "off" list.  This also, incidentally, removes it from
        // the "on" list if it is there.
        node()->set_attrib(la->add_off_light(light), priority);
        
      } else {
        // Create a new LightAttrib for this node that turns off the
        // indicated light.
        CPT(LightAttrib) la = DCAST(LightAttrib, LightAttrib::make());
        node()->set_attrib(la->add_off_light(light), priority);
      }
      return;
    }
  }
  nassert_raise("Not a Light object.");
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_light
//       Access: Published
//  Description: Completely removes any lighting operations that may
//               have been set via set_light() or set_light_off()
//               from this particular node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_light() {
  nassertv_always(!is_empty());
  node()->clear_attrib(LightAttrib::get_class_type());
  node()->clear_effect(PolylightEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_light
//       Access: Published
//  Description: Removes any reference to the indicated Light or
//               PolylightNode from the NodePath.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_light(const NodePath &light) {
  nassertv_always(!is_empty());

  if (!light.is_empty()) {
    Light *light_obj = light.node()->as_light();
    if (light_obj != (Light *)NULL) {
      const RenderAttrib *attrib =
        node()->get_attrib(LightAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        CPT(LightAttrib) la = DCAST(LightAttrib, attrib);
        la = DCAST(LightAttrib, la->remove_on_light(light));
        la = DCAST(LightAttrib, la->remove_off_light(light));
        
        if (la->is_identity()) {
          node()->clear_attrib(LightAttrib::get_class_type());
          
        } else {
          int priority = node()->get_state()->get_override(LightAttrib::get_class_type());
          node()->set_attrib(la, priority);
        }
      }
      return;

    } else if (light.node()->is_of_type(PolylightNode::get_class_type())) {
      const RenderEffect *effect =
        node()->get_effect(PolylightEffect::get_class_type());
      if (effect != (const RenderEffect *)NULL) {
        CPT(PolylightEffect) ple = DCAST(PolylightEffect, effect);
        ple = DCAST(PolylightEffect, ple->remove_light(light));
        node()->set_effect(ple);
      }
      return;
    }
  }
  nassert_raise("Not a Light object.");
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_light
//       Access: Published
//  Description: Returns true if the indicated Light or PolylightNode
//               has been specifically enabled on this particular
//               node.  This means that someone called set_light() on
//               this node with the indicated light.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_light(const NodePath &light) const {
  nassertr_always(!is_empty(), false);

  if (!light.is_empty()) {
    Light *light_obj = light.node()->as_light();
    if (light_obj != (Light *)NULL) {
      const RenderAttrib *attrib =
        node()->get_attrib(LightAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const LightAttrib *la = DCAST(LightAttrib, attrib);
        return la->has_on_light(light);
      }
      return false;

    } else if (light.node()->is_of_type(PolylightNode::get_class_type())) {
      const RenderEffect *effect =
        node()->get_effect(PolylightEffect::get_class_type());
      if (effect != (const RenderEffect *)NULL) {
        const PolylightEffect *ple = DCAST(PolylightEffect, effect);
        return ple->has_light(light);
      }
      return false;
    }
  }
  nassert_raise("Not a Light object.");
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_light_off
//       Access: Published
//  Description: Returns true if all Lights have been specifically
//               disabled on this particular node.  This means that
//               someone called set_light_off() on this node with no
//               parameters.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_light_off() const {
  nassertr_always(!is_empty(), false);

  const RenderAttrib *attrib =
    node()->get_attrib(LightAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const LightAttrib *la = DCAST(LightAttrib, attrib);
    return la->has_all_off();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_light_off
//       Access: Published
//  Description: Returns true if the indicated Light has been
//               specifically disabled on this particular node.  This
//               means that someone called set_light_off() on this
//               node with the indicated light.
//
//               This interface does not support PolylightNodes, which
//               cannot be turned off at a lower level.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_light_off(const NodePath &light) const {
  nassertr_always(!is_empty(), false);
  if (!light.is_empty()) {
    Light *light_obj = light.node()->as_light();
    if (light_obj != (Light *)NULL) {
      const RenderAttrib *attrib =
        node()->get_attrib(LightAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const LightAttrib *la = DCAST(LightAttrib, attrib);
        return la->has_off_light(light);
      }
    }
  }
  nassert_raise("Not a Light object.");
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_clip_plane
//       Access: Published
//  Description: Adds the indicated clipping plane to the list of
//               planes that apply to geometry at this node and below.
//               The clipping plane itself, a PlaneNode, should be
//               parented into the scene graph elsewhere, to represent
//               the plane's position in space; but until
//               set_clip_plane() is called it will clip no geometry.
////////////////////////////////////////////////////////////////////
void NodePath::
set_clip_plane(const NodePath &clip_plane, int priority) {
  nassertv_always(!is_empty());
  if (!clip_plane.is_empty() && clip_plane.node()->is_of_type(PlaneNode::get_class_type())) {
    const RenderAttrib *attrib =
      node()->get_attrib(ClipPlaneAttrib::get_class_type());
    if (attrib != (const RenderAttrib *)NULL) {
      priority = max(priority,
                     node()->get_state()->get_override(ClipPlaneAttrib::get_class_type()));
      const ClipPlaneAttrib *la = DCAST(ClipPlaneAttrib, attrib);
      
      // Modify the existing ClipPlaneAttrib to add the indicated
      // clip_plane.
      node()->set_attrib(la->add_on_plane(clip_plane), priority);
      
    } else {
      // Create a new ClipPlaneAttrib for this node.
      CPT(ClipPlaneAttrib) la = DCAST(ClipPlaneAttrib, ClipPlaneAttrib::make());
      node()->set_attrib(la->add_on_plane(clip_plane), priority);
    }
    return;
  }
  nassert_raise("Not a PlaneNode object.");
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_clip_plane_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using no clip_planes at all.  This is different
//               from not specifying a clip_plane; rather, this
//               specifically contradicts set_clip_plane() at a higher
//               node level (or, with a priority, overrides a
//               set_clip_plane() at a lower level).
//
//               If no clip_planes are in effect on a particular piece
//               of geometry, that geometry is rendered without being
//               clipped (other than by the viewing frustum).
////////////////////////////////////////////////////////////////////
void NodePath::
set_clip_plane_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(ClipPlaneAttrib::make_all_off(), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_clip_plane_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               without being clipped by the indicated PlaneNode.
//               This is different from not specifying the PlaneNode;
//               rather, this specifically contradicts
//               set_clip_plane() at a higher node level (or, with a
//               priority, overrides a set_clip_plane() at a lower
//               level).
////////////////////////////////////////////////////////////////////
void NodePath::
set_clip_plane_off(const NodePath &clip_plane, int priority) {
  nassertv_always(!is_empty());

  if (!clip_plane.is_empty() && clip_plane.node()->is_of_type(PlaneNode::get_class_type())) {
    const RenderAttrib *attrib =
      node()->get_attrib(ClipPlaneAttrib::get_class_type());
    if (attrib != (const RenderAttrib *)NULL) {
      priority = max(priority,
                     node()->get_state()->get_override(ClipPlaneAttrib::get_class_type()));
      const ClipPlaneAttrib *la = DCAST(ClipPlaneAttrib, attrib);
      
      // Modify the existing ClipPlaneAttrib to add the indicated clip_plane
      // to the "off" list.  This also, incidentally, removes it from
      // the "on" list if it is there.
      node()->set_attrib(la->add_off_plane(clip_plane), priority);
      
    } else {
      // Create a new ClipPlaneAttrib for this node that turns off the
      // indicated clip_plane.
      CPT(ClipPlaneAttrib) la = DCAST(ClipPlaneAttrib, ClipPlaneAttrib::make());
      node()->set_attrib(la->add_off_plane(clip_plane), priority);
    }
    return;
  }
  nassert_raise("Not a PlaneNode object.");
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_clip_plane
//       Access: Published
//  Description: Completely removes any clip planes that may have been
//               set via set_clip_plane() or set_clip_plane_off() from
//               this particular node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_clip_plane() {
  nassertv_always(!is_empty());
  node()->clear_attrib(ClipPlaneAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_clip_plane
//       Access: Published
//  Description: Removes any reference to the indicated clipping plane
//               from the NodePath.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_clip_plane(const NodePath &clip_plane) {
  nassertv_always(!is_empty());

  if (!clip_plane.is_empty() && clip_plane.node()->is_of_type(PlaneNode::get_class_type())) {
    const RenderAttrib *attrib =
      node()->get_attrib(ClipPlaneAttrib::get_class_type());
    if (attrib != (const RenderAttrib *)NULL) {
      CPT(ClipPlaneAttrib) la = DCAST(ClipPlaneAttrib, attrib);
      la = DCAST(ClipPlaneAttrib, la->remove_on_plane(clip_plane));
      la = DCAST(ClipPlaneAttrib, la->remove_off_plane(clip_plane));
        
      if (la->is_identity()) {
        node()->clear_attrib(ClipPlaneAttrib::get_class_type());
          
      } else {
        int priority = node()->get_state()->get_override(ClipPlaneAttrib::get_class_type());
        node()->set_attrib(la, priority);
      }
    }
    return;
  }
  nassert_raise("Not a PlaneNode object.");
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_clip_plane
//       Access: Published
//  Description: Returns true if the indicated clipping plane has been
//               specifically applied to this particular node.  This
//               means that someone called set_clip_plane() on this
//               node with the indicated clip_plane.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_clip_plane(const NodePath &clip_plane) const {
  nassertr_always(!is_empty(), false);

  if (!clip_plane.is_empty() && clip_plane.node()->is_of_type(PlaneNode::get_class_type())) {
    const RenderAttrib *attrib =
      node()->get_attrib(ClipPlaneAttrib::get_class_type());
    if (attrib != (const RenderAttrib *)NULL) {
      const ClipPlaneAttrib *la = DCAST(ClipPlaneAttrib, attrib);
      return la->has_on_plane(clip_plane);
    }
    return false;
  }
  nassert_raise("Not a PlaneNode object.");
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_clip_plane_off
//       Access: Published
//  Description: Returns true if all clipping planes have been
//               specifically disabled on this particular node.  This
//               means that someone called set_clip_plane_off() on
//               this node with no parameters.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_clip_plane_off() const {
  nassertr_always(!is_empty(), false);

  const RenderAttrib *attrib =
    node()->get_attrib(ClipPlaneAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ClipPlaneAttrib *la = DCAST(ClipPlaneAttrib, attrib);
    return la->has_all_off();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_clip_plane_off
//       Access: Published
//  Description: Returns true if the indicated clipping plane has been
//               specifically disabled on this particular node.  This
//               means that someone called set_clip_plane_off() on
//               this node with the indicated clip_plane.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_clip_plane_off(const NodePath &clip_plane) const {
  nassertr_always(!is_empty(), false);
  if (!clip_plane.is_empty() && clip_plane.node()->is_of_type(PlaneNode::get_class_type())) {
    const RenderAttrib *attrib =
      node()->get_attrib(ClipPlaneAttrib::get_class_type());
    if (attrib != (const RenderAttrib *)NULL) {
      const ClipPlaneAttrib *la = DCAST(ClipPlaneAttrib, attrib);
      return la->has_off_plane(clip_plane);
    }
  }
  nassert_raise("Not a PlaneNode object.");
  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_bin
//       Access: Published
//  Description: Assigns the geometry at this level and below to the
//               named rendering bin.  It is the user's responsibility
//               to ensure that such a bin already exists, either via
//               the cull-bin Configrc variable, or by explicitly
//               creating a GeomBin of the appropriate type at
//               runtime.
//
//               There are two default bins created when Panda is
//               started: "default" and "fixed".  Normally, all
//               geometry is assigned to "default" unless specified
//               otherwise.  This bin renders opaque geometry in
//               state-sorted order, followed by transparent geometry
//               sorted back-to-front.  If any geometry is assigned to
//               "fixed", this will be rendered following all the
//               geometry in "default", in the order specified by
//               draw_order for each piece of geometry so assigned.
//
//               The draw_order parameter is meaningful only for
//               GeomBinFixed type bins, e.g. "fixed".  Other kinds of
//               bins ignore it.
////////////////////////////////////////////////////////////////////
void NodePath::
set_bin(const string &bin_name, int draw_order, int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(CullBinAttrib::make(bin_name, draw_order), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_bin
//       Access: Published
//  Description: Completely removes any bin adjustment that may have
//               been set via set_bin() from this particular node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_bin() {
  nassertv_always(!is_empty());
  node()->clear_attrib(CullBinAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_bin
//       Access: Published
//  Description: Returns true if the node has been assigned to the a
//               particular rendering bin via set_bin(), false
//               otherwise.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_bin() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(CullBinAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_bin_name
//       Access: Published
//  Description: Returns the name of the bin that this particular node
//               was assigned to via set_bin(), or the empty string if
//               no bin was assigned.  See set_bin() and has_bin().
////////////////////////////////////////////////////////////////////
string NodePath::
get_bin_name() const {
  nassertr_always(!is_empty(), string());
  const RenderAttrib *attrib =
    node()->get_attrib(CullBinAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const CullBinAttrib *ba = DCAST(CullBinAttrib, attrib);
    return ba->get_bin_name();
  }

  return string();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_bin_draw_order
//       Access: Published
//  Description: Returns the drawing order associated with the bin
//               that this particular node was assigned to via
//               set_bin(), or 0 if no bin was assigned.  See
//               set_bin() and has_bin().
////////////////////////////////////////////////////////////////////
int NodePath::
get_bin_draw_order() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(CullBinAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const CullBinAttrib *ba = DCAST(CullBinAttrib, attrib);
    return ba->get_draw_order();
  }

  return 0;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_texture
//       Access: Published
//  Description: Adds the indicated texture to the list of textures
//               that will be rendered on the default texture stage.
//
//               This is the deprecated single-texture variant of this
//               method; it is now superceded by set_texture() that
//               accepts a stage and texture.  However, this method
//               may be used in the presence of multitexture if you
//               just want to adjust the default stage.
////////////////////////////////////////////////////////////////////
void NodePath::
set_texture(Texture *tex, int priority) {
  nassertv_always(!is_empty());
  PT(TextureStage) stage = TextureStage::get_default();
  set_texture(stage, tex, priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_texture
//       Access: Published
//  Description: Adds the indicated texture to the list of textures
//               that will be rendered on the indicated multitexture
//               stage.  If there are multiple texture stages
//               specified (possibly on multiple different nodes at
//               different levels), they will all be applied to
//               geometry together, according to the stage
//               specification set up in the TextureStage object.
////////////////////////////////////////////////////////////////////
void NodePath::
set_texture(TextureStage *stage, Texture *tex, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(TextureAttrib::get_class_type()));
    const TextureAttrib *tsa = DCAST(TextureAttrib, attrib);

    // Modify the existing TextureAttrib to add the indicated
    // texture.
    node()->set_attrib(tsa->add_on_stage(stage, tex), priority);

  } else {
    // Create a new TextureAttrib for this node.
    CPT(TextureAttrib) tsa = DCAST(TextureAttrib, TextureAttrib::make());
    node()->set_attrib(tsa->add_on_stage(stage, tex), priority);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_texture_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using no texture, on any stage.  This is different
//               from not specifying a texture; rather, this
//               specifically contradicts set_texture() at a higher
//               node level (or, with a priority, overrides a
//               set_texture() at a lower level).
////////////////////////////////////////////////////////////////////
void NodePath::
set_texture_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(TextureAttrib::make_all_off(), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_texture_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using no texture, on the indicated stage.  This is
//               different from not specifying a texture; rather, this
//               specifically contradicts set_texture() at a higher
//               node level (or, with a priority, overrides a
//               set_texture() at a lower level).
////////////////////////////////////////////////////////////////////
void NodePath::
set_texture_off(TextureStage *stage, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(TextureAttrib::get_class_type()));
    const TextureAttrib *tsa = DCAST(TextureAttrib, attrib);

    // Modify the existing TextureAttrib to add the indicated texture
    // to the "off" list.  This also, incidentally, removes it from
    // the "on" list if it is there.
    node()->set_attrib(tsa->add_off_stage(stage), priority);

  } else {
    // Create a new TextureAttrib for this node that turns off the
    // indicated stage.
    CPT(TextureAttrib) tsa = DCAST(TextureAttrib, TextureAttrib::make());
    node()->set_attrib(tsa->add_off_stage(stage), priority);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_texture
//       Access: Published
//  Description: Completely removes any texture adjustment that may
//               have been set via set_texture() or set_texture_off()
//               from this particular node.  This allows whatever
//               textures might be otherwise affecting the geometry to
//               show instead.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_texture() {
  nassertv_always(!is_empty());
  node()->clear_attrib(TextureAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_texture
//       Access: Published
//  Description: Removes any reference to the indicated texture stage
//               from the NodePath.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_texture(TextureStage *stage) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    CPT(TextureAttrib) tsa = DCAST(TextureAttrib, attrib);
    tsa = DCAST(TextureAttrib, tsa->remove_on_stage(stage));
    tsa = DCAST(TextureAttrib, tsa->remove_off_stage(stage));

    if (tsa->is_identity()) {
      node()->clear_attrib(TextureAttrib::get_class_type());

    } else {
      int priority = node()->get_state()->get_override(TextureAttrib::get_class_type());
      node()->set_attrib(tsa, priority);
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_texture
//       Access: Published
//  Description: Returns true if a texture has been applied to this
//               particular node via set_texture(), false otherwise.
//               This is not the same thing as asking whether the
//               geometry at this node will be rendered with
//               texturing, as there may be a texture in effect from a
//               higher or lower level.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_texture() const {
  return get_texture() != (Texture *)NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_texture
//       Access: Published
//  Description: Returns true if texturing has been specifically
//               enabled on this particular node for the indicated
//               stage.  This means that someone called
//               set_texture() on this node with the indicated stage
//               name, or the stage_name is the default stage_name,
//               and someone called set_texture() on this node.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_texture(TextureStage *stage) const {
  nassertr_always(!is_empty(), false);

  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    return ta->has_on_stage(stage);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_texture_off
//       Access: Published
//  Description: Returns true if texturing has been specifically
//               disabled on this particular node via
//               set_texture_off(), false otherwise.  This is not the
//               same thing as asking whether the geometry at this
//               node will be rendered untextured, as there may be a
//               texture in effect from a higher or lower level.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_texture_off() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    return ta->has_all_off();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_texture_off
//       Access: Published
//  Description: Returns true if texturing has been specifically
//               disabled on this particular node for the indicated
//               stage.  This means that someone called
//               set_texture_off() on this node with the indicated
//               stage name, or that someone called set_texture_off()
//               on this node to remove all stages.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_texture_off(TextureStage *stage) const {
  nassertr_always(!is_empty(), false);

  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    return ta->has_off_stage(stage);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_texture
//       Access: Published
//  Description: Returns the base-level texture that has been set on
//               this particular node, or NULL if no texture has been
//               set.  This is not necessarily the texture that will
//               be applied to the geometry at or below this level, as
//               another texture at a higher or lower level may
//               override.
//
//               See also find_texture().
////////////////////////////////////////////////////////////////////
Texture *NodePath::
get_texture() const {
  nassertr_always(!is_empty(), NULL);
  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    return ta->get_texture();
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_texture
//       Access: Published
//  Description: Returns the texture that has been set on the
//               indicated stage for this particular node, or NULL if
//               no texture has been set for this stage.
////////////////////////////////////////////////////////////////////
Texture *NodePath::
get_texture(TextureStage *stage) const {
  nassertr_always(!is_empty(), NULL);
  const RenderAttrib *attrib =
    node()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    return ta->get_on_texture(stage);
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader(Shader *sha, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ShaderAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(ShaderAttrib::get_class_type()));
    const ShaderAttrib *sa = DCAST(ShaderAttrib, attrib);
    node()->set_attrib(sa->set_shader(sha, priority));
  } else {
    // Create a new ShaderAttrib for this node.
    CPT(ShaderAttrib) sa = DCAST(ShaderAttrib, ShaderAttrib::make());
    node()->set_attrib(sa->set_shader(sha, priority));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_off
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_off(int priority) {
  set_shader(NULL, priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_shader
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
clear_shader() {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ShaderAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ShaderAttrib *sa = DCAST(ShaderAttrib, attrib);
    node()->set_attrib(sa->clear_shader());
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_shader
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
const Shader *NodePath::
get_shader() const {
  nassertr_always(!is_empty(), NULL);
  const RenderAttrib *attrib =
    node()->get_attrib(ShaderAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ShaderAttrib *sa = DCAST(ShaderAttrib, attrib);
    return sa->get_shader();
  }
  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(const ShaderInput *inp) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ShaderAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ShaderAttrib *sa = DCAST(ShaderAttrib, attrib);
    node()->set_attrib(sa->set_shader_input(inp));
  } else {
    // Create a new ShaderAttrib for this node.
    CPT(ShaderAttrib) sa = DCAST(ShaderAttrib, ShaderAttrib::make());
    node()->set_attrib(sa->set_shader_input(inp));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
const ShaderInput *NodePath::
get_shader_input(InternalName *id) const {
  nassertr_always(!is_empty(), NULL);
  
  const RenderAttrib *attrib =
    node()->get_attrib(ShaderAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ShaderAttrib *sa = DCAST(ShaderAttrib, attrib);
    return sa->get_shader_input(id);
  }
  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
clear_shader_input(InternalName *id) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(ShaderAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const ShaderAttrib *sa = DCAST(ShaderAttrib, attrib);
    node()->set_attrib(sa->clear_shader_input(id));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(InternalName *id, Texture *tex, int priority) {
  set_shader_input(new ShaderInput(id,tex,priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(InternalName *id, const NodePath &np, int priority) {
  set_shader_input(new ShaderInput(id,np,priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(InternalName *id, const LVector4f &v, int priority) {
  set_shader_input(new ShaderInput(id,v,priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(InternalName *id, double n1, double n2, double n3, double n4, int priority) {
  set_shader_input(new ShaderInput(id,LVector4f(n1,n2,n3,n4),priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(const string &id, Texture *tex, int priority) {
  set_shader_input(new ShaderInput(InternalName::make(id),tex,priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(const string &id, const NodePath &np, int priority) {
  set_shader_input(new ShaderInput(InternalName::make(id),np,priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(const string &id, const LVector4f &v, int priority) {
  set_shader_input(new ShaderInput(InternalName::make(id),v,priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
set_shader_input(const string &id, double n1, double n2, double n3, double n4, int priority) {
  set_shader_input(new ShaderInput(InternalName::make(id),LVector4f(n1,n2,n3,n4),priority));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
const ShaderInput *NodePath::
get_shader_input(const string &id) const {
  return get_shader_input(InternalName::make(id));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_shader_input
//       Access: Published
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
clear_shader_input(const string &id) {
  clear_shader_input(InternalName::make(id));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_tex_transform
//       Access: Published
//  Description: Sets the texture matrix on the current node to the
//               indicated transform for the given stage.
////////////////////////////////////////////////////////////////////
void NodePath::
set_tex_transform(TextureStage *stage, const TransformState *transform) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TexMatrixAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexMatrixAttrib *tma = DCAST(TexMatrixAttrib, attrib);

    // Modify the existing TexMatrixAttrib to add the indicated
    // stage.
    node()->set_attrib(tma->add_stage(stage, transform));

  } else {
    // Create a new TexMatrixAttrib for this node.
    node()->set_attrib(TexMatrixAttrib::make(stage, transform));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_tex_transform
//       Access: Published
//  Description: Removes all texture matrices from the current node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_tex_transform() {
  nassertv_always(!is_empty());
  node()->clear_attrib(TexMatrixAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_tex_transform
//       Access: Published
//  Description: Removes the texture matrix on the current node for
//               the given stage.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_tex_transform(TextureStage *stage) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TexMatrixAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    CPT(TexMatrixAttrib) tma = DCAST(TexMatrixAttrib, attrib);
    tma = DCAST(TexMatrixAttrib, tma->remove_stage(stage));

    if (tma->is_empty()) {
      node()->clear_attrib(TexMatrixAttrib::get_class_type());

    } else {
      node()->set_attrib(tma);
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_tex_transform
//       Access: Published
//  Description: Returns true if there is an explicit texture matrix
//               on the current node for the given stage.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_tex_transform(TextureStage *stage) const {
  nassertr_always(!is_empty(), false);

  const RenderAttrib *attrib =
    node()->get_attrib(TexMatrixAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexMatrixAttrib *tma = DCAST(TexMatrixAttrib, attrib);
    return tma->has_stage(stage);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_tex_transform
//       Access: Published
//  Description: Returns the texture matrix on the current node for the
//               given stage, or identity transform if there is no
//               explicit transform set for the given stage.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
get_tex_transform(TextureStage *stage) const {
  nassertr_always(!is_empty(), NULL);

  const RenderAttrib *attrib =
    node()->get_attrib(TexMatrixAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexMatrixAttrib *tma = DCAST(TexMatrixAttrib, attrib);
    return tma->get_transform(stage);
  }

  return TransformState::make_identity();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_tex_transform
//       Access: Published
//  Description: Sets the texture matrix on the current node to the
//               indicated transform for the given stage.
////////////////////////////////////////////////////////////////////
void NodePath::
set_tex_transform(const NodePath &other, TextureStage *stage, const TransformState *transform) {
  nassertv(_error_type == ET_ok && other._error_type == ET_ok);
  nassertv_always(!is_empty());

  CPT(RenderState) state = get_state(other);
  const RenderAttrib *attrib =
    state->get_attrib(TexMatrixAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexMatrixAttrib *tma = DCAST(TexMatrixAttrib, attrib);

    // Modify the existing TexMatrixAttrib to add the indicated
    // stage.
    state = state->add_attrib(tma->add_stage(stage, transform));

  } else {
    // Create a new TexMatrixAttrib for this node.
    state = state->add_attrib(TexMatrixAttrib::make(stage, transform));
  }

  // Now compose that with our parent's state.
  CPT(RenderState) rel_state;
  if (has_parent()) {
    rel_state = other.get_state(get_parent());
  } else {
    rel_state = other.get_state(NodePath());
  }
  CPT(RenderState) new_state = rel_state->compose(state);

  // And apply only the TexMatrixAttrib to the current node, leaving
  // the others unchanged.
  node()->set_attrib(new_state->get_attrib(TexMatrixAttrib::get_class_type()));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_tex_transform
//       Access: Published
//  Description: Returns the texture matrix on the current node for the
//               given stage, relative to the other node.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
get_tex_transform(const NodePath &other, TextureStage *stage) const {
  nassertr(_error_type == ET_ok && other._error_type == ET_ok, TransformState::make_identity());

  CPT(RenderState) state = get_state(other);
  const RenderAttrib *attrib =
    state->get_attrib(TexMatrixAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexMatrixAttrib *tma = DCAST(TexMatrixAttrib, attrib);
    return tma->get_transform(stage);
  }

  return TransformState::make_identity();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_tex_gen
//       Access: Published
//  Description: Enables automatic texture coordinate generation for
//               the indicated texture stage.
////////////////////////////////////////////////////////////////////
void NodePath::
set_tex_gen(TextureStage *stage, RenderAttrib::TexGenMode mode, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TexGenAttrib::get_class_type());

  CPT(TexGenAttrib) tga;

  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(TextureAttrib::get_class_type()));
    tga = DCAST(TexGenAttrib, attrib);

  } else {
    tga = DCAST(TexGenAttrib, TexGenAttrib::make());
  }

  node()->set_attrib(tga->add_stage(stage, mode), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_tex_gen
//       Access: Published
//  Description: Enables automatic texture coordinate generation for
//               the indicated texture stage.  This version of this
//               method is useful when setting M_light_vector, which
//               requires the name of the texture coordinate set that
//               supplies the tangent and binormal, as well as the
//               specific light to generate coordinates for.
////////////////////////////////////////////////////////////////////
void NodePath::
set_tex_gen(TextureStage *stage, RenderAttrib::TexGenMode mode, 
            const string &source_name, const NodePath &light, int priority) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TexGenAttrib::get_class_type());

  CPT(TexGenAttrib) tga;

  if (attrib != (const RenderAttrib *)NULL) {
    priority = max(priority,
                   node()->get_state()->get_override(TextureAttrib::get_class_type()));
    tga = DCAST(TexGenAttrib, attrib);

  } else {
    tga = DCAST(TexGenAttrib, TexGenAttrib::make());
  }

  node()->set_attrib(tga->add_stage(stage, mode, source_name, light), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_tex_gen
//       Access: Published
//  Description: Removes the texture coordinate generation mode from
//               all texture stages on this node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_tex_gen() {
  nassertv_always(!is_empty());
  node()->clear_attrib(TexGenAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_tex_gen
//       Access: Published
//  Description: Disables automatic texture coordinate generation for
//               the indicated texture stage.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_tex_gen(TextureStage *stage) {
  nassertv_always(!is_empty());

  const RenderAttrib *attrib =
    node()->get_attrib(TexGenAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    CPT(TexGenAttrib) tga = DCAST(TexGenAttrib, attrib);
    tga = DCAST(TexGenAttrib, tga->remove_stage(stage));

    if (tga->is_empty()) {
      node()->clear_attrib(TexGenAttrib::get_class_type());

    } else {
      node()->set_attrib(tga);
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_tex_gen
//       Access: Published
//  Description: Returns true if there is a mode for automatic texture
//               coordinate generation on the current node for the
//               given stage.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_tex_gen(TextureStage *stage) const {
  nassertr_always(!is_empty(), false);

  const RenderAttrib *attrib =
    node()->get_attrib(TexGenAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexGenAttrib *tga = DCAST(TexGenAttrib, attrib);
    return tga->has_stage(stage);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_tex_gen
//       Access: Published
//  Description: Returns the texture coordinate generation mode for
//               the given stage, or M_off if there is no explicit
//               mode set for the given stage.
////////////////////////////////////////////////////////////////////
RenderAttrib::TexGenMode NodePath::
get_tex_gen(TextureStage *stage) const {
  nassertr_always(!is_empty(), TexGenAttrib::M_off);

  const RenderAttrib *attrib =
    node()->get_attrib(TexGenAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexGenAttrib *tga = DCAST(TexGenAttrib, attrib);
    return tga->get_mode(stage);
  }

  return TexGenAttrib::M_off;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_tex_gen_light
//       Access: Published
//  Description: Returns the particular Light set for the indicated
//               texgen mode's texture stage, or empty NodePath if no
//               light is set.  This is only meaningful if the texgen
//               mode (returned by get_tex_gen()) is M_light_vector.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
get_tex_gen_light(TextureStage *stage) const {
  nassertr_always(!is_empty(), NodePath::fail());

  const RenderAttrib *attrib =
    node()->get_attrib(TexGenAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TexGenAttrib *tga = DCAST(TexGenAttrib, attrib);
    return tga->get_light(stage);
  }

  return NodePath();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_tex_projector
//       Access: Published
//  Description: Establishes a TexProjectorEffect on this node, which
//               can be used to establish projective texturing (but
//               see also the NodePath::project_texture() convenience
//               function), or it can be used to bind this node's
//               texture transform to particular node's position in
//               space, allowing a LerpInterval (for instance) to
//               adjust this node's texture coordinates.
////////////////////////////////////////////////////////////////////
void NodePath::
set_tex_projector(TextureStage *stage, const NodePath &from, const NodePath &to) {
  nassertv_always(!is_empty());

  const RenderEffect *effect =
    node()->get_effect(TexProjectorEffect::get_class_type());

  CPT(TexProjectorEffect) tpe;

  if (effect != (const RenderEffect *)NULL) {
    tpe = DCAST(TexProjectorEffect, effect);

  } else {
    tpe = DCAST(TexProjectorEffect, TexProjectorEffect::make());
  }

  node()->set_effect(tpe->add_stage(stage, from, to));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_tex_projector
//       Access: Published
//  Description: Removes the TexProjectorEffect for the indicated
//               stage from this node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_tex_projector(TextureStage *stage) {
  nassertv_always(!is_empty());

  const RenderEffect *effect =
    node()->get_effect(TexProjectorEffect::get_class_type());
  if (effect != (const RenderEffect *)NULL) {
    CPT(TexProjectorEffect) tpe = DCAST(TexProjectorEffect, effect);
    tpe = DCAST(TexProjectorEffect, tpe->remove_stage(stage));

    if (tpe->is_empty()) {
      node()->clear_effect(TexProjectorEffect::get_class_type());

    } else {
      node()->set_effect(tpe);
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_tex_projector
//       Access: Published
//  Description: Removes the TexProjectorEffect for all stages from
//               this node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_tex_projector() {
  nassertv_always(!is_empty());
  node()->clear_effect(TexProjectorEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_tex_projector
//       Access: Published
//  Description: Returns true if this node has a TexProjectorEffect
//               for the indicated stage, false otherwise.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_tex_projector(TextureStage *stage) const {
  nassertr_always(!is_empty(), false);

  const RenderEffect *effect =
    node()->get_effect(TexProjectorEffect::get_class_type());
  if (effect != (const RenderEffect *)NULL) {
    const TexProjectorEffect *tpe = DCAST(TexProjectorEffect, effect);
    return tpe->has_stage(stage);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_tex_projector_from
//       Access: Published
//  Description: Returns the "from" node associated with the
//               TexProjectorEffect on the indicated stage.  The
//               relative transform between the "from" and the "to"
//               nodes is automatically applied to the texture
//               transform each frame.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
get_tex_projector_from(TextureStage *stage) const {
  nassertr_always(!is_empty(), NodePath::fail());

  const RenderEffect *effect =
    node()->get_effect(TexProjectorEffect::get_class_type());
  if (effect != (const RenderEffect *)NULL) {
    const TexProjectorEffect *tpe = DCAST(TexProjectorEffect, effect);
    return tpe->get_from(stage);
  }

  return NodePath::not_found();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_tex_projector_to
//       Access: Published
//  Description: Returns the "to" node associated with the
//               TexProjectorEffect on the indicated stage.  The
//               relative transform between the "from" and the "to"
//               nodes is automatically applied to the texture
//               transform each frame.
////////////////////////////////////////////////////////////////////
NodePath NodePath::
get_tex_projector_to(TextureStage *stage) const {
  nassertr_always(!is_empty(), NodePath::fail());

  const RenderEffect *effect =
    node()->get_effect(TexProjectorEffect::get_class_type());
  if (effect != (const RenderEffect *)NULL) {
    const TexProjectorEffect *tpe = DCAST(TexProjectorEffect, effect);
    return tpe->get_to(stage);
  }

  return NodePath::not_found();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::project_texture
//       Access: Published
//  Description: A convenience function to enable projective texturing
//               at this node level and below, using the indicated
//               NodePath (which should contain a LensNode) as the
//               projector.
////////////////////////////////////////////////////////////////////
void NodePath::
project_texture(TextureStage *stage, Texture *tex, const NodePath &projector) {
  nassertv(!projector.is_empty() && projector.node()->is_of_type(LensNode::get_class_type()));
  set_texture(stage, tex);
  set_tex_gen(stage, TexGenAttrib::M_world_position);
  set_tex_projector(stage, NodePath(), projector);
}


////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_normal_map
//       Access: Published
//  Description: A convenience function to set up a normal map on this
//               geometry.  This uses the single highest-priority
//               light on the object only.  It also requires
//               multitexture, and consumes at least two texture
//               stages, in addition to what may already be in use.
//
//               The normal_map parameter is the texture that contains
//               the normal map information (with a 3-d delta vector
//               encoded into the r,g,b of each texel).  texcoord_name is
//               the name of the texture coordinate set that contains
//               the tangent and binormal we wish to use.  If
//               preserve_color is true, then one additional texture
//               stage is consumed to blend in the geometry's original
//               vertex color.
//
//               Only one normal map may be in effect through this
//               interface at any given time.
////////////////////////////////////////////////////////////////////
void NodePath::
set_normal_map(Texture *normal_map, const string &texcoord_name,
               bool preserve_color) {
  clear_normal_map();

  // First, we apply the normal map itself, to the bottom layer.
  PT(TextureStage) normal_map_ts = new TextureStage("__normal_map");
  normal_map_ts->set_texcoord_name(texcoord_name);
  normal_map_ts->set_sort(-20);
  normal_map_ts->set_mode(TextureStage::M_replace);
  set_texture(normal_map_ts, normal_map);

  // Then, we apply a normalization map, to normalize, per-pixel, the
  // vector to the light.
  PT(Texture) normalization_map = TexturePool::get_normalization_cube_map(32);
  PT(TextureStage) normalization_map_ts = new TextureStage("__normalization_map");
  normalization_map_ts->set_combine_rgb
    (TextureStage::CM_dot3_rgb, 
     TextureStage::CS_texture, TextureStage::CO_src_color,
     TextureStage::CS_previous, TextureStage::CO_src_color);
  normalization_map_ts->set_texcoord_name("light_vector");
  normalization_map_ts->set_sort(-15);
  set_texture(normalization_map_ts, normalization_map);

  // Finally, we enable M_light_vector texture coordinate generation.
  set_tex_gen(normalization_map_ts, TexGenAttrib::M_light_vector, 
              texcoord_name, NodePath());

  if (preserve_color) {
    // One more stage to get back the original color.
    PT(TextureStage) orig_color_ts = new TextureStage("__orig_color");
    orig_color_ts->set_combine_rgb
      (TextureStage::CM_modulate,
       TextureStage::CS_primary_color, TextureStage::CO_src_color,
       TextureStage::CS_previous, TextureStage::CO_src_color);
    set_texture(orig_color_ts, normal_map);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_normal_map
//       Access: Published
//  Description: Undoes the effect of a previous call to
//               set_normal_map().
////////////////////////////////////////////////////////////////////
void NodePath::
clear_normal_map() {
  // Scan through the TextureStages, and if we find any whose name
  // matches one of the stages that would have been left by
  // set_normal_map(), remove it from the state.

  CPT(RenderAttrib) attrib =
    get_state()->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    for (int i = 0; i < ta->get_num_on_stages(); i++) {
      TextureStage *stage = ta->get_on_stage(i);
      if (stage->get_name() == "__normal_map") {
        clear_texture(stage);

      } else if (stage->get_name() == "__normalization_map") {
        clear_texture(stage);
        clear_tex_gen(stage);

      } else if (stage->get_name() == "__orig_color") {
        clear_texture(stage);
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_vertex_column
//       Access: Published
//  Description: Returns true if there are at least some vertices at
//               this node and below that contain a reference to the
//               indicated vertex data column name, false otherwise.
//
//               This is particularly useful for testing whether a
//               particular model has a given texture coordinate set
//               (but see has_texcoord()).
////////////////////////////////////////////////////////////////////
bool NodePath::
has_vertex_column(const InternalName *name) const {
  nassertr_always(!is_empty(), false);
  return r_has_vertex_column(node(), name);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_vertex_columns
//       Access: Published
//  Description: Returns a list of all vertex array columns stored on
//               some geometry found at this node level and below.
////////////////////////////////////////////////////////////////////
InternalNameCollection NodePath::
find_all_vertex_columns() const {
  nassertr_always(!is_empty(), InternalNameCollection());
  InternalNames vertex_columns;
  r_find_all_vertex_columns(node(), vertex_columns);

  InternalNameCollection tc;
  InternalNames::iterator ti;
  for (ti = vertex_columns.begin(); ti != vertex_columns.end(); ++ti) {
    tc.add_name(*ti);
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_vertex_columns
//       Access: Published
//  Description: Returns a list of all vertex array columns stored on
//               some geometry found at this node level and below that
//               match the indicated name (which may contain wildcard
//               characters).
////////////////////////////////////////////////////////////////////
InternalNameCollection NodePath::
find_all_vertex_columns(const string &name) const {
  nassertr_always(!is_empty(), InternalNameCollection());
  InternalNames vertex_columns;
  r_find_all_vertex_columns(node(), vertex_columns);

  GlobPattern glob(name);

  InternalNameCollection tc;
  InternalNames::iterator ti;
  for (ti = vertex_columns.begin(); ti != vertex_columns.end(); ++ti) {
    InternalName *name = (*ti);
    if (glob.matches(name->get_name())) {
      tc.add_name(name);
    }
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_texcoords
//       Access: Published
//  Description: Returns a list of all texture coordinate sets used by
//               any geometry at this node level and below.
////////////////////////////////////////////////////////////////////
InternalNameCollection NodePath::
find_all_texcoords() const {
  nassertr_always(!is_empty(), InternalNameCollection());
  InternalNames vertex_columns;
  r_find_all_vertex_columns(node(), vertex_columns);

  CPT(InternalName) texcoord_name = InternalName::get_texcoord();
  
  InternalNameCollection tc;
  InternalNames::iterator ti;
  for (ti = vertex_columns.begin(); ti != vertex_columns.end(); ++ti) {
    if ((*ti)->get_top() == texcoord_name) {
      tc.add_name(*ti);
    }
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_texcoords
//       Access: Published
//  Description: Returns a list of all texture coordinate sets used by
//               any geometry at this node level and below that match
//               the indicated name (which may contain wildcard
//               characters).
////////////////////////////////////////////////////////////////////
InternalNameCollection NodePath::
find_all_texcoords(const string &name) const {
  nassertr_always(!is_empty(), InternalNameCollection());
  InternalNames vertex_columns;
  r_find_all_vertex_columns(node(), vertex_columns);

  GlobPattern glob(name);
  CPT(InternalName) texcoord_name = InternalName::get_texcoord();

  InternalNameCollection tc;
  InternalNames::iterator ti;
  for (ti = vertex_columns.begin(); ti != vertex_columns.end(); ++ti) {
    InternalName *name = (*ti);
    if (name->get_top() == texcoord_name) {
      // This is a texture coordinate name.  Figure out the basename
      // of the texture coordinates.
      int index = name->find_ancestor("texcoord");
      nassertr(index != -1, InternalNameCollection());
      string net_basename = name->get_net_basename(index - 1);

      if (glob.matches(net_basename)) {
        tc.add_name(name);
      }
    }
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_texture
//       Access: Published
//  Description: Returns the first texture found applied to geometry
//               at this node or below that matches the indicated name
//               (which may contain wildcards).  Returns the texture
//               if it is found, or NULL if it is not.
////////////////////////////////////////////////////////////////////
Texture *NodePath::
find_texture(const string &name) const {
  nassertr_always(!is_empty(), NULL);
  GlobPattern glob(name);
  return r_find_texture(node(), get_net_state(), glob);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_texture
//       Access: Published
//  Description: Returns the first texture found applied to geometry
//               at this node or below that is assigned to the
//               indicated texture stage.  Returns the texture if it
//               is found, or NULL if it is not.
////////////////////////////////////////////////////////////////////
Texture *NodePath::
find_texture(TextureStage *stage) const {
  nassertr_always(!is_empty(), NULL);
  return r_find_texture(node(), stage);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_textures
//       Access: Published
//  Description: Returns a list of a textures applied to geometry at
//               this node and below.
////////////////////////////////////////////////////////////////////
TextureCollection NodePath::
find_all_textures() const {
  nassertr_always(!is_empty(), TextureCollection());
  Textures textures;
  r_find_all_textures(node(), get_net_state(), textures);

  TextureCollection tc;
  Textures::iterator ti;
  for (ti = textures.begin(); ti != textures.end(); ++ti) {
    tc.add_texture(*ti);
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_textures
//       Access: Published
//  Description: Returns a list of a textures applied to geometry at
//               this node and below that match the indicated name
//               (which may contain wildcard characters).
////////////////////////////////////////////////////////////////////
TextureCollection NodePath::
find_all_textures(const string &name) const {
  nassertr_always(!is_empty(), TextureCollection());
  Textures textures;
  r_find_all_textures(node(), get_net_state(), textures);

  GlobPattern glob(name);

  TextureCollection tc;
  Textures::iterator ti;
  for (ti = textures.begin(); ti != textures.end(); ++ti) {
    Texture *texture = (*ti);
    if (glob.matches(texture->get_name())) {
      tc.add_texture(texture);
    }
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_textures
//       Access: Published
//  Description: Returns a list of a textures on geometry at
//               this node and below that are assigned to the
//               indicated texture stage.
////////////////////////////////////////////////////////////////////
TextureCollection NodePath::
find_all_textures(TextureStage *stage) const {
  nassertr_always(!is_empty(), TextureCollection());
  Textures textures;
  r_find_all_textures(node(), stage, textures);

  TextureCollection tc;
  Textures::iterator ti;
  for (ti = textures.begin(); ti != textures.end(); ++ti) {
    Texture *texture = (*ti);
    tc.add_texture(texture);
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_texture_stage
//       Access: Published
//  Description: Returns the first TextureStage found applied to
//               geometry at this node or below that matches the
//               indicated name (which may contain wildcards).
//               Returns the TextureStage if it is found, or NULL if
//               it is not.
////////////////////////////////////////////////////////////////////
TextureStage *NodePath::
find_texture_stage(const string &name) const {
  nassertr_always(!is_empty(), NULL);
  GlobPattern glob(name);
  return r_find_texture_stage(node(), get_net_state(), glob);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_texture_stages
//       Access: Published
//  Description: Returns a list of a TextureStages applied to geometry
//               at this node and below.
////////////////////////////////////////////////////////////////////
TextureStageCollection NodePath::
find_all_texture_stages() const {
  nassertr_always(!is_empty(), TextureStageCollection());
  TextureStages texture_stages;
  r_find_all_texture_stages(node(), get_net_state(), texture_stages);

  TextureStageCollection tc;
  TextureStages::iterator ti;
  for (ti = texture_stages.begin(); ti != texture_stages.end(); ++ti) {
    tc.add_texture_stage(*ti);
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::unify_texture_stages
//       Access: Published
//  Description: Searches through all TextureStages at this node and
//               below.  Any TextureStages that share the same name as
//               the indicated TextureStage object are replaced with
//               this object, thus ensuring that all geometry at this
//               node and below with a particular TextureStage name is
//               using the same TextureStage object.
////////////////////////////////////////////////////////////////////
void NodePath::
unify_texture_stages(TextureStage *stage) {
  nassertv_always(!is_empty());
  r_unify_texture_stages(node(), stage);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_texture_stages
//       Access: Published
//  Description: Returns a list of a TextureStages applied to geometry
//               at this node and below that match the indicated name
//               (which may contain wildcard characters).
////////////////////////////////////////////////////////////////////
TextureStageCollection NodePath::
find_all_texture_stages(const string &name) const {
  nassertr_always(!is_empty(), TextureStageCollection());
  TextureStages texture_stages;
  r_find_all_texture_stages(node(), get_net_state(), texture_stages);

  GlobPattern glob(name);

  TextureStageCollection tc;
  TextureStages::iterator ti;
  for (ti = texture_stages.begin(); ti != texture_stages.end(); ++ti) {
    TextureStage *texture_stage = (*ti);
    if (glob.matches(texture_stage->get_name())) {
      tc.add_texture_stage(texture_stage);
    }
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_material
//       Access: Published
//  Description: Returns the first material found applied to geometry
//               at this node or below that matches the indicated name
//               (which may contain wildcards).  Returns the material
//               if it is found, or NULL if it is not.
////////////////////////////////////////////////////////////////////
Material *NodePath::
find_material(const string &name) const {
  nassertr_always(!is_empty(), NULL);
  GlobPattern glob(name);
  return r_find_material(node(), get_net_state(), glob);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_materials
//       Access: Published
//  Description: Returns a list of a materials applied to geometry at
//               this node and below.
////////////////////////////////////////////////////////////////////
MaterialCollection NodePath::
find_all_materials() const {
  nassertr_always(!is_empty(), MaterialCollection());
  Materials materials;
  r_find_all_materials(node(), get_net_state(), materials);

  MaterialCollection tc;
  Materials::iterator ti;
  for (ti = materials.begin(); ti != materials.end(); ++ti) {
    tc.add_material(*ti);
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_all_materials
//       Access: Published
//  Description: Returns a list of a materials applied to geometry at
//               this node and below that match the indicated name
//               (which may contain wildcard characters).
////////////////////////////////////////////////////////////////////
MaterialCollection NodePath::
find_all_materials(const string &name) const {
  nassertr_always(!is_empty(), MaterialCollection());
  Materials materials;
  r_find_all_materials(node(), get_net_state(), materials);

  GlobPattern glob(name);

  MaterialCollection tc;
  Materials::iterator ti;
  for (ti = materials.begin(); ti != materials.end(); ++ti) {
    Material *material = (*ti);
    if (glob.matches(material->get_name())) {
      tc.add_material(material);
    }
  }
  return tc;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_material
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using the indicated material.
//
//               Previously, this operation made a copy of the
//               material structure, but nowadays it assigns the
//               pointer directly.
////////////////////////////////////////////////////////////////////
void NodePath::
set_material(Material *mat, int priority) {
  nassertv_always(!is_empty());
  nassertv(mat != NULL);
  node()->set_attrib(MaterialAttrib::make(mat), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_material_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using no material.  This is normally the default, but
//               it may be useful to use this to contradict
//               set_material() at a higher node level (or, with a
//               priority, to override a set_material() at a lower
//               level).
////////////////////////////////////////////////////////////////////
void NodePath::
set_material_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(MaterialAttrib::make_off(), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_material
//       Access: Published
//  Description: Completely removes any material adjustment that may
//               have been set via set_material() from this particular
//               node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_material() {
  nassertv_always(!is_empty());
  node()->clear_attrib(MaterialAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_material
//       Access: Published
//  Description: Returns true if a material has been applied to this
//               particular node via set_material(), false otherwise.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_material() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(MaterialAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const MaterialAttrib *ma = DCAST(MaterialAttrib, attrib);
    return !ma->is_off();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_material
//       Access: Published
//  Description: Returns the material that has been set on this
//               particular node, or NULL if no material has been set.
//               This is not necessarily the material that will be
//               applied to the geometry at or below this level, as
//               another material at a higher or lower level may
//               override.

//               See also find_material().
////////////////////////////////////////////////////////////////////
PT(Material) NodePath::
get_material() const {
  nassertr_always(!is_empty(), NULL);
  const RenderAttrib *attrib =
    node()->get_attrib(MaterialAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const MaterialAttrib *ma = DCAST(MaterialAttrib, attrib);
    return ma->get_material();
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_fog
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using the indicated fog.
////////////////////////////////////////////////////////////////////
void NodePath::
set_fog(Fog *fog, int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(FogAttrib::make(fog), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_fog_off
//       Access: Published
//  Description: Sets the geometry at this level and below to render
//               using no fog.  This is normally the default, but
//               it may be useful to use this to contradict
//               set_fog() at a higher node level (or, with a
//               priority, to override a set_fog() at a lower
//               level).
////////////////////////////////////////////////////////////////////
void NodePath::
set_fog_off(int priority) {
  nassertv_always(!is_empty());
  node()->set_attrib(FogAttrib::make_off(), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_fog
//       Access: Published
//  Description: Completely removes any fog adjustment that may
//               have been set via set_fog() or set_fog_off()
//               from this particular node.  This allows whatever
//               fogs might be otherwise affecting the geometry to
//               show instead.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_fog() {
  nassertv_always(!is_empty());
  node()->clear_attrib(FogAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_fog
//       Access: Published
//  Description: Returns true if a fog has been applied to this
//               particular node via set_fog(), false otherwise.
//               This is not the same thing as asking whether the
//               geometry at this node will be rendered with
//               fog, as there may be a fog in effect from a higher or
//               lower level.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_fog() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(FogAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const FogAttrib *fa = DCAST(FogAttrib, attrib);
    return !fa->is_off();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_fog_off
//       Access: Published
//  Description: Returns true if a fog has been specifically
//               disabled on this particular node via
//               set_fog_off(), false otherwise.  This is not the
//               same thing as asking whether the geometry at this
//               node will be rendered unfogged, as there may be a
//               fog in effect from a higher or lower level.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_fog_off() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(FogAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const FogAttrib *fa = DCAST(FogAttrib, attrib);
    return fa->is_off();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_fog
//       Access: Published
//  Description: Returns the fog that has been set on this
//               particular node, or NULL if no fog has been set.
//               This is not necessarily the fog that will be
//               applied to the geometry at or below this level, as
//               another fog at a higher or lower level may
//               override.
////////////////////////////////////////////////////////////////////
Fog *NodePath::
get_fog() const {
  nassertr_always(!is_empty(), NULL);
  const RenderAttrib *attrib =
    node()->get_attrib(FogAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const FogAttrib *fa = DCAST(FogAttrib, attrib);
    return fa->get_fog();
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_render_mode_wireframe
//       Access: Published
//  Description: Sets up the geometry at this level and below (unless
//               overridden) to render in wireframe mode.
////////////////////////////////////////////////////////////////////
void NodePath::
set_render_mode_wireframe(int priority) {
  nassertv_always(!is_empty());
  float thickness = get_render_mode_thickness();
  bool perspective = get_render_mode_perspective();
  node()->set_attrib(RenderModeAttrib::make(RenderModeAttrib::M_wireframe, thickness, perspective), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_render_mode_filled
//       Access: Published
//  Description: Sets up the geometry at this level and below (unless
//               overridden) to render in filled (i.e. not wireframe)
//               mode.
////////////////////////////////////////////////////////////////////
void NodePath::
set_render_mode_filled(int priority) {
  nassertv_always(!is_empty());
  float thickness = get_render_mode_thickness();
  bool perspective = get_render_mode_perspective();
  node()->set_attrib(RenderModeAttrib::make(RenderModeAttrib::M_filled, thickness, perspective), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_render_mode_perspective
//       Access: Published
//  Description: Sets up the point geometry at this level and below to
//               render as perspective sprites (that is, billboarded
//               quads).  The thickness, as specified with
//               set_render_mode_thickness(), is the width of each
//               point in 3-D units, unless it is overridden on a
//               per-vertex basis.  This does not affect geometry
//               other than points.
//
//               If you want the quads to be individually textured,
//               you should also set a TexGenAttrib::M_point_sprite on
//               the node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_render_mode_perspective(bool perspective, int priority) {
  nassertv_always(!is_empty());
  RenderModeAttrib::Mode mode = get_render_mode();
  float thickness = get_render_mode_thickness();
  node()->set_attrib(RenderModeAttrib::make(mode, thickness, perspective), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_render_mode_thickness
//       Access: Published
//  Description: Sets up the point geometry at this level and below to
//               render as thick points (that is, billboarded
//               quads).  The thickness is in pixels, unless
//               set_render_mode_perspective is also true, in which
//               case it is in 3-D units.
//
//               If you want the quads to be individually textured,
//               you should also set a TexGenAttrib::M_point_sprite on
//               the node.
////////////////////////////////////////////////////////////////////
void NodePath::
set_render_mode_thickness(float thickness, int priority) {
  nassertv_always(!is_empty());
  RenderModeAttrib::Mode mode = get_render_mode();
  bool perspective = get_render_mode_perspective();
  node()->set_attrib(RenderModeAttrib::make(mode, thickness, perspective), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_render_mode
//       Access: Published
//  Description: Sets up the geometry at this level and below (unless
//               overridden) to render in the specified mode and with
//               the indicated line and/or point thickness.
////////////////////////////////////////////////////////////////////
void NodePath::
set_render_mode(RenderModeAttrib::Mode mode, float thickness, int priority) {
  nassertv_always(!is_empty());

  node()->set_attrib(RenderModeAttrib::make(mode, thickness), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_render_mode
//       Access: Published
//  Description: Completely removes any render mode adjustment that
//               may have been set on this node via
//               set_render_mode_wireframe() or
//               set_render_mode_filled().
////////////////////////////////////////////////////////////////////
void NodePath::
clear_render_mode() {
  nassertv_always(!is_empty());
  node()->clear_attrib(RenderModeAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_render_mode
//       Access: Published
//  Description: Returns true if a render mode has been explicitly set
//               on this particular node via set_render_mode() (or
//               set_render_mode_wireframe() or
//               set_render_mode_filled()), false otherwise.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_render_mode() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(RenderModeAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_render_mode
//       Access: Published
//  Description: Returns the render mode that has been specifically
//               set on this node via set_render_mode(), or
//               M_unchanged if nothing has been set.
////////////////////////////////////////////////////////////////////
RenderModeAttrib::Mode NodePath::
get_render_mode() const {
  nassertr_always(!is_empty(), RenderModeAttrib::M_unchanged);
  const RenderAttrib *attrib =
    node()->get_attrib(RenderModeAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const RenderModeAttrib *ta = DCAST(RenderModeAttrib, attrib);
    return ta->get_mode();
  }

  return RenderModeAttrib::M_unchanged;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_render_mode_thickness
//       Access: Published
//  Description: Returns the render mode thickness that has been
//               specifically set on this node via set_render_mode(),
//               or 1.0 if nothing has been set.
////////////////////////////////////////////////////////////////////
float NodePath::
get_render_mode_thickness() const {
  nassertr_always(!is_empty(), 0.0f);
  const RenderAttrib *attrib =
    node()->get_attrib(RenderModeAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const RenderModeAttrib *ta = DCAST(RenderModeAttrib, attrib);
    return ta->get_thickness();
  }

  return 1.0f;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_render_mode_perspective
//       Access: Published
//  Description: Returns the flag that has been set on this node via
//               set_render_mode_perspective(), or false if no flag
//               has been set.
////////////////////////////////////////////////////////////////////
bool NodePath::
get_render_mode_perspective() const {
  nassertr_always(!is_empty(), 0.0f);
  const RenderAttrib *attrib =
    node()->get_attrib(RenderModeAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const RenderModeAttrib *ta = DCAST(RenderModeAttrib, attrib);
    return ta->get_perspective();
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_two_sided
//       Access: Published
//  Description: Specifically sets or disables two-sided rendering
//               mode on this particular node.  If no other nodes
//               override, this will cause backfacing polygons to be
//               drawn (in two-sided mode, true) or culled (in
//               one-sided mode, false).
////////////////////////////////////////////////////////////////////
void NodePath::
set_two_sided(bool two_sided, int priority) {
  nassertv_always(!is_empty());

  CullFaceAttrib::Mode mode =
    two_sided ?
    CullFaceAttrib::M_cull_none :
    CullFaceAttrib::M_cull_clockwise;

  node()->set_attrib(CullFaceAttrib::make(mode), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_two_sided
//       Access: Published
//  Description: Completely removes any two-sided adjustment that
//               may have been set on this node via set_two_sided().
//               The geometry at this level and below will
//               subsequently be rendered either two-sided or
//               one-sided, according to whatever other nodes may have
//               had set_two_sided() on it, or according to the
//               initial state otherwise.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_two_sided() {
  nassertv_always(!is_empty());
  node()->clear_attrib(CullFaceAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_two_sided
//       Access: Published
//  Description: Returns true if a two-sided adjustment has been
//               explicitly set on this particular node via
//               set_two_sided().  If this returns true, then
//               get_two_sided() may be called to determine which has
//               been set.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_two_sided() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(CullFaceAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_two_sided
//       Access: Published
//  Description: Returns true if two-sided rendering has been
//               specifically set on this node via set_two_sided(), or
//               false if one-sided rendering has been specifically
//               set, or if nothing has been specifically set.  See
//               also has_two_sided().  This does not necessarily
//               imply that the geometry will or will not be rendered
//               two-sided, as there may be other nodes that override.
////////////////////////////////////////////////////////////////////
bool NodePath::
get_two_sided() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(CullFaceAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const CullFaceAttrib *cfa = DCAST(CullFaceAttrib, attrib);
    return (cfa->get_actual_mode() == CullFaceAttrib::M_cull_none);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_depth_test
//       Access: Published
//  Description: Specifically sets or disables the testing of the
//               depth buffer on this particular node.  This is
//               normally on in the 3-d scene graph and off in the 2-d
//               scene graph; it should be on for rendering most 3-d
//               objects properly.
////////////////////////////////////////////////////////////////////
void NodePath::
set_depth_test(bool depth_test, int priority) {
  nassertv_always(!is_empty());

  DepthTestAttrib::PandaCompareFunc mode =
    depth_test ?
    DepthTestAttrib::M_less :
    DepthTestAttrib::M_none;

  node()->set_attrib(DepthTestAttrib::make(mode), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_depth_test
//       Access: Published
//  Description: Completely removes any depth-test adjustment that
//               may have been set on this node via set_depth_test().
////////////////////////////////////////////////////////////////////
void NodePath::
clear_depth_test() {
  nassertv_always(!is_empty());
  node()->clear_attrib(DepthTestAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_depth_test
//       Access: Published
//  Description: Returns true if a depth-test adjustment has been
//               explicitly set on this particular node via
//               set_depth_test().  If this returns true, then
//               get_depth_test() may be called to determine which has
//               been set.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_depth_test() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(DepthTestAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_depth_test
//       Access: Published
//  Description: Returns true if depth-test rendering has been
//               specifically set on this node via set_depth_test(), or
//               false if depth-test rendering has been specifically
//               disabled, or if nothing has been specifically set.  See
//               also has_depth_test().
////////////////////////////////////////////////////////////////////
bool NodePath::
get_depth_test() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(DepthTestAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const DepthTestAttrib *dta = DCAST(DepthTestAttrib, attrib);
    return (dta->get_mode() != DepthTestAttrib::M_none);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_depth_write
//       Access: Published
//  Description: Specifically sets or disables the writing to the
//               depth buffer on this particular node.  This is
//               normally on in the 3-d scene graph and off in the 2-d
//               scene graph; it should be on for rendering most 3-d
//               objects properly.
////////////////////////////////////////////////////////////////////
void NodePath::
set_depth_write(bool depth_write, int priority) {
  nassertv_always(!is_empty());

  DepthWriteAttrib::Mode mode =
    depth_write ?
    DepthWriteAttrib::M_on :
    DepthWriteAttrib::M_off;

  node()->set_attrib(DepthWriteAttrib::make(mode), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_depth_write
//       Access: Published
//  Description: Completely removes any depth-write adjustment that
//               may have been set on this node via set_depth_write().
////////////////////////////////////////////////////////////////////
void NodePath::
clear_depth_write() {
  nassertv_always(!is_empty());
  node()->clear_attrib(DepthWriteAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_depth_write
//       Access: Published
//  Description: Returns true if a depth-write adjustment has been
//               explicitly set on this particular node via
//               set_depth_write().  If this returns true, then
//               get_depth_write() may be called to determine which has
//               been set.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_depth_write() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(DepthWriteAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_depth_write
//       Access: Published
//  Description: Returns true if depth-write rendering has been
//               specifically set on this node via set_depth_write(), or
//               false if depth-write rendering has been specifically
//               disabled, or if nothing has been specifically set.  See
//               also has_depth_write().
////////////////////////////////////////////////////////////////////
bool NodePath::
get_depth_write() const {
  nassertr_always(!is_empty(), false);
  const RenderAttrib *attrib =
    node()->get_attrib(DepthWriteAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const DepthWriteAttrib *dta = DCAST(DepthWriteAttrib, attrib);
    return (dta->get_mode() != DepthWriteAttrib::M_off);
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::do_billboard_axis
//       Access: Published
//  Description: Performs a billboard-type rotate to the indicated
//               camera node, one time only, and leaves the object
//               rotated.  This is similar in principle to heads_up().
////////////////////////////////////////////////////////////////////
void NodePath::
do_billboard_axis(const NodePath &camera, float offset) {
  nassertv_always(!is_empty());

  CPT(TransformState) transform = camera.get_transform(get_parent());
  const LMatrix4f &rel_mat = transform->get_mat();

  LVector3f up = LVector3f::up();
  LVector3f rel_pos = -rel_mat.get_row3(3);

  LQuaternionf quat;
  ::heads_up(quat, rel_pos, up);
  set_quat(quat);

  // Also slide the geometry towards the camera according to the
  // offset factor.
  if (offset != 0.0f) {
    LVector3f translate = rel_mat.get_row3(3);
    translate.normalize();
    translate *= offset;
    set_pos(translate);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::do_billboard_point_eye
//       Access: Published
//  Description: Performs a billboard-type rotate to the indicated
//               camera node, one time only, and leaves the object
//               rotated.  This is similar in principle to look_at(),
//               although the point_eye billboard effect cannot be
//               achieved using the ordinary look_at() call.
////////////////////////////////////////////////////////////////////
void NodePath::
do_billboard_point_eye(const NodePath &camera, float offset) {
  nassertv_always(!is_empty());

  CPT(TransformState) transform = camera.get_transform(get_parent());
  const LMatrix4f &rel_mat = transform->get_mat();

  LVector3f up = LVector3f::up() * rel_mat;
  LVector3f rel_pos = LVector3f::forward() * rel_mat;

  LQuaternionf quat;
  ::look_at(quat, rel_pos, up);
  set_quat(quat);

  // Also slide the geometry towards the camera according to the
  // offset factor.
  if (offset != 0.0f) {
    LVector3f translate = rel_mat.get_row3(3);
    translate.normalize();
    translate *= offset;
    set_pos(translate);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::do_billboard_point_world
//       Access: Published
//  Description: Performs a billboard-type rotate to the indicated
//               camera node, one time only, and leaves the object
//               rotated.  This is similar in principle to look_at().
////////////////////////////////////////////////////////////////////
void NodePath::
do_billboard_point_world(const NodePath &camera, float offset) {
  nassertv_always(!is_empty());

  CPT(TransformState) transform = camera.get_transform(get_parent());
  const LMatrix4f &rel_mat = transform->get_mat();

  LVector3f up = LVector3f::up();
  LVector3f rel_pos = -rel_mat.get_row3(3);

  LQuaternionf quat;
  ::look_at(quat, rel_pos, up);
  set_quat(quat);

  // Also slide the geometry towards the camera according to the
  // offset factor.
  if (offset != 0.0f) {
    LVector3f translate = rel_mat.get_row3(3);
    translate.normalize();
    translate *= offset;
    set_pos(translate);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_billboard_axis
//       Access: Published
//  Description: Puts a billboard transition on the node such that it
//               will rotate in two dimensions around the up axis,
//               towards a specified "camera" instead of to the
//               viewing camera.
////////////////////////////////////////////////////////////////////
void NodePath::
set_billboard_axis(const NodePath &camera, float offset) {
  nassertv_always(!is_empty());
  CPT(RenderEffect) billboard = BillboardEffect::make
    (LVector3f::up(), false, true, 
     offset, camera, LPoint3f(0.0f, 0.0f, 0.0f));
  node()->set_effect(billboard);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_billboard_point_eye
//       Access: Published
//  Description: Puts a billboard transition on the node such that it
//               will rotate in three dimensions about the origin,
//               keeping its up vector oriented to the top of the
//               camera, towards a specified "camera" instead of to
//               the viewing camera.
////////////////////////////////////////////////////////////////////
void NodePath::
set_billboard_point_eye(const NodePath &camera, float offset) {
  nassertv_always(!is_empty());
  CPT(RenderEffect) billboard = BillboardEffect::make
    (LVector3f::up(), true, false,
     offset, camera, LPoint3f(0.0f, 0.0f, 0.0f));
  node()->set_effect(billboard);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_billboard_point_world
//       Access: Published
//  Description: Puts a billboard transition on the node such that it
//               will rotate in three dimensions about the origin,
//               keeping its up vector oriented to the sky, towards a
//               specified "camera" instead of to the viewing camera.
////////////////////////////////////////////////////////////////////
void NodePath::
set_billboard_point_world(const NodePath &camera, float offset) {
  nassertv_always(!is_empty());
  CPT(RenderEffect) billboard = BillboardEffect::make
    (LVector3f::up(), false, false,
     offset, camera, LPoint3f(0.0f, 0.0f, 0.0f));
  node()->set_effect(billboard);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_billboard
//       Access: Published
//  Description: Removes any billboard effect from the node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_billboard() {
  nassertv_always(!is_empty());
  node()->clear_effect(BillboardEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_billboard
//       Access: Published
//  Description: Returns true if there is any billboard effect on
//               the node.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_billboard() const {
  nassertr_always(!is_empty(), false);
  return node()->has_effect(BillboardEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_compass
//       Access: Published
//  Description: Puts a compass effect on the node, so that it will
//               retain a fixed rotation relative to the reference
//               node (or render if the reference node is empty)
//               regardless of the transforms above it.
////////////////////////////////////////////////////////////////////
void NodePath::
set_compass(const NodePath &reference) {
  nassertv_always(!is_empty());
  node()->set_effect(CompassEffect::make(reference));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_compass
//       Access: Published
//  Description: Removes any compass effect from the node.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_compass() {
  nassertv_always(!is_empty());
  node()->clear_effect(CompassEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_compass
//       Access: Published
//  Description: Returns true if there is any compass effect on
//               the node.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_compass() const {
  nassertr_always(!is_empty(), false);
  return node()->has_effect(CompassEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_transparency
//       Access: Published
//  Description: Specifically sets or disables transparent rendering
//               mode on this particular node.  If no other nodes
//               override, this will cause items with a non-1 value
//               for alpha color to be rendered partially transparent.
////////////////////////////////////////////////////////////////////
void NodePath::
set_transparency(TransparencyAttrib::Mode mode, int priority) {
  nassertv_always(!is_empty());

  node()->set_attrib(TransparencyAttrib::make(mode), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_transparency
//       Access: Published
//  Description: Completely removes any transparency adjustment that
//               may have been set on this node via set_transparency().
//               The geometry at this level and below will
//               subsequently be rendered either transparent or not,
//               to whatever other nodes may have had
//               set_transparency() on them.
////////////////////////////////////////////////////////////////////
void NodePath::
clear_transparency() {
  nassertv_always(!is_empty());
  node()->clear_attrib(TransparencyAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_transparency
//       Access: Published
//  Description: Returns true if a transparent-rendering adjustment
//               has been explicitly set on this particular node via
//               set_transparency().  If this returns true, then
//               get_transparency() may be called to determine whether
//               transparency has been explicitly enabled or
//               explicitly disabled for this node.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_transparency() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(TransparencyAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_transparency
//       Access: Published
//  Description: Returns the transparent rendering that has been
//               specifically set on this node via set_transparency(), or
//               M_none if nontransparent rendering has been specifically
//               set, or if nothing has been specifically set.  See
//               also has_transparency().  This does not necessarily
//               imply that the geometry will or will not be rendered
//               transparent, as there may be other nodes that override.
////////////////////////////////////////////////////////////////////
TransparencyAttrib::Mode NodePath::
get_transparency() const {
  nassertr_always(!is_empty(), TransparencyAttrib::M_none);
  const RenderAttrib *attrib =
    node()->get_attrib(TransparencyAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TransparencyAttrib *ta = DCAST(TransparencyAttrib, attrib);
    return ta->get_mode();
  }

  return TransparencyAttrib::M_none;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::set_antialias
//       Access: Published
//  Description: Specifies the antialiasing type that should be
//               applied at this node and below.  See AntialiasAttrib.
////////////////////////////////////////////////////////////////////
void NodePath::
set_antialias(unsigned short mode, int priority) {
  nassertv_always(!is_empty());

  node()->set_attrib(AntialiasAttrib::make(mode), priority);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::clear_antialias
//       Access: Published
//  Description: Completely removes any antialias setting that
//               may have been set on this node via set_antialias().
////////////////////////////////////////////////////////////////////
void NodePath::
clear_antialias() {
  nassertv_always(!is_empty());
  node()->clear_attrib(AntialiasAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::has_antialias
//       Access: Published
//  Description: Returns true if an antialias setting has been
//               explicitly mode on this particular node via
//               set_antialias().  If this returns true, then
//               get_antialias() may be called to determine what the
//               setting was.
////////////////////////////////////////////////////////////////////
bool NodePath::
has_antialias() const {
  nassertr_always(!is_empty(), false);
  return node()->has_attrib(AntialiasAttrib::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_antialias
//       Access: Published
//  Description: Returns the antialias setting that has been
//               specifically set on this node via set_antialias(), or
//               M_none if no setting has been made.
////////////////////////////////////////////////////////////////////
unsigned short NodePath::
get_antialias() const {
  nassertr_always(!is_empty(), AntialiasAttrib::M_none);
  const RenderAttrib *attrib =
    node()->get_attrib(AntialiasAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const AntialiasAttrib *ta = DCAST(AntialiasAttrib, attrib);
    return ta->get_mode();
  }

  return AntialiasAttrib::M_none;
}


////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_hidden_ancestor
//       Access: Published
//  Description: Returns the NodePath at or above the referenced node
//               that is hidden to the indicated camera(s), or an
//               empty NodePath if no ancestor of the referenced node
//               is hidden (and the node should be visible).
////////////////////////////////////////////////////////////////////
NodePath NodePath::
get_hidden_ancestor(DrawMask camera_mask, Thread *current_thread) const {
  int pipeline_stage = current_thread->get_pipeline_stage();

  NodePathComponent *comp;
  for (comp = _head; 
       comp != (NodePathComponent *)NULL; 
       comp = comp->get_next(pipeline_stage, current_thread)) {
    PandaNode *node = comp->get_node();
    if (node->is_overall_hidden() ||
        ((node->get_draw_show_mask() | ~node->get_draw_control_mask()) & camera_mask).is_zero()) {
      NodePath result;
      result._head = comp;
      return result;
    }
  }

  return not_found();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::stash
//       Access: Published
//  Description: Removes the referenced node (and the entire subgraph
//               below this node) from the scene graph in any normal
//               sense.  The node will no longer be visible and is not
//               tested for collisions; furthermore, no normal scene
//               graph traversal will visit the node.  The node's
//               bounding volume no longer contributes to its parent's
//               bounding volume.
//
//               A stashed node cannot be located by a normal find()
//               operation (although a special find string can still
//               retrieve it).
////////////////////////////////////////////////////////////////////
void NodePath::
stash(int sort, Thread *current_thread) {
  nassertv_always(!is_singleton() && !is_empty());
  nassertv(verify_complete());

  int pipeline_stage = current_thread->get_pipeline_stage();
  bool reparented = PandaNode::reparent(_head->get_next(pipeline_stage, current_thread),
					_head, sort, true, pipeline_stage,
                                        current_thread);
  nassertv(reparented);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::unstash
//       Access: Published
//  Description: Undoes the effect of a previous stash() on this
//               node: makes the referenced node (and the entire
//               subgraph below this node) once again part of the
//               scene graph.
////////////////////////////////////////////////////////////////////
void NodePath::
unstash(int sort, Thread *current_thread) {
  nassertv_always(!is_singleton() && !is_empty());
  nassertv(verify_complete());

  int pipeline_stage = current_thread->get_pipeline_stage();
  bool reparented = PandaNode::reparent(_head->get_next(pipeline_stage, current_thread),
					_head, sort, false, pipeline_stage,
                                        current_thread);
  nassertv(reparented);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::unstash_all
//       Access: Published
//  Description: Unstashes this node and all stashed child nodes.
////////////////////////////////////////////////////////////////////
void NodePath::
unstash_all(Thread *current_thread) {
  NodePathCollection stashed_descendents = find_all_matches("**/@@*");
  stashed_descendents.unstash();
  unstash(0, current_thread);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_stashed_ancestor
//       Access: Published
//  Description: Returns the NodePath at or above the referenced node
//               that is stashed, or an empty NodePath if no ancestor
//               of the referenced node is stashed (and the node should
//               be visible).
////////////////////////////////////////////////////////////////////
NodePath NodePath::
get_stashed_ancestor(Thread *current_thread) const {
  NodePathComponent *comp = _head;
  if (comp != (NodePathComponent *)NULL) {
    int pipeline_stage = current_thread->get_pipeline_stage();
    NodePathComponent *next = comp->get_next(pipeline_stage, current_thread);

    while (next != (NodePathComponent *)NULL) {
      PandaNode *node = comp->get_node();
      PandaNode *parent_node = next->get_node();

      if (parent_node->find_stashed(node) >= 0) {
        NodePath result;
        result._head = comp;
        return result;
      }

      comp = next;
      next = next->get_next(pipeline_stage, current_thread);
    }
  }

  return not_found();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::verify_complete
//       Access: Published
//  Description: Returns true if all of the nodes described in the
//               NodePath are connected, or false otherwise.
////////////////////////////////////////////////////////////////////
bool NodePath::
verify_complete(Thread *current_thread) const {
  if (is_empty()) {
    return true;
  }

  const NodePathComponent *comp = _head;
  nassertr(comp != (const NodePathComponent *)NULL, false);

  int pipeline_stage = current_thread->get_pipeline_stage();

  PandaNode *node = comp->get_node();
  nassertr(node != (const PandaNode *)NULL, false);
  int length = comp->get_length(pipeline_stage, current_thread);

  comp = comp->get_next(pipeline_stage, current_thread);
  length--;
  while (comp != (const NodePathComponent *)NULL) {
    PandaNode *next_node = comp->get_node();
    nassertr(next_node != (const PandaNode *)NULL, false);

    if (node->find_parent(next_node) < 0) {
      pgraph_cat.warning()
        << *this << " is incomplete; " << *node << " is not a child of "
        << *next_node << "\n";
      return false;
    }

    if (comp->get_length(pipeline_stage, current_thread) != length) {
      pgraph_cat.warning()
        << *this << " is incomplete; length at " << *next_node
        << " indicates " << comp->get_length(pipeline_stage, current_thread)
	<< " while length at " << *node << " indicates " << length << "\n";
      return false;
    }

    node = next_node;
    comp = comp->get_next(pipeline_stage, current_thread);
    length--;
  }

  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::prepare_scene
//       Access: Published
//  Description: Walks through the scene graph beginning at the bottom
//               node, and does whatever initialization is required to
//               render the scene properly with the indicated GSG.  It
//               is not strictly necessary to call this, since the GSG
//               will initialize itself when the scene is rendered,
//               but this may take some of the overhead away from that
//               process.
//
//               In particular, this will ensure that textures within
//               the scene are loaded in texture memory, and display
//               lists are built up from static geometry.
////////////////////////////////////////////////////////////////////
void NodePath::
prepare_scene(GraphicsStateGuardianBase *gsg) {
  nassertv_always(!is_empty());

  PreparedGraphicsObjects *prepared_objects = gsg->get_prepared_objects();

  CPT(RenderState) net_state = get_net_state();
  r_prepare_scene(node(), net_state, prepared_objects);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::show_bounds
//       Access: Published
//  Description: Causes the bounding volume of the bottom node and all
//               of its descendants (that is, the bounding volume
//               associated with the the bottom arc) to be rendered,
//               if possible.  The rendering method is less than
//               optimal; this is intended primarily for debugging.
////////////////////////////////////////////////////////////////////
void NodePath::
show_bounds() {
  nassertv_always(!is_empty());
  node()->set_effect(ShowBoundsEffect::make(false));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::show_tight_bounds
//       Access: Published
//  Description: Similar to show_bounds(), this draws a bounding box
//               representing the "tight" bounds of this node and all
//               of its descendants.  The bounding box is recomputed
//               every frame by reexamining all of the vertices; this
//               is far from efficient, but this is intended for
//               debugging.
////////////////////////////////////////////////////////////////////
void NodePath::
show_tight_bounds() {
  nassertv_always(!is_empty());
  node()->set_effect(ShowBoundsEffect::make(true));
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::hide_bounds
//       Access: Published
//  Description: Stops the rendering of the bounding volume begun with
//               show_bounds().
////////////////////////////////////////////////////////////////////
void NodePath::
hide_bounds() {
  nassertv_always(!is_empty());
  node()->clear_effect(ShowBoundsEffect::get_class_type());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::get_bounds
//       Access: Published
//  Description: Returns a newly-allocated bounding volume containing
//               the bottom node and all of its descendants.  This is
//               the bounding volume on the bottom arc, converted to
//               the local coordinate space of the node.
////////////////////////////////////////////////////////////////////
PT(BoundingVolume) NodePath::
get_bounds(Thread *current_thread) const {
  nassertr_always(!is_empty(), new BoundingSphere);
  return node()->get_bounds(current_thread)->make_copy();
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::force_recompute_bounds
//       Access: Published
//  Description: Forces the recomputing of all the bounding volumes at
//               every node in the subgraph beginning at this node and
//               below.
//
//               This should not normally need to be called, since the
//               bounding volumes are supposed to be recomputed
//               automatically when necessary.  It may be useful when
//               debugging, to verify that the bounding volumes have
//               not become inadvertently stale; it may also be useful
//               to force animated characters to update their bounding
//               volumes (which does not presently happen
//               automatically).
////////////////////////////////////////////////////////////////////
void NodePath::
force_recompute_bounds() {
  nassertv_always(!is_empty());
  r_force_recompute_bounds(node());
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::write_bounds
//       Access: Published
//  Description: Writes a description of the bounding volume
//               containing the bottom node and all of its descendants
//               to the indicated output stream.
////////////////////////////////////////////////////////////////////
void NodePath::
write_bounds(ostream &out) const {
  get_bounds()->write(out);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::calc_tight_bounds
//       Access: Published
//  Description: Calculates the minimum and maximum vertices of all
//               Geoms at this NodePath's bottom node and below.  This
//               is a tight bounding box; it will generally be tighter
//               than the bounding volume returned by get_bounds()
//               (but it is more expensive to compute).
//
//               The return value is true if any points are within the
//               bounding volume, or false if none are.
////////////////////////////////////////////////////////////////////
bool NodePath::
calc_tight_bounds(LPoint3f &min_point, LPoint3f &max_point,
                  Thread *current_thread) const {
  min_point.set(0.0f, 0.0f, 0.0f);
  max_point.set(0.0f, 0.0f, 0.0f);
  nassertr_always(!is_empty(), false);

  bool found_any = false;
  node()->calc_tight_bounds(min_point, max_point, found_any, 
                            TransformState::make_identity(),
                            current_thread);

  return found_any;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::analyze
//       Access: Published
//  Description: Analyzes the geometry below this node and reports the
//               number of vertices, triangles, etc.  This is the same
//               information reported by the bam-info program.
////////////////////////////////////////////////////////////////////
void NodePath::
analyze() const {
  nassertv_always(!is_empty());
  SceneGraphAnalyzer sga;
  sga.add_node(node());
  sga.write(nout);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::flatten_light
//       Access: Published
//  Description: Lightly flattens out the hierarchy below this node by
//               applying transforms, colors, and texture matrices
//               from the arcs onto the vertices, but does not remove
//               any nodes.
//
//               This can result in improved rendering performance
//               because there will be fewer transforms in the
//               resulting scene graph, but the number of nodes will
//               remain the same.
//
//               Particularly, any NodePaths that reference nodes
//               within this hierarchy will not be damaged.  However,
//               since this operation will remove transforms from the
//               scene graph, it may be dangerous to apply to arcs
//               where you expect to dynamically modify the transform,
//               or where you expect the geometry to remain in a
//               particular local coordinate system.
//
//               The return value is always 0, since flatten_light
//               does not remove any arcs.
////////////////////////////////////////////////////////////////////
int NodePath::
flatten_light() {
  nassertr_always(!is_empty(), 0);
  SceneGraphReducer gr;
  gr.apply_attribs(node());

  return 0;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::flatten_medium
//       Access: Published
//  Description: A more thorough flattening than flatten_light(), this
//               first applies all the transforms, colors, and texture
//               matrices from the arcs onto the vertices, and then
//               removes unneeded grouping nodes--nodes that have
//               exactly one child, for instance, but have no special
//               properties in themselves.
//
//               This results in improved perforamance over
//               flatten_light() because the number of nodes in the
//               scene graph is reduced.
//
//               If max_children is specified, it represents the
//               maximum number of children a node is allowed to have
//               and still be flattened.  Normally, this is 1; we
//               don't typically want to flatten a node that has
//               multiple children.  However, sometimes this may be
//               desirable; set this parameter to control the limit.
//               If this is set to -1, there is no limit.
//
//               The return value is the number of arcs removed.
////////////////////////////////////////////////////////////////////
int NodePath::
flatten_medium() {
  nassertr_always(!is_empty(), 0);
  SceneGraphReducer gr;
  gr.apply_attribs(node());
  int num_removed = gr.flatten(node(), 0);

  gr.collect_vertex_data(node());
  gr.unify(node());

  return num_removed;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::flatten_strong
//       Access: Published
//  Description: The strongest possible flattening.  This first
//               applies all of the transforms to the vertices, as in
//               flatten_medium(), but then it will combine sibling
//               nodes together when possible, in addition to removing
//               unnecessary parent-child nodes.  This can result in
//               substantially fewer nodes, but any nicely-grouped
//               hierachical bounding volumes may be lost.
//
//               It is generally a good idea to apply this kind of
//               flattening only to nodes that will be culled largely
//               as a single unit, like a car.  Applying this to an
//               entire scene may result in overall poorer performance
//               because of less-effective culling.
////////////////////////////////////////////////////////////////////
int NodePath::
flatten_strong() {
  nassertr_always(!is_empty(), 0);
  SceneGraphReducer gr;
  gr.apply_attribs(node());
  int num_removed = gr.flatten(node(), ~0);

  gr.collect_vertex_data(node(), ~(SceneGraphReducer::CVD_format | SceneGraphReducer::CVD_name));
  gr.unify(node());

  return num_removed;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_net_tag
//       Access: Published
//  Description: Returns the lowest ancestor of this node that
//               contains a tag definition with the indicated key, if
//               any, or an empty NodePath if no ancestor of this node
//               contains this tag definition.  See set_tag().
////////////////////////////////////////////////////////////////////
NodePath NodePath::
find_net_tag(const string &key) const {
  if (is_empty()) {
    return NodePath::not_found();
  }
  if (has_tag(key)) {
    return *this;
  }
  return get_parent().find_net_tag(key);
}

#ifdef HAVE_PYTHON 
////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_net_python_tag
//       Access: Published
//  Description: Returns the lowest ancestor of this node that
//               contains a tag definition with the indicated key, if
//               any, or an empty NodePath if no ancestor of this node
//               contains this tag definition.  See set_python_tag().
////////////////////////////////////////////////////////////////////
NodePath NodePath::
find_net_python_tag(const string &key) const {
  if (is_empty()) {
    return NodePath::not_found();
  }
  if (has_python_tag(key)) {
    return *this;
  }
  return get_parent().find_net_python_tag(key);
}
#endif  // HAVE_PYTHON

////////////////////////////////////////////////////////////////////
//     Function: NodePath::write_bam_file
//       Access: Published
//  Description: Writes the contents of this node and below out to a
//               bam file with the indicated filename.  This file may
//               then be read in again, as is, at some later point.
//               Returns true if successful, false on some kind of
//               error.
////////////////////////////////////////////////////////////////////
bool NodePath::
write_bam_file(const string &filename) const {
  nassertr_always(!is_empty(), false);

  BamFile bam_file;

  bool okflag = false;

  if (bam_file.open_write(filename)) {
    if (bam_file.write_object(node())) {
      okflag = true;
    }
    bam_file.close();
  }
  return okflag;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_common_ancestor
//       Access: Private, Static
//  Description: Walks up from both NodePaths to find the first node
//               that both have in common, if any.  Fills a_count and
//               b_count with the number of nodes below the common
//               node in each path.
//
//               The return value is the NodePathComponent of the node
//               they have in common, or NULL if they have nothing in
//               common.
////////////////////////////////////////////////////////////////////
NodePathComponent *NodePath::
find_common_ancestor(const NodePath &a, const NodePath &b,
                     int &a_count, int &b_count, Thread *current_thread) {
  nassertr(!a.is_empty() && !b.is_empty(), NULL);
  NodePathComponent *ac = a._head;
  NodePathComponent *bc = b._head;
  a_count = 0;
  b_count = 0;

  int pipeline_stage = current_thread->get_pipeline_stage();

  // Shorten up the longer one until they are the same length.
  while (ac->get_length(pipeline_stage, current_thread) > bc->get_length(pipeline_stage, current_thread)) {
    nassertr(ac != (NodePathComponent *)NULL, NULL);
    ac = ac->get_next(pipeline_stage, current_thread);
    a_count++;
  }
  while (bc->get_length(pipeline_stage, current_thread) > ac->get_length(pipeline_stage, current_thread)) {
    nassertr(bc != (NodePathComponent *)NULL, NULL);
    bc = bc->get_next(pipeline_stage, current_thread);
    b_count++;
  }

  // Now shorten them both up until we reach the same component.
  while (ac != bc) {
    // These shouldn't go to NULL unless they both go there together. 
    nassertr(ac != (NodePathComponent *)NULL, NULL);
    nassertr(bc != (NodePathComponent *)NULL, NULL);
    ac = ac->get_next(pipeline_stage, current_thread);
    a_count++;
    bc = bc->get_next(pipeline_stage, current_thread);
    b_count++;
  }

  return ac;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_get_net_state
//       Access: Private
//  Description: Recursively determines the net state changes to the
//               indicated component node from the root of the graph.
////////////////////////////////////////////////////////////////////
CPT(RenderState) NodePath::
r_get_net_state(NodePathComponent *comp, Thread *current_thread) const {
  if (comp == (NodePathComponent *)NULL) {
    return RenderState::make_empty();
  } else {
    CPT(RenderState) state = comp->get_node()->get_state(current_thread);
    int pipeline_stage = current_thread->get_pipeline_stage();
    return r_get_net_state(comp->get_next(pipeline_stage, current_thread), current_thread)->compose(state);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_get_partial_state
//       Access: Private
//  Description: Recursively determines the net state changes to the
//               indicated component node from the nth node above it.
//               If n exceeds the length of the path, this returns the
//               net transform from the root of the graph.
////////////////////////////////////////////////////////////////////
CPT(RenderState) NodePath::
r_get_partial_state(NodePathComponent *comp, int n, 
                    Thread *current_thread) const {
  if (n == 0 || comp == (NodePathComponent *)NULL) {
    return RenderState::make_empty();
  } else {
    CPT(RenderState) state = comp->get_node()->get_state(current_thread);
    int pipeline_stage = current_thread->get_pipeline_stage();
    return r_get_partial_state(comp->get_next(pipeline_stage, current_thread), n - 1, current_thread)->compose(state);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_get_net_transform
//       Access: Private
//  Description: Recursively determines the net transform to the
//               indicated component node from the root of the graph.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
r_get_net_transform(NodePathComponent *comp, Thread *current_thread) const {
  if (comp == (NodePathComponent *)NULL) {
    return TransformState::make_identity();
  } else {
    int pipeline_stage = current_thread->get_pipeline_stage();
    CPT(TransformState) net_transform = r_get_net_transform(comp->get_next(pipeline_stage, current_thread), current_thread);
    PandaNode *node = comp->get_node();
    CPT(TransformState) transform = node->get_transform(current_thread);

    CPT(RenderEffects) effects = node->get_effects(current_thread);
    if (effects->has_adjust_transform()) {
      effects->adjust_transform(net_transform, transform, node);
    }
      
    return net_transform->compose(transform);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_get_partial_transform
//       Access: Private
//  Description: Recursively determines the net transform to the
//               indicated component node from the nth node above it.
//               If n exceeds the length of the path, this returns the
//               net transform from the root of the graph.
//
//               If any node in the path had a net_transform effect
//               applied, returns NULL--in this case the partial
//               transform cannot be easily determined.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
r_get_partial_transform(NodePathComponent *comp, int n, 
                        Thread *current_thread) const {
  if (n == 0 || comp == (NodePathComponent *)NULL) {
    return TransformState::make_identity();
  } else {
    if (comp->get_node()->get_effects(current_thread)->has_adjust_transform()) {
      return NULL;
    }
    CPT(TransformState) transform = comp->get_node()->get_transform(current_thread);
    int pipeline_stage = current_thread->get_pipeline_stage();
    CPT(TransformState) partial = r_get_partial_transform(comp->get_next(pipeline_stage, current_thread), n - 1, current_thread);
    if (partial == (const TransformState *)NULL) {
      return NULL;
    }
    return partial->compose(transform);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_get_net_prev_transform
//       Access: Private
//  Description: Recursively determines the net "previous" transform
//               to the indicated component node from the root of the
//               graph.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
r_get_net_prev_transform(NodePathComponent *comp, Thread *current_thread) const {
  if (comp == (NodePathComponent *)NULL) {
    return TransformState::make_identity();
  } else {
    CPT(TransformState) transform = comp->get_node()->get_prev_transform(current_thread);
    int pipeline_stage = current_thread->get_pipeline_stage();
    return r_get_net_prev_transform(comp->get_next(pipeline_stage, current_thread), current_thread)->compose(transform);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_get_partial_prev_transform
//       Access: Private
//  Description: Recursively determines the net "previous" transform
//               to the indicated component node from the nth node
//               above it.  If n exceeds the length of the path, this
//               returns the net previous transform from the root of
//               the graph.
////////////////////////////////////////////////////////////////////
CPT(TransformState) NodePath::
r_get_partial_prev_transform(NodePathComponent *comp, int n, Thread *current_thread) const {
  if (n == 0 || comp == (NodePathComponent *)NULL) {
    return TransformState::make_identity();
  } else {
    CPT(TransformState) transform = comp->get_node()->get_prev_transform(current_thread);
    int pipeline_stage = current_thread->get_pipeline_stage();
    return r_get_partial_prev_transform(comp->get_next(pipeline_stage, current_thread), n - 1, current_thread)->compose(transform);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_matches
//       Access: Private
//  Description: Finds up to max_matches matches against the given
//               path string from this node and deeper.  The
//               max_matches count indicates the maximum number of
//               matches to return, or -1 not to limit the number
//               returned.
////////////////////////////////////////////////////////////////////
void NodePath::
find_matches(NodePathCollection &result, const string &path,
             int max_matches) const {
  if (is_empty()) {
    pgraph_cat.warning()
      << "Attempt to extend an empty NodePath by '" << path
      << "'.\n";
    return;
  }
  FindApproxPath approx_path;
  if (approx_path.add_string(path)) {
    find_matches(result, approx_path, max_matches);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_matches
//       Access: Private
//  Description: Finds up to max_matches matches against the given
//               approx_path from this node and deeper.  The
//               max_matches count indicates the maximum number of
//               matches to return, or -1 not to limit the number
//               returned.
////////////////////////////////////////////////////////////////////
void NodePath::
find_matches(NodePathCollection &result, FindApproxPath &approx_path,
             int max_matches) const {
  if (is_empty()) {
    pgraph_cat.warning()
      << "Attempt to extend an empty NodePath by: " << approx_path << ".\n";
    return;
  }

  // We start with just one entry on the level.
  FindApproxLevelEntry *level = 
    new FindApproxLevelEntry(WorkingNodePath(*this), approx_path);
  nassertv(level->_node_path.is_valid());

  find_matches(result, level, max_matches);
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::find_matches
//       Access: Private
//  Description: The fundamental implementation of find_matches(),
//               given a starting level (a linked list of
//               FindApproxLevelEntry objects).
////////////////////////////////////////////////////////////////////
void NodePath::
find_matches(NodePathCollection &result, FindApproxLevelEntry *level,
             int max_matches) const {
  
  int num_levels_remaining = _max_search_depth;

  FindApproxLevelEntry *deleted_entries = NULL;

  while (num_levels_remaining > 0 && level != NULL) {
    if (pgraph_cat.is_spam()) {
      pgraph_cat.spam()
        << "find_matches pass: " << result << ", "
        << max_matches << ", " << num_levels_remaining << "\n";
      level->write_level(pgraph_cat.spam(false), 4);
    }

    num_levels_remaining--;

    FindApproxLevelEntry *next_level = NULL;

    // For each node in the current level, build up the set of possible
    // matches in the next level.
    FindApproxLevelEntry *entry = level;
    while (entry != (FindApproxLevelEntry *)NULL) {
      if (entry->consider_node(result, next_level, max_matches, 0)) {
        // If we found the requisite number of matches, we can stop.
        // Delete all remaining entries and return immediately.

        while (entry != (FindApproxLevelEntry *)NULL) {
          FindApproxLevelEntry *next = entry->_next;
          delete entry;
          entry = next;
        }
        while (next_level != (FindApproxLevelEntry *)NULL) {
          FindApproxLevelEntry *next = next_level->_next;
          delete next_level;
          next_level = next;
        }
        while (deleted_entries != (FindApproxLevelEntry *)NULL) {
          FindApproxLevelEntry *next = deleted_entries->_next;
          delete deleted_entries;
          deleted_entries = next;
        }
        return;
      }

      // Move the entry to the delete chain so we can delete it before
      // we return from this method.  (We can't delete it immediately,
      // because there might be WorkingNodePaths in the next_level
      // that reference the WorkingNodePath object within the entry.)
      FindApproxLevelEntry *next = entry->_next;
      entry->_next = deleted_entries;
      deleted_entries = entry;

      entry = next;
    }
    
    // Make sure the remaining entries from this level are added to
    // the delete chain.
    while (entry != (FindApproxLevelEntry *)NULL) {
      FindApproxLevelEntry *next = entry->_next;
      entry->_next = deleted_entries;
      deleted_entries = entry;

      entry = next;
    }

    level = next_level;
  }

  // Now it's safe to delete all entries on the delete chain.
  while (deleted_entries != (FindApproxLevelEntry *)NULL) {
    FindApproxLevelEntry *next = deleted_entries->_next;
    delete deleted_entries;
    deleted_entries = next;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_adjust_all_priorities
//       Access: Private
//  Description: The recursive implementation of
//               adjust_all_priorities().  This walks through the
//               subgraph defined by the indicated node and below.
////////////////////////////////////////////////////////////////////
void NodePath::
r_adjust_all_priorities(PandaNode *node, int adjustment) {
  node->set_state(node->get_state()->adjust_all_priorities(adjustment));
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      gnode->set_geom_state(i, gnode->get_geom_state(i)->adjust_all_priorities(adjustment));
    }
  }

  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    r_adjust_all_priorities(cr.get_child(i), adjustment);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_force_recompute_bounds
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_force_recompute_bounds(PandaNode *node) {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      const Geom *geom = gnode->get_geom(i);
      geom->mark_bounds_stale();
    }
  }

  node->mark_bounds_stale();

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    r_force_recompute_bounds(cr.get_child(i));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_set_collide_mask
//       Access: Private
//  Description: Recursively applies the indicated collide mask to the
//               nodes at and below this node.
////////////////////////////////////////////////////////////////////
void NodePath::
r_set_collide_mask(PandaNode *node, 
                   CollideMask and_mask, CollideMask or_mask,
                   TypeHandle node_type) {
  if (node->is_of_type(node_type)) {
    CollideMask into_collide_mask = node->get_into_collide_mask();
    into_collide_mask = (into_collide_mask & and_mask) | or_mask;
    node->set_into_collide_mask(into_collide_mask);
  }

  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    r_set_collide_mask(cr.get_child(i), and_mask, or_mask, node_type);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_has_vertex_column
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
bool NodePath::
r_has_vertex_column(PandaNode *node, const InternalName *name) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_R(gnode, node, false);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      const Geom *geom = gnode->get_geom(i);
      CPT(GeomVertexData) vdata = geom->get_vertex_data();
      if (vdata->has_column(name)) {
        return true;
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    if (r_has_vertex_column(child, name)) {
      return true;
    }
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_all_vertex_columns
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_find_all_vertex_columns(PandaNode *node,
                          NodePath::InternalNames &vertex_columns) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; ++i) {
      const Geom *geom = gnode->get_geom(i);
      const GeomVertexFormat *format = geom->get_vertex_data()->get_format();
      int num_arrays = format->get_num_arrays();
      for (int j = 0; j < num_arrays; ++j) {
        const GeomVertexArrayFormat *array = format->get_array(j);
        int num_columns = array->get_num_columns();
        for (int k = 0; k < num_columns; ++k) {
          const GeomVertexColumn *column = array->get_column(k);
          vertex_columns.insert(column->get_name());
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    r_find_all_vertex_columns(child, vertex_columns);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_texture
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
Texture *NodePath::
r_find_texture(PandaNode *node, const RenderState *state,
               const GlobPattern &glob) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_R(gnode, node, NULL);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = 
        state->compose(gnode->get_geom_state(i));

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        for (int i = 0; i < ta->get_num_on_stages(); i++) {
          Texture *texture = ta->get_on_texture(ta->get_on_stage(i));
          if (texture != (Texture *)NULL) {
            if (glob.matches(texture->get_name())) {
              return texture;
            }
          }
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    CPT(RenderState) next_state = state->compose(child->get_state());

    Texture *result = r_find_texture(child, next_state, glob);
    if (result != (Texture *)NULL) {
      return result;
    }
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_all_textures
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_find_all_textures(PandaNode *node, const RenderState *state,
                    NodePath::Textures &textures) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = 
        state->compose(gnode->get_geom_state(i));

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        for (int i = 0; i < ta->get_num_on_stages(); i++) {
          Texture *texture = ta->get_on_texture(ta->get_on_stage(i));
          if (texture != (Texture *)NULL) {
            textures.insert(texture);
          }
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    CPT(RenderState) next_state = state->compose(child->get_state());
    r_find_all_textures(child, next_state, textures);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_texture
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
Texture * NodePath::
r_find_texture(PandaNode *node, TextureStage *stage) const {
  // Look for a TextureAttrib on the node.
  const RenderAttrib *attrib =
    node->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    if (ta->has_on_stage(stage)) {
      return ta->get_on_texture(stage);
    }
  }

  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_R(gnode, node, NULL);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = gnode->get_geom_state(i);

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        if (ta->has_on_stage(stage)) {
          return ta->get_on_texture(stage);
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);

    Texture *result = r_find_texture(child, stage);
    if (result != (Texture *)NULL) {
      return result;
    }
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_all_textures
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_find_all_textures(PandaNode *node, TextureStage *stage,
                    NodePath::Textures &textures) const {
  // Look for a TextureAttrib on the node.
  const RenderAttrib *attrib =
    node->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    if (ta->has_on_stage(stage)) {
      textures.insert(ta->get_on_texture(stage));
    }
  }

  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = gnode->get_geom_state(i);

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        if (ta->has_on_stage(stage)) {
          textures.insert(ta->get_on_texture(stage));
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    r_find_all_textures(child, stage, textures);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_texture_stage
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
TextureStage * NodePath::
r_find_texture_stage(PandaNode *node, const RenderState *state,
                     const GlobPattern &glob) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_R(gnode, node, NULL);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = 
        state->compose(gnode->get_geom_state(i));

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        for (int i = 0; i < ta->get_num_on_stages(); i++) {
          TextureStage *texture_stage = ta->get_on_stage(i);
          if (texture_stage != (TextureStage *)NULL) {
            if (glob.matches(texture_stage->get_name())) {
              return texture_stage;
            }
          }
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    CPT(RenderState) next_state = state->compose(child->get_state());

    TextureStage *result = r_find_texture_stage(child, next_state, glob);
    if (result != (TextureStage *)NULL) {
      return result;
    }
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_all_texture_stages
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_find_all_texture_stages(PandaNode *node, const RenderState *state,
                          NodePath::TextureStages &texture_stages) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = 
        state->compose(gnode->get_geom_state(i));

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        for (int i = 0; i < ta->get_num_on_stages(); i++) {
          TextureStage *texture_stage = ta->get_on_stage(i);
          if (texture_stage != (TextureStage *)NULL) {
            texture_stages.insert(texture_stage);
          }
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    CPT(RenderState) next_state = state->compose(child->get_state());
    r_find_all_texture_stages(child, next_state, texture_stages);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_unify_texture_stages
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_unify_texture_stages(PandaNode *node, TextureStage *stage) {
  // Look for a TextureAttrib on the state.
  const RenderAttrib *attrib =
    node->get_attrib(TextureAttrib::get_class_type());
  if (attrib != (const RenderAttrib *)NULL) {
    const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
    CPT(RenderAttrib) new_attrib = ta->unify_texture_stages(stage);
    if (new_attrib != ta) {
      node->set_attrib(new_attrib);
    }
  }

  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) state = gnode->get_geom_state(i);

      // Look for a TextureAttrib on the state.
      const RenderAttrib *attrib =
        state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta = DCAST(TextureAttrib, attrib);
        CPT(RenderAttrib) new_attrib = ta->unify_texture_stages(stage);
        if (new_attrib != ta) {
          CPT(RenderState) new_state = state->add_attrib(new_attrib);
          gnode->set_geom_state(i, new_state);
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    r_unify_texture_stages(child, stage);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_material
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
Material *NodePath::
r_find_material(PandaNode *node, const RenderState *state,
               const GlobPattern &glob) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_R(gnode, node, NULL);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = 
        state->compose(gnode->get_geom_state(i));

      // Look for a MaterialAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(MaterialAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const MaterialAttrib *ta = DCAST(MaterialAttrib, attrib);
        if (!ta->is_off()) {
          Material *material = ta->get_material();
          if (material != (Material *)NULL) {
            return material;
          }
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    CPT(RenderState) next_state = state->compose(child->get_state());

    Material *result = r_find_material(child, next_state, glob);
    if (result != (Material *)NULL) {
      return result;
    }
  }

  return NULL;
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_find_all_materials
//       Access: Private
//  Description: 
////////////////////////////////////////////////////////////////////
void NodePath::
r_find_all_materials(PandaNode *node, const RenderState *state,
                    NodePath::Materials &materials) const {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = 
        state->compose(gnode->get_geom_state(i));

      // Look for a MaterialAttrib on the state.
      const RenderAttrib *attrib =
        geom_state->get_attrib(MaterialAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const MaterialAttrib *ta = DCAST(MaterialAttrib, attrib);
        if (!ta->is_off()) {
          Material *material = ta->get_material();
          if (material != (Material *)NULL) {
            materials.insert(material);
          }
        }
      }
    }
  }

  // Now consider children.
  PandaNode::Children cr = node->get_children();
  int num_children = cr.get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = cr.get_child(i);
    CPT(RenderState) next_state = state->compose(child->get_state());
    r_find_all_materials(child, next_state, materials);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: NodePath::r_prepare_scene
//       Access: Private
//  Description: The recursive implementation of prepare_scene.
////////////////////////////////////////////////////////////////////
void NodePath::
r_prepare_scene(PandaNode *node, const RenderState *state,
                PreparedGraphicsObjects *prepared_objects) {
  if (node->is_geom_node()) {
    GeomNode *gnode;
    DCAST_INTO_V(gnode, node);

    /* 
       Not implemented yet in pgraph.  Maybe we don't need this anyway.
    if (retained_mode) {
      gnode->prepare(gsg);
    }
    */

    int num_geoms = gnode->get_num_geoms();
    for (int i = 0; i < num_geoms; i++) {
      CPT(RenderState) geom_state = state->compose(gnode->get_geom_state(i));
      const RenderAttrib *attrib = 
        geom_state->get_attrib(TextureAttrib::get_class_type());
      if (attrib != (const RenderAttrib *)NULL) {
        const TextureAttrib *ta;
        DCAST_INTO_V(ta, attrib);
        Texture *texture = ta->get_texture();
        if (texture != (Texture *)NULL) {
          texture->prepare(prepared_objects);
        }
      }
    }
  }

  int num_children = node->get_num_children();
  for (int i = 0; i < num_children; i++) {
    PandaNode *child = node->get_child(i);
    CPT(RenderState) child_state = state->compose(child->get_state());
    r_prepare_scene(child, child_state, prepared_objects);
  }
}
