Browse Source

pgraph: Major traversal optimizations

In particular, these optimize the traversal of nodes that are not in view, which are discarded more efficiently.
This change affects both the cull and collision traversers.
rdb 4 years ago
parent
commit
fac6916151
67 changed files with 1009 additions and 885 deletions
  1. 1 11
      contrib/src/speedtree/speedTreeNode.cxx
  2. 0 1
      contrib/src/speedtree/speedTreeNode.h
  3. 1 11
      panda/src/bullet/bulletDebugNode.cxx
  4. 0 1
      panda/src/bullet/bulletDebugNode.h
  5. 126 21
      panda/src/collide/collisionLevelState.I
  6. 3 1
      panda/src/collide/collisionLevelState.h
  7. 28 3
      panda/src/collide/collisionLevelStateBase.I
  8. 5 0
      panda/src/collide/collisionLevelStateBase.h
  9. 4 21
      panda/src/collide/collisionNode.cxx
  10. 0 1
      panda/src/collide/collisionNode.h
  11. 92 99
      panda/src/collide/collisionTraverser.cxx
  12. 3 16
      panda/src/collide/collisionVisualizer.cxx
  13. 0 1
      panda/src/collide/collisionVisualizer.h
  14. 1 1
      panda/src/grutil/pipeOcclusionCullTraverser.cxx
  15. 1 2
      panda/src/grutil/rigidBodyCombiner.cxx
  16. 2 8
      panda/src/grutil/shaderTerrainMesh.cxx
  17. 0 1
      panda/src/grutil/shaderTerrainMesh.h
  18. 1 11
      panda/src/parametrics/ropeNode.cxx
  19. 0 1
      panda/src/parametrics/ropeNode.h
  20. 1 11
      panda/src/parametrics/sheetNode.cxx
  21. 0 1
      panda/src/parametrics/sheetNode.h
  22. 1 1
      panda/src/pgraph/config_pgraph.h
  23. 23 12
      panda/src/pgraph/cullPlanes.cxx
  24. 6 5
      panda/src/pgraph/cullPlanes.h
  25. 103 42
      panda/src/pgraph/cullTraverser.I
  26. 90 50
      panda/src/pgraph/cullTraverser.cxx
  27. 23 11
      panda/src/pgraph/cullTraverser.h
  28. 99 31
      panda/src/pgraph/cullTraverserData.I
  29. 104 124
      panda/src/pgraph/cullTraverserData.cxx
  30. 18 6
      panda/src/pgraph/cullTraverserData.h
  31. 3 12
      panda/src/pgraph/geomNode.cxx
  32. 0 1
      panda/src/pgraph/geomNode.h
  33. 15 27
      panda/src/pgraph/instancedNode.cxx
  34. 1 12
      panda/src/pgraph/occluderNode.cxx
  35. 0 1
      panda/src/pgraph/occluderNode.h
  36. 65 0
      panda/src/pgraph/pandaNode.I
  37. 75 51
      panda/src/pgraph/pandaNode.cxx
  38. 28 4
      panda/src/pgraph/pandaNode.h
  39. 1 11
      panda/src/pgraph/planeNode.cxx
  40. 0 1
      panda/src/pgraph/planeNode.h
  41. 1 12
      panda/src/pgraph/portalNode.cxx
  42. 0 1
      panda/src/pgraph/portalNode.h
  43. 1 1
      panda/src/pgraph/showBoundsEffect.h
  44. 14 0
      panda/src/pgraph/transformState.I
  45. 1 0
      panda/src/pgraph/transformState.h
  46. 1 11
      panda/src/pgraphnodes/callbackNode.cxx
  47. 0 1
      panda/src/pgraphnodes/callbackNode.h
  48. 1 11
      panda/src/pgraphnodes/computeNode.cxx
  49. 0 1
      panda/src/pgraphnodes/computeNode.h
  50. 23 42
      panda/src/pgraphnodes/fadeLodNode.cxx
  51. 16 25
      panda/src/pgraphnodes/lodNode.cxx
  52. 2 9
      panda/src/pgraphnodes/nodeCullCallbackData.cxx
  53. 2 14
      panda/src/pgraphnodes/selectiveChildNode.I
  54. 0 41
      panda/src/pgraphnodes/selectiveChildNode.cxx
  55. 2 15
      panda/src/pgraphnodes/selectiveChildNode.h
  56. 4 15
      panda/src/pgraphnodes/sequenceNode.cxx
  57. 0 1
      panda/src/pgraphnodes/sequenceNode.h
  58. 4 15
      panda/src/pgraphnodes/switchNode.cxx
  59. 0 1
      panda/src/pgraphnodes/switchNode.h
  60. 1 2
      panda/src/pgui/pgEntry.cxx
  61. 2 13
      panda/src/pgui/pgItem.cxx
  62. 0 1
      panda/src/pgui/pgItem.h
  63. 4 13
      panda/src/pgui/pgTop.cxx
  64. 0 1
      panda/src/pgui/pgTop.h
  65. 4 0
      panda/src/pipeline/cycleDataLockedStageReader.I
  66. 2 13
      panda/src/text/textNode.cxx
  67. 0 1
      panda/src/text/textNode.h

+ 1 - 11
contrib/src/speedtree/speedTreeNode.cxx

@@ -1002,17 +1002,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool SpeedTreeNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes
@@ -1162,6 +1151,7 @@ set_transparent_texture_mode(SpeedTree::ETextureAlphaRenderMode eMode) const {
 void SpeedTreeNode::
 init_node() {
   PandaNode::set_cull_callback();
+  PandaNode::set_renderable();
 
   _is_valid = false;
   _needs_repopulate = false;

+ 0 - 1
contrib/src/speedtree/speedTreeNode.h

@@ -157,7 +157,6 @@ public:
                                          GeomTransformer &transformer);
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
   void prepare_scene(GraphicsStateGuardianBase *gsgbase, const RenderState *net_state);

+ 1 - 11
panda/src/bullet/bulletDebugNode.cxx

@@ -48,6 +48,7 @@ BulletDebugNode(const char *name) : PandaNode(name) {
   set_bounds(bounds);
   set_final(true);
   set_overall_hidden(true);
+  set_renderable();
 }
 
 /**
@@ -151,17 +152,6 @@ draw_mask_changed() {
   }
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool BulletDebugNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes

+ 0 - 1
panda/src/bullet/bulletDebugNode.h

@@ -55,7 +55,6 @@ public:
   virtual bool safe_to_combine_children() const;
   virtual bool safe_to_flatten_below() const;
 
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
 private:

+ 126 - 21
panda/src/collide/collisionLevelState.I

@@ -30,9 +30,9 @@ CollisionLevelState(const NodePath &node_path) :
  */
 template<class MaskType>
 INLINE CollisionLevelState<MaskType>::
-CollisionLevelState(const CollisionLevelState<MaskType> &parent, PandaNode *child) :
+CollisionLevelState(const CollisionLevelState<MaskType> &parent, const PandaNode::DownConnection &child, MaskType mask) :
   CollisionLevelStateBase(parent, child),
-  _current(parent._current)
+  _current(mask)
 {
 }
 #endif  // CPPPARSER
@@ -101,21 +101,21 @@ prepare_collider(const ColliderDef &def, const NodePath &root) {
 template<class MaskType>
 bool CollisionLevelState<MaskType>::
 any_in_bounds() {
-#ifndef NDEBUG
+#ifdef NDEBUG
+  const bool is_spam = false;
+#else
+  const bool is_spam = collide_cat.is_spam();
+#endif
   int indent_level = 0;
-  if (collide_cat.is_spam()) {
+  if (is_spam) {
     indent_level = _node_path.get_num_nodes() * 2;
     collide_cat.spam();
     indent(collide_cat.spam(false), indent_level)
       << "Considering " << _node_path.get_node_path() << "\n";
   }
-#endif  // NDEBUG
 
-  PandaNode *pnode = node();
-
-  CPT(BoundingVolume) node_bv = pnode->get_bounds();
-  if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-    const GeometricBoundingVolume *node_gbv = (const GeometricBoundingVolume *)node_bv.p();
+  if (_node_gbv != nullptr) {
+    PandaNode *pnode = node();
     CollideMask this_mask = pnode->get_net_collide_mask();
 
     int num_colliders = get_num_colliders();
@@ -131,13 +131,11 @@ any_in_bounds() {
           // Also don't test a node with itself, or with any of its
           // descendants.
           if (pnode == cnode) {
-#ifndef NDEBUG
-            if (collide_cat.is_spam()) {
+            if (is_spam) {
               indent(collide_cat.spam(false), indent_level)
                 << "Not comparing " << c << " to " << _node_path
                 << " (same node)\n";
             }
-#endif  // NDEBUG
 
           } else {
             // There are bits in common, and it's not the same instance, so go
@@ -148,16 +146,14 @@ any_in_bounds() {
             is_in = true;  // If there's no bounding volume, we're implicitly in.
 
             if (col_gbv != nullptr) {
-              is_in = (node_gbv->contains(col_gbv) != 0);
+              is_in = (_node_gbv->contains(col_gbv) != 0);
               _node_volume_pcollector.add_level(1);
 
-#ifndef NDEBUG
-              if (collide_cat.is_spam()) {
+              if (is_spam) {
                 indent(collide_cat.spam(false), indent_level)
                   << "Comparing " << c << ": " << *col_gbv
-                  << " to " << *node_gbv << ", is_in = " << is_in << "\n";
+                  << " to " << *_node_gbv << ", is_in = " << is_in << "\n";
               }
-#endif  // NDEBUG
             }
           }
         }
@@ -171,8 +167,7 @@ any_in_bounds() {
     }
   }
 
-#ifndef NDEBUG
-  if (collide_cat.is_spam()) {
+  if (is_spam) {
     int num_active_colliders = 0;
     int num_colliders = get_num_colliders();
     for (int c = 0; c < num_colliders; c++) {
@@ -201,11 +196,121 @@ any_in_bounds() {
     collide_cat.spam(false)
       << "\n";
   }
-#endif  // NDEBUG
   return has_any_collider();
 }
 #endif  // CPPPARSER
 
+#ifndef CPPPARSER
+/**
+ * Checks the bounding volume of the given child of the current node against
+ * each of our colliders.  Returns a mask indicating which colliders are inside
+ * of the bounding volume.
+ */
+template<class MaskType>
+MaskType CollisionLevelState<MaskType>::
+get_child_mask(const PandaNode::DownConnection &child) const {
+  PandaNode *pnode = child.get_child();
+#ifdef NDEBUG
+  const bool is_spam = false;
+#else
+  const bool is_spam = collide_cat.is_spam();
+#endif
+  int indent_level = 0;
+  if (is_spam) {
+    indent_level = (_node_path.get_num_nodes() + 1) * 2;
+    collide_cat.spam();
+    indent(collide_cat.spam(false), indent_level)
+      << "Considering " << _node_path << "/" << pnode->get_name() << "\n";
+  }
+
+  MaskType mask = _current;
+
+  const GeometricBoundingVolume *node_gbv = child.get_bounds();
+  if (node_gbv != nullptr) {
+    CollideMask node_mask = child.get_net_collide_mask();
+
+    int num_colliders = get_num_colliders();
+    for (int c = 0; c < num_colliders; c++) {
+      if (mask.get_bit(c)) {
+        CollisionNode *cnode = get_collider_node(c);
+        bool is_in = false;
+
+        // Don't even bother testing the bounding volume if there are no
+        // collide bits in common between our collider and this node.
+        CollideMask from_mask = cnode->get_from_collide_mask() & _include_mask;
+        if (!(from_mask & node_mask).is_zero()) {
+          // Also don't test a node with itself, or with any of its
+          // descendants.
+          if (pnode == cnode) {
+            if (is_spam) {
+              indent(collide_cat.spam(false), indent_level)
+                << "Not comparing " << c << " to " << _node_path << "/"
+                << pnode->get_name() << " (same node)\n";
+            }
+
+          } else {
+            // There are bits in common, and it's not the same instance, so go
+            // ahead and try the bounding volume.
+            const GeometricBoundingVolume *col_gbv =
+              get_local_bound(c);
+
+            is_in = true;  // If there's no bounding volume, we're implicitly in.
+
+            if (col_gbv != nullptr) {
+              is_in = (node_gbv->contains(col_gbv) != 0);
+              _node_volume_pcollector.add_level(1);
+
+              if (is_spam) {
+                indent(collide_cat.spam(false), indent_level)
+                  << "Comparing " << c << ": " << *col_gbv
+                  << " to " << *node_gbv << ", is_in = " << is_in << "\n";
+              }
+            }
+          }
+        }
+
+        if (!is_in) {
+          // This collider cannot intersect with any geometry at this node or
+          // below.
+          mask.clear_bit(c);
+        }
+      }
+    }
+  }
+
+  if (is_spam) {
+    int num_active_colliders = 0;
+    int num_colliders = get_num_colliders();
+    for (int c = 0; c < num_colliders; c++) {
+      if (mask.get_bit(c)) {
+        num_active_colliders++;
+      }
+    }
+
+    collide_cat.spam();
+    indent(collide_cat.spam(false), indent_level)
+      << _node_path.get_node_path() << "/" << pnode->get_name() << " has "
+      << num_active_colliders << " interested colliders";
+    if (num_colliders != 0) {
+      collide_cat.spam(false)
+        << " (";
+      for (int c = 0; c < num_colliders; c++) {
+        if (mask.get_bit(c)) {
+          CollisionNode *cnode = get_collider_node(c);
+          collide_cat.spam(false)
+            << " " << c << ". " << cnode->get_name();
+        }
+      }
+      collide_cat.spam(false)
+        << " )";
+    }
+    collide_cat.spam(false)
+      << "\n";
+  }
+  return mask;
+}
+#endif  // CPPPARSER
+
 #ifndef CPPPARSER
 /**
  * Applies the inverse transform from the current node, if any, onto all the

+ 3 - 1
panda/src/collide/collisionLevelState.h

@@ -37,7 +37,8 @@ public:
 #ifndef CPPPARSER
   INLINE CollisionLevelState(const NodePath &node_path);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &parent,
-                             PandaNode *child);
+                             const PandaNode::DownConnection &child,
+                             MaskType mask);
   INLINE CollisionLevelState(const CollisionLevelState<MaskType> &copy);
   INLINE void operator = (const CollisionLevelState<MaskType> &copy);
 
@@ -45,6 +46,7 @@ public:
   INLINE void prepare_collider(const ColliderDef &def, const NodePath &root);
 
   bool any_in_bounds();
+  MaskType get_child_mask(const PandaNode::DownConnection &child) const;
   bool apply_transform();
 
   INLINE static bool has_max_colliders();

+ 28 - 3
panda/src/collide/collisionLevelStateBase.I

@@ -18,7 +18,8 @@ INLINE CollisionLevelStateBase::
 CollisionLevelStateBase(const NodePath &node_path) :
   _node_path(node_path),
   _colliders(get_class_type()),
-  _include_mask(CollideMask::all_on())
+  _include_mask(CollideMask::all_on()),
+  _node_gbv(node_path.node()->get_bounds()->as_geometric_bounding_volume())
 {
 }
 
@@ -30,7 +31,21 @@ CollisionLevelStateBase(const CollisionLevelStateBase &parent, PandaNode *child)
   _node_path(parent._node_path, child),
   _colliders(parent._colliders),
   _include_mask(parent._include_mask),
-  _local_bounds(parent._local_bounds)
+  _local_bounds(parent._local_bounds),
+  _node_gbv(child->get_bounds()->as_geometric_bounding_volume())
+{
+}
+
+/**
+ * This constructor goes to the next child node in the traversal.
+ */
+INLINE CollisionLevelStateBase::
+CollisionLevelStateBase(const CollisionLevelStateBase &parent, const PandaNode::DownConnection &child) :
+  _node_path(parent._node_path, child.get_child()),
+  _colliders(parent._colliders),
+  _include_mask(parent._include_mask),
+  _local_bounds(parent._local_bounds),
+  _node_gbv(child.get_bounds())
 {
 }
 
@@ -43,7 +58,8 @@ CollisionLevelStateBase(const CollisionLevelStateBase &copy) :
   _colliders(copy._colliders),
   _include_mask(copy._include_mask),
   _local_bounds(copy._local_bounds),
-  _parent_bounds(copy._parent_bounds)
+  _parent_bounds(copy._parent_bounds),
+  _node_gbv(copy._node_gbv)
 {
 }
 
@@ -57,6 +73,7 @@ operator = (const CollisionLevelStateBase &copy) {
   _include_mask = copy._include_mask;
   _local_bounds = copy._local_bounds;
   _parent_bounds = copy._parent_bounds;
+  _node_gbv = copy._node_gbv;
 }
 
 /**
@@ -113,6 +130,14 @@ get_collider_node_path(int n) const {
   return _colliders[n]._node_path;
 }
 
+/**
+ * Returns the bounding volume of the current node.
+ */
+INLINE const GeometricBoundingVolume *CollisionLevelStateBase::
+get_node_bound() const {
+  return _node_gbv;
+}
+
 /**
  * Returns the bounding volume of the indicated collider, transformed into the
  * current node's transform space.

+ 5 - 0
panda/src/collide/collisionLevelStateBase.h

@@ -52,6 +52,8 @@ public:
   INLINE CollisionLevelStateBase(const NodePath &node_path);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
                                  PandaNode *child);
+  INLINE CollisionLevelStateBase(const CollisionLevelStateBase &parent,
+                                 const PandaNode::DownConnection &child);
   INLINE CollisionLevelStateBase(const CollisionLevelStateBase &copy);
   INLINE void operator = (const CollisionLevelStateBase &copy);
 
@@ -67,6 +69,7 @@ public:
   INLINE const CollisionSolid *get_collider(int n) const;
   INLINE CollisionNode *get_collider_node(int n) const;
   INLINE NodePath get_collider_node_path(int n) const;
+  INLINE const GeometricBoundingVolume *get_node_bound() const;
   INLINE const GeometricBoundingVolume *get_local_bound(int n) const;
   INLINE const GeometricBoundingVolume *get_parent_bound(int n) const;
 
@@ -80,6 +83,8 @@ protected:
   Colliders _colliders;
   CollideMask _include_mask;
 
+  const GeometricBoundingVolume *_node_gbv = nullptr;
+
   typedef PTA(CPT(GeometricBoundingVolume)) BoundingVolumes;
   BoundingVolumes _local_bounds;
   BoundingVolumes _parent_bounds;

+ 4 - 21
panda/src/collide/collisionNode.cxx

@@ -43,6 +43,7 @@ CollisionNode(const std::string &name) :
   _collider_sort(0)
 {
   set_cull_callback();
+  set_renderable();
 
   // CollisionNodes are hidden by default.
   set_overall_hidden(true);
@@ -186,11 +187,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     CPT(CollisionSolid) solid = (*si).get_read_pointer();
     PT(PandaNode) node = solid->get_viz(trav, data, false);
     if (node != nullptr) {
-      CullTraverserData next_data(data, node);
-
       // We don't want to inherit the render state from above for these guys.
-      next_data._state = RenderState::make_empty();
-      trav->traverse(next_data);
+      trav->traverse_down(data, node, data._net_transform, RenderState::make_empty());
     }
   }
 
@@ -208,12 +206,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
         CPT(CollisionSolid) solid = (*si).get_read_pointer();
         PT(PandaNode) node = solid->get_viz(trav, data, false);
         if (node != nullptr) {
-          CullTraverserData next_data(data, node);
-
-          next_data._net_transform =
-            next_data._net_transform->compose(transform);
-          next_data._state = get_last_pos_state();
-          trav->traverse(next_data);
+          trav->traverse_down(data, node,
+            data._net_transform->compose(transform), get_last_pos_state());
         }
       }
     }
@@ -223,17 +217,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool CollisionNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * A simple downcast check.  Returns true if this kind of node happens to
  * inherit from CollisionNode, false otherwise.

+ 0 - 1
panda/src/collide/collisionNode.h

@@ -43,7 +43,6 @@ public:
   virtual CollideMask get_legal_collide_mask() const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual bool is_collision_node() const;
 
   virtual void output(std::ostream &out) const;

+ 92 - 99
panda/src/collide/collisionTraverser.cxx

@@ -288,7 +288,9 @@ traverse(const NodePath &root) {
 #ifdef DO_PSTATS
         PStatTimer pass_timer(get_pass_collector(pass));
 #endif
-        r_traverse_single(level_states[pass], pass);
+        if (level_states[pass].any_in_bounds()) {
+          r_traverse_single(level_states[pass], pass);
+        }
       }
     }
   }
@@ -562,22 +564,13 @@ prepare_colliders_single(CollisionTraverser::LevelStatesSingle &level_states,
  */
 void CollisionTraverser::
 r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
     return;
   }
 
   PandaNode *node = level_state.node();
   if (node->is_collision_node()) {
-    CollisionNode *cnode;
-    DCAST_INTO_V(cnode, node);
-    CPT(BoundingVolume) node_bv = cnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    CollisionNode *cnode = (CollisionNode *)node;
 
     CollisionEntry entry;
     entry._into_node = cnode;
@@ -589,13 +582,14 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     int num_colliders = level_state.get_num_colliders();
     for (int c = 0; c < num_colliders; ++c) {
       if (level_state.has_collider(c)) {
-        entry._from_node = level_state.get_collider_node(c);
+        CollisionNode *from_node = level_state.get_collider_node(c);
 
-        if ((entry._from_node->get_from_collide_mask() &
+        if ((from_node->get_from_collide_mask() &
              cnode->get_into_collide_mask()) != 0) {
           #ifdef DO_PSTATS
           // PStatTimer collide_timer(_solid_collide_collectors[pass]);
           #endif
+          entry._from_node = from_node;
           entry._from_node_path = level_state.get_collider_node_path(c);
           entry._from = level_state.get_collider(c);
 
@@ -603,7 +597,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -616,13 +610,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     }
     #endif
 
-    GeomNode *gnode;
-    DCAST_INTO_V(gnode, node);
-    CPT(BoundingVolume) node_bv = gnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    GeomNode *gnode = (GeomNode *)node;
 
     CollisionEntry entry;
     entry._into_node = gnode;
@@ -648,7 +636,7 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -658,9 +646,14 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     int index = node->get_visible_child();
-    if (index >= 0 && index < node->get_num_children()) {
-      CollisionLevelStateSingle next_state(level_state, node->get_child(index));
-      r_traverse_single(next_state, pass);
+    PandaNode::Children children = node->get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateSingle next_state(level_state, child, mask);
+        r_traverse_single(next_state, pass);
+      }
     }
 
   } else if (node->is_lod_node()) {
@@ -673,12 +666,16 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateSingle next_state(level_state, children.get_child(i));
-      if (i != index) {
-        next_state.set_include_mask(next_state.get_include_mask() &
-          ~GeomNode::get_default_collide_mask());
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateSingle next_state(level_state, child, mask);
+        if (i != index) {
+          next_state.set_include_mask(next_state.get_include_mask() &
+            ~GeomNode::get_default_collide_mask());
+        }
+        r_traverse_single(next_state, pass);
       }
-      r_traverse_single(next_state, pass);
     }
 
   } else {
@@ -686,8 +683,12 @@ r_traverse_single(CollisionLevelStateSingle &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateSingle next_state(level_state, children.get_child(i));
-      r_traverse_single(next_state, pass);
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateSingle::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateSingle next_state(level_state, child, mask);
+        r_traverse_single(next_state, pass);
+      }
     }
   }
 }
@@ -773,22 +774,13 @@ prepare_colliders_double(CollisionTraverser::LevelStatesDouble &level_states,
  */
 void CollisionTraverser::
 r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
     return;
   }
 
   PandaNode *node = level_state.node();
   if (node->is_collision_node()) {
-    CollisionNode *cnode;
-    DCAST_INTO_V(cnode, node);
-    CPT(BoundingVolume) node_bv = cnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    CollisionNode *cnode = (CollisionNode *)node;
 
     CollisionEntry entry;
     entry._into_node = cnode;
@@ -814,7 +806,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -827,13 +819,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     }
     #endif
 
-    GeomNode *gnode;
-    DCAST_INTO_V(gnode, node);
-    CPT(BoundingVolume) node_bv = gnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    GeomNode *gnode = (GeomNode *)node;
 
     CollisionEntry entry;
     entry._into_node = gnode;
@@ -859,7 +845,7 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -869,9 +855,14 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     int index = node->get_visible_child();
-    if (index >= 0 && index < node->get_num_children()) {
-      CollisionLevelStateDouble next_state(level_state, node->get_child(index));
-      r_traverse_double(next_state, pass);
+    PandaNode::Children children = node->get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateDouble next_state(level_state, child, mask);
+        r_traverse_double(next_state, pass);
+      }
     }
 
   } else if (node->is_lod_node()) {
@@ -880,16 +871,20 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     // visit all other levels without GeomNode::get_default_collide_mask(),
     // allowing only collision with CollisionNodes and special geometry under
     // higher levels of detail.
-    int index = DCAST(LODNode, node)->get_lowest_switch();
+    int index = ((LODNode *)node)->get_lowest_switch();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateDouble next_state(level_state, children.get_child(i));
-      if (i != index) {
-        next_state.set_include_mask(next_state.get_include_mask() &
-          ~GeomNode::get_default_collide_mask());
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateDouble next_state(level_state, child, mask);
+        if (i != index) {
+          next_state.set_include_mask(next_state.get_include_mask() &
+            ~GeomNode::get_default_collide_mask());
+        }
+        r_traverse_double(next_state, pass);
       }
-      r_traverse_double(next_state, pass);
     }
 
   } else {
@@ -897,8 +892,12 @@ r_traverse_double(CollisionLevelStateDouble &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateDouble next_state(level_state, children.get_child(i));
-      r_traverse_double(next_state, pass);
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateDouble::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateDouble next_state(level_state, child, mask);
+        r_traverse_double(next_state, pass);
+      }
     }
   }
 }
@@ -984,22 +983,13 @@ prepare_colliders_quad(CollisionTraverser::LevelStatesQuad &level_states,
  */
 void CollisionTraverser::
 r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
-  if (!level_state.any_in_bounds()) {
-    return;
-  }
   if (!level_state.apply_transform()) {
     return;
   }
 
   PandaNode *node = level_state.node();
   if (node->is_collision_node()) {
-    CollisionNode *cnode;
-    DCAST_INTO_V(cnode, node);
-    CPT(BoundingVolume) node_bv = cnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    CollisionNode *cnode = (CollisionNode *)node;
 
     CollisionEntry entry;
     entry._into_node = cnode;
@@ -1025,7 +1015,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -1038,13 +1028,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     }
     #endif
 
-    GeomNode *gnode;
-    DCAST_INTO_V(gnode, node);
-    CPT(BoundingVolume) node_bv = gnode->get_bounds();
-    const GeometricBoundingVolume *node_gbv = nullptr;
-    if (node_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-      DCAST_INTO_V(node_gbv, node_bv);
-    }
+    GeomNode *gnode = (GeomNode *)node;
 
     CollisionEntry entry;
     entry._into_node = gnode;
@@ -1070,7 +1054,7 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
               entry,
               level_state.get_parent_bound(c),
               level_state.get_local_bound(c),
-              node_gbv);
+              level_state.get_node_bound());
         }
       }
     }
@@ -1080,9 +1064,14 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     // If it's a switch node or sequence node, visit just the one visible
     // child.
     int index = node->get_visible_child();
-    if (index >= 0 && index < node->get_num_children()) {
-      CollisionLevelStateQuad next_state(level_state, node->get_child(index));
-      r_traverse_quad(next_state, pass);
+    PandaNode::Children children = node->get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateQuad next_state(level_state, child, mask);
+        r_traverse_quad(next_state, pass);
+      }
     }
 
   } else if (node->is_lod_node()) {
@@ -1091,16 +1080,20 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     // visit all other levels without GeomNode::get_default_collide_mask(),
     // allowing only collision with CollisionNodes and special geometry under
     // higher levels of detail.
-    int index = DCAST(LODNode, node)->get_lowest_switch();
+    int index = ((LODNode *)node)->get_lowest_switch();
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateQuad next_state(level_state, children.get_child(i));
-      if (i != index) {
-        next_state.set_include_mask(next_state.get_include_mask() &
-          ~GeomNode::get_default_collide_mask());
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateQuad next_state(level_state, child, mask);
+        if (i != index) {
+          next_state.set_include_mask(next_state.get_include_mask() &
+            ~GeomNode::get_default_collide_mask());
+        }
+        r_traverse_quad(next_state, pass);
       }
-      r_traverse_quad(next_state, pass);
     }
 
   } else {
@@ -1108,8 +1101,12 @@ r_traverse_quad(CollisionLevelStateQuad &level_state, size_t pass) {
     PandaNode::Children children = node->get_children();
     int num_children = children.get_num_children();
     for (int i = 0; i < num_children; ++i) {
-      CollisionLevelStateQuad next_state(level_state, children.get_child(i));
-      r_traverse_quad(next_state, pass);
+      const PandaNode::DownConnection &child = children.get_child_connection(i);
+      CollisionLevelStateQuad::CurrentMask mask = level_state.get_child_mask(child);
+      if (!mask.is_zero()) {
+        CollisionLevelStateQuad next_state(level_state, child, mask);
+        r_traverse_quad(next_state, pass);
+      }
     }
   }
 }
@@ -1162,10 +1159,7 @@ compare_collider_to_node(CollisionEntry &entry,
         // CollisionNodes.  We are already filtering out tests for a
         // CollisionNode into itself.
         CPT(BoundingVolume) solid_bv = entry._into->get_bounds();
-        const GeometricBoundingVolume *solid_gbv = nullptr;
-        if (solid_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
-          solid_gbv = (const GeometricBoundingVolume *)solid_bv.p();
-        }
+        const GeometricBoundingVolume *solid_gbv = solid_bv->as_geometric_bounding_volume();
 
         compare_collider_to_solid(entry, from_node_gbv, solid_gbv);
       }
@@ -1198,14 +1192,13 @@ compare_collider_to_geom_node(CollisionEntry &entry,
       if (geom != nullptr) {
         CPT(BoundingVolume) geom_bv = geom->get_bounds();
         const GeometricBoundingVolume *geom_gbv = nullptr;
-        if (num_geoms > 1 &&
-            geom_bv->is_of_type(GeometricBoundingVolume::get_class_type())) {
+        if (num_geoms > 1) {
           // Only bother to test against each geom's bounding volume if we
           // have more than one geom in the node, as a slight optimization.
           // (If the node contains just one geom, then the node's bounding
           // volume, which we just tested, is the same as the geom's bounding
           // volume.)
-          DCAST_INTO_V(geom_gbv, geom_bv);
+          geom_gbv = geom_bv->as_geometric_bounding_volume();
         }
 
         compare_collider_to_geom(entry, geom, from_node_gbv, geom_gbv);

+ 3 - 16
panda/src/collide/collisionVisualizer.cxx

@@ -43,6 +43,7 @@ TypeHandle CollisionVisualizer::_type_handle;
 CollisionVisualizer::
 CollisionVisualizer(const std::string &name) : PandaNode(name), _lock("CollisionVisualizer") {
   set_cull_callback();
+  set_renderable();
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // doesn't appear to have any geometry within it).
@@ -62,6 +63,7 @@ CollisionVisualizer(const CollisionVisualizer &copy) :
   _normal_scale(copy._normal_scale) {
 
   set_cull_callback();
+  set_renderable();
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // doesn't appear to have any geometry within it).
@@ -145,12 +147,9 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
       bool was_detected = (solid_info._detected_count > 0);
       PT(PandaNode) node = solid->get_viz(trav, xform_data, !was_detected);
       if (node != nullptr) {
-        CullTraverserData next_data(xform_data, node);
-
         // We don't want to inherit the render state from above for these
         // guys.
-        next_data._state = get_viz_state();
-        trav->traverse(next_data);
+        trav->traverse_down(xform_data, node, xform_data._net_transform, get_viz_state());
       }
     }
 
@@ -251,18 +250,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool CollisionVisualizer::
-is_renderable() const {
-  return true;
-}
-
-
 /**
  * Writes a brief description of the node to the indicated output stream.
  * This is invoked by the << operator.  It may be overridden in derived

+ 0 - 1
panda/src/collide/collisionVisualizer.h

@@ -50,7 +50,6 @@ public:
   // from parent class PandaNode.
   virtual PandaNode *make_copy() const;
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void output(std::ostream &out) const;
 
   // from parent class CollisionRecorder.

+ 1 - 1
panda/src/grutil/pipeOcclusionCullTraverser.cxx

@@ -290,7 +290,7 @@ bool PipeOcclusionCullTraverser::
 is_in_view(CullTraverserData &data) {
   _next_query = nullptr;
 
-  if (!CullTraverser::is_in_view(data)) {
+  if (!data.is_in_view(get_camera_mask())) {
     return false;
   }
   if (!_live) {

+ 1 - 2
panda/src/grutil/rigidBodyCombiner.cxx

@@ -145,8 +145,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   }
 
   // Render the internal scene only--this is the optimized scene.
-  CullTraverserData next_data(data, _internal_root);
-  trav->traverse(next_data);
+  trav->traverse_down(data, _internal_root);
 
   // Do not directly render the nodes beneath this node.
   return false;

+ 2 - 8
panda/src/grutil/shaderTerrainMesh.cxx

@@ -107,6 +107,7 @@ ShaderTerrainMesh::ShaderTerrainMesh() :
   _update_enabled(true),
   _heightfield_tex(nullptr)
 {
+  set_renderable();
 }
 
 /**
@@ -440,14 +441,7 @@ void ShaderTerrainMesh::do_create_chunk_geom() {
 }
 
 /**
- * @copydoc PandaNode::is_renderable()
- */
-bool ShaderTerrainMesh::is_renderable() const {
-  return true;
-}
-
-/**
- * @copydoc PandaNode::is_renderable()
+ * @copydoc PandaNode::safe_to_flatten()
  */
 bool ShaderTerrainMesh::safe_to_flatten() const {
   return false;

+ 0 - 1
panda/src/grutil/shaderTerrainMesh.h

@@ -85,7 +85,6 @@ PUBLISHED:
 public:
 
   // Methods derived from PandaNode
-  virtual bool is_renderable() const;
   virtual bool safe_to_flatten() const;
   virtual bool safe_to_combine() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);

+ 1 - 11
panda/src/parametrics/ropeNode.cxx

@@ -71,6 +71,7 @@ RopeNode(const std::string &name) :
   PandaNode(name)
 {
   set_cull_callback();
+  set_renderable();
 }
 
 /**
@@ -162,17 +163,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool RopeNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  *
  */

+ 0 - 1
panda/src/parametrics/ropeNode.h

@@ -45,7 +45,6 @@ public:
 
   virtual bool safe_to_transform() const;
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
 PUBLISHED:
   enum RenderMode {

+ 1 - 11
panda/src/parametrics/sheetNode.cxx

@@ -70,6 +70,7 @@ SheetNode(const std::string &name) :
   PandaNode(name)
 {
   set_cull_callback();
+  set_renderable();
 }
 
 /**
@@ -140,17 +141,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool SheetNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  *
  */

+ 0 - 1
panda/src/parametrics/sheetNode.h

@@ -43,7 +43,6 @@ public:
 
   virtual bool safe_to_transform() const;
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
 PUBLISHED:
   INLINE void set_surface(NurbsSurfaceEvaluator *surface);

+ 1 - 1
panda/src/pgraph/config_pgraph.h

@@ -30,7 +30,7 @@ NotifyCategoryDecl(pgraph, EXPCL_PANDA_PGRAPH, EXPTP_PANDA_PGRAPH);
 NotifyCategoryDecl(loader, EXPCL_PANDA_PGRAPH, EXPTP_PANDA_PGRAPH);
 NotifyCategoryDecl(portal, EXPCL_PANDA_PGRAPH, EXPTP_PANDA_PGRAPH);
 
-extern ConfigVariableBool fake_view_frustum_cull;
+extern EXPCL_PANDA_PGRAPH ConfigVariableBool fake_view_frustum_cull;
 extern ConfigVariableBool clip_plane_cull;
 extern ConfigVariableBool allow_portal_cull;
 extern ConfigVariableBool debug_portal_cull;

+ 23 - 12
panda/src/pgraph/cullPlanes.cxx

@@ -79,26 +79,33 @@ xform(const LMatrix4 &mat) const {
  * will be added to the state, unless those ClipPlanes are also listed in
  * off_attrib.
  */
-CPT(CullPlanes) CullPlanes::
-apply_state(const CullTraverser *trav, const CullTraverserData *data,
+void CullPlanes::
+apply_state(CPT(CullPlanes) &planes,
+            const CullTraverser *trav, const CullTraverserData *data,
             const ClipPlaneAttrib *net_attrib,
             const ClipPlaneAttrib *off_attrib,
-            const OccluderEffect *node_effect) const {
+            const OccluderEffect *node_effect) {
   if (net_attrib == nullptr && node_effect == nullptr) {
-    return this;
+    return;
   }
 
   PT(CullPlanes) new_planes;
-  if (get_ref_count() == 1) {
-    new_planes = (CullPlanes *)this;
-  } else {
-    new_planes = new CullPlanes(*this);
+  if (planes != nullptr) {
+    if (planes->get_ref_count() == 1) {
+      new_planes = (CullPlanes *)planes.p();
+    } else {
+      new_planes = new CullPlanes(*planes);
+    }
   }
 
   CPT(TransformState) net_transform = nullptr;
 
   if (net_attrib != nullptr) {
     int num_on_planes = net_attrib->get_num_on_planes();
+    if (num_on_planes > 0 && new_planes.is_null()) {
+      new_planes = new CullPlanes;
+    }
+
     for (int i = 0; i < num_on_planes; ++i) {
       NodePath clip_plane = net_attrib->get_on_plane(i);
       Planes::const_iterator pi = new_planes->_planes.find(clip_plane);
@@ -128,13 +135,17 @@ apply_state(const CullTraverser *trav, const CullTraverserData *data,
     const Lens *lens = scene->get_lens();
 
     int num_on_occluders = node_effect->get_num_on_occluders();
+    if (num_on_occluders > 0 && new_planes.is_null()) {
+      new_planes = new CullPlanes;
+    }
+
     for (int i = 0; i < num_on_occluders; ++i) {
       NodePath occluder = node_effect->get_on_occluder(i);
       Occluders::const_iterator oi = new_planes->_occluders.find(occluder);
       if (oi == new_planes->_occluders.end()) {
         // Here's a new occluder; consider adding it to the list.
         OccluderNode *occluder_node = DCAST(OccluderNode, occluder.node());
-        nassertr(occluder_node->get_num_vertices() == 4, new_planes);
+        nassertv(occluder_node->get_num_vertices() == 4);
 
         CPT(TransformState) occluder_transform = occluder.get_transform(scene->get_cull_center());
 
@@ -247,7 +258,7 @@ apply_state(const CullTraverser *trav, const CullTraverserData *data,
         // existing occluder volumes.
         bool is_enclosed = false;
         Occluders::const_iterator oi;
-        for (oi = _occluders.begin(); oi != _occluders.end(); ++oi) {
+        for (oi = new_planes->_occluders.begin(); oi != new_planes->_occluders.end(); ++oi) {
           int occluder_result = (*oi).second->contains(occluder_gbv);
           if ((occluder_result & BoundingVolume::IF_all) != 0) {
             is_enclosed = true;
@@ -292,14 +303,14 @@ apply_state(const CullTraverser *trav, const CullTraverserData *data,
 
         if (show_occluder_volumes) {
           // Draw the frustum for visualization.
-          nassertr(net_transform != nullptr, new_planes);
+          nassertv(net_transform != nullptr);
           trav->draw_bounding_volume(frustum, data->get_internal_transform(trav));
         }
       }
     }
   }
 
-  return new_planes;
+  planes = std::move(new_planes);
 }
 
 /**

+ 6 - 5
panda/src/pgraph/cullPlanes.h

@@ -53,11 +53,12 @@ public:
 
   static CPT(CullPlanes) make_empty();
   CPT(CullPlanes) xform(const LMatrix4 &mat) const;
-  CPT(CullPlanes) apply_state(const CullTraverser *trav,
-                              const CullTraverserData *data,
-                              const ClipPlaneAttrib *net_attrib,
-                              const ClipPlaneAttrib *off_attrib,
-                              const OccluderEffect *node_effect) const;
+  static void apply_state(CPT(CullPlanes) &planes,
+                          const CullTraverser *trav,
+                          const CullTraverserData *data,
+                          const ClipPlaneAttrib *net_attrib,
+                          const ClipPlaneAttrib *off_attrib,
+                          const OccluderEffect *node_effect);
   CPT(CullPlanes) do_cull(int &result, CPT(RenderState) &state,
                           const GeometricBoundingVolume *node_gbv) const;
 

+ 103 - 42
panda/src/pgraph/cullTraverser.I

@@ -183,6 +183,14 @@ get_effective_incomplete_render() const {
   return _effective_incomplete_render;
 }
 
+/**
+ * Returns true if fake view frustum culling is active.
+ */
+INLINE bool CullTraverser::
+get_fake_view_frustum_cull() const {
+  return _fake_view_frustum_cull;
+}
+
 /**
  * Flushes the PStatCollectors used during traversal.
  */
@@ -195,53 +203,106 @@ flush_level() {
 }
 
 /**
- * This is implemented inline to reduce recursion.
+ * Calls traverse_down on each child.
+ */
+INLINE void CullTraverser::
+traverse_below(CullTraverserData &data) {
+  PandaNodePipelineReader *node_reader = data.node_reader();
+  PandaNode::Children children = node_reader->get_children();
+  node_reader->release();
+  int num_children = children.get_num_children();
+  for (int i = 0; i < num_children; ++i) {
+    traverse_down(data, children.get_child_connection(i), data._state);
+  }
+}
+
+/**
+ * Traverses a child of the given node/data.
+ */
+INLINE void CullTraverser::
+traverse_down(const CullTraverserData &data, PandaNode *node) {
+  traverse_down(data, node, data._net_transform, data._state);
+}
+
+/**
+ * Traverses down into the given node/data, overriding the current transform
+ * and state.  Note that the state and transform on the given node are still
+ * applied.
  */
 INLINE void CullTraverser::
-do_traverse(CullTraverserData &data) {
-  if (is_in_view(data)) {
-    if (pgraph_cat.is_spam()) {
-      pgraph_cat.spam()
-        << "\n" << data.get_node_path()
-        << " " << data._draw_mask << "\n";
+traverse_down(const CullTraverserData &data, PandaNode *node,
+               const TransformState *net_transform, const RenderState *state) {
+  PandaNodePipelineReader node_reader(node, data._node_reader.get_current_thread());
+
+  int result = data.is_child_in_view(node_reader, _camera_mask);
+  if (result == BoundingVolume::IF_no_intersection) {
+#ifdef NDEBUG
+    return;
+#else
+    if (UNLIKELY(_fake_view_frustum_cull)) {
+      do_fake_cull(data, node, net_transform, state);
     }
+    return;
+#endif
+  }
+
+  GeometricBoundingVolume *view_frustum = nullptr;
+  if ((result & BoundingVolume::IF_all) == 0 && !node_reader.is_final()) {
+    view_frustum = data._view_frustum;
+  }
 
-    PandaNodePipelineReader *node_reader = data.node_reader();
-    int fancy_bits = node_reader->get_fancy_bits();
-
-    if (fancy_bits == 0 && data._cull_planes->is_empty()) {
-      // Nothing interesting in this node; just move on.
-
-    } else {
-      // Something in this node is worth taking a closer look.
-      const RenderEffects *node_effects = node_reader->get_effects();
-      if (node_effects->has_show_bounds()) {
-        // If we should show the bounding volume for this node, make it up
-        // now.
-        show_bounds(data, node_effects->has_show_tight_bounds());
-      }
-
-      data.apply_transform_and_state(this);
-
-      const FogAttrib *fog = (const FogAttrib *)
-        node_reader->get_state()->get_attrib(FogAttrib::get_class_slot());
-
-      if (fog != nullptr && fog->get_fog() != nullptr) {
-        // If we just introduced a FogAttrib here, call adjust_to_camera()
-        // now.  This maybe isn't the perfect time to call it, but it's good
-        // enough; and at this time we have all the information we need for
-        // it.
-        fog->get_fog()->adjust_to_camera(get_camera_transform());
-      }
-
-      if (fancy_bits & PandaNode::FB_cull_callback) {
-        PandaNode *node = data.node();
-        if (!node->cull_callback(this, data)) {
-          return;
-        }
-      }
+  CullTraverserData next_data(data, std::move(node_reader), net_transform, state, view_frustum);
+  if (data._cull_planes != nullptr) {
+    const GeometricBoundingVolume *node_gbv = node_reader.get_bounds()->as_geometric_bounding_volume();
+    if (!next_data.apply_cull_planes(data._cull_planes, node_gbv)) {
+      return;
     }
+  }
+
+  do_traverse(next_data);
+}
+
+/**
+ * Traverses down into the given node, as though it were a child node of the
+ * current node.
+ */
+INLINE void CullTraverser::
+traverse_down(const CullTraverserData &data, const PandaNode::DownConnection &child) {
+  traverse_down(data, child, data._state);
+}
 
-    traverse_below(data);
+/**
+ * Traverses down into the given node, as though it were a child node of the
+ * current node, except that the current state is ignored and replaced with the
+ * given state.  The state on the child node is still applied.
+ */
+INLINE void CullTraverser::
+traverse_down(const CullTraverserData &data, const PandaNode::DownConnection &child, const RenderState *state) {
+  int result = data.is_child_in_view(child, _camera_mask);
+  if (result == BoundingVolume::IF_no_intersection) {
+#ifdef NDEBUG
+    return;
+#else
+    if (UNLIKELY(_fake_view_frustum_cull)) {
+      do_fake_cull(data, child.get_child(), data._net_transform, state);
+    }
+    return;
+#endif
+  }
+
+  PandaNodePipelineReader node_reader(child.get_child(), data._node_reader.get_current_thread());
+
+  GeometricBoundingVolume *view_frustum = nullptr;
+  if ((result & BoundingVolume::IF_all) == 0 && !node_reader.is_final()) {
+    view_frustum = data._view_frustum;
   }
+
+  CullTraverserData next_data(data, std::move(node_reader), data._net_transform, state, view_frustum);
+
+  if (data._cull_planes != nullptr &&
+      !next_data.apply_cull_planes(data._cull_planes, child.get_bounds())) {
+    return;
+  }
+
+  do_traverse(next_data);
 }

+ 90 - 50
panda/src/pgraph/cullTraverser.cxx

@@ -17,6 +17,7 @@
 #include "transformState.h"
 #include "renderState.h"
 #include "colorAttrib.h"
+#include "textureAttrib.h"
 #include "renderModeAttrib.h"
 #include "cullFaceAttrib.h"
 #include "depthOffsetAttrib.h"
@@ -48,14 +49,15 @@ TypeHandle CullTraverser::_type_handle;
 CullTraverser::
 CullTraverser() :
   _gsg(nullptr),
-  _current_thread(Thread::get_current_thread())
+  _current_thread(Thread::get_current_thread()),
+  _camera_mask(DrawMask::all_on()),
+  _fake_view_frustum_cull(false),
+  _has_tag_state_key(false),
+  _initial_state(RenderState::make_empty()),
+  _cull_handler(nullptr),
+  _portal_clipper(nullptr),
+  _effective_incomplete_render(false)
 {
-  _camera_mask = DrawMask::all_on();
-  _has_tag_state_key = false;
-  _initial_state = RenderState::make_empty();
-  _cull_handler = nullptr;
-  _portal_clipper = nullptr;
-  _effective_incomplete_render = true;
 }
 
 /**
@@ -67,6 +69,7 @@ CullTraverser(const CullTraverser &copy) :
   _current_thread(copy._current_thread),
   _scene_setup(copy._scene_setup),
   _camera_mask(copy._camera_mask),
+  _fake_view_frustum_cull(copy._fake_view_frustum_cull),
   _has_tag_state_key(copy._has_tag_state_key),
   _tag_state_key(copy._tag_state_key),
   _initial_state(copy._initial_state),
@@ -99,6 +102,10 @@ set_scene(SceneSetup *scene_setup, GraphicsStateGuardianBase *gsg,
   _effective_incomplete_render = _gsg->get_incomplete_render() && dr_incomplete_render;
 
   _view_frustum = scene_setup->get_view_frustum();
+
+#ifndef NDEBUG
+  _fake_view_frustum_cull = fake_view_frustum_cull;
+#endif
 }
 
 /**
@@ -132,7 +139,9 @@ traverse(const NodePath &root) {
                            _initial_state, _view_frustum,
                            _current_thread);
 
-    traverse(data);
+    if (data.is_in_view(_camera_mask)) {
+      do_traverse(data);
+    }
 
     // Finally add the lines to be drawn
     if (debug_portal_cull) {
@@ -143,45 +152,68 @@ traverse(const NodePath &root) {
     NodePath cull_center = _scene_setup->get_cull_center();
     CPT(TransformState) transform = cull_center.get_transform(root);
 
-    CullTraverserData my_data(data, portal_viewer._previous);
-    my_data._net_transform = my_data._net_transform->compose(transform);
-    traverse(my_data);
+    CullTraverserData my_data(data, portal_viewer._previous,
+                              data._net_transform->compose(transform),
+                              data._state, data._view_frustum);
+    if (my_data.is_in_view(_camera_mask)) {
+      do_traverse(my_data);
+    }
 
   } else {
     CullTraverserData data(root, TransformState::make_identity(),
                            _initial_state, _view_frustum,
                            _current_thread);
 
-    do_traverse(data);
+    if (data.is_in_view(_camera_mask)) {
+      do_traverse(data);
+    }
   }
 }
 
 /**
- * Traverses from the next node with the given data, which has been
- * constructed with the node but has not yet been converted into the node's
- * space.
+ * Internal method called by traverse() and traverse_down().  Traverses the
+ * given node, assuming it has already been checked with is_in_view().
  */
 void CullTraverser::
-traverse(CullTraverserData &data) {
-  do_traverse(data);
-}
+do_traverse(CullTraverserData &data) {
+#ifndef NDEBUG
+  if (UNLIKELY(pgraph_cat.is_spam())) {
+    pgraph_cat.spam()
+      << "\n" << data.get_node_path()
+      << " " << data._draw_mask << "\n";
+  }
+#endif
 
-/**
- * Traverses all the children of the indicated node, with the given data,
- * which has been converted into the node's space.
- */
-void CullTraverser::
-traverse_below(CullTraverserData &data) {
-  _nodes_pcollector.add_level(1);
-  PandaNodePipelineReader *node_reader = data.node_reader();
   PandaNode *node = data.node();
+  PandaNodePipelineReader *node_reader = data.node_reader();
+  int fancy_bits = node_reader->get_fancy_bits();
+
+  if ((fancy_bits & ~PandaNode::FB_renderable) == 0 && data._cull_planes == nullptr) {
+    // Nothing interesting in this node; just move on.
+  }
+  else {
+    // Something in this node is worth taking a closer look.
+    if (fancy_bits & PandaNode::FB_show_bounds) {
+      // If we should show the bounding volume for this node, make it up
+      // now.
+      show_bounds(data, (fancy_bits & PandaNode::FB_show_tight_bounds) != 0);
+    }
 
-  if (!data.is_this_node_hidden(_camera_mask)) {
+    data.apply_transform_and_state(this);
+
+    if (fancy_bits & PandaNode::FB_cull_callback) {
+      if (!node->cull_callback(this, data)) {
+        return;
+      }
+    }
+  }
+
+  if ((fancy_bits & PandaNode::FB_renderable) != 0 &&
+      !data.is_this_node_hidden(_camera_mask)) {
     node->add_for_draw(this, data);
 
     // Check for a decal effect.
-    const RenderEffects *node_effects = node_reader->get_effects();
-    if (node_effects->has_decal()) {
+    if (fancy_bits & PandaNode::FB_decal) {
       // If we *are* implementing decals with DepthOffsetAttribs, apply it
       // now, so that each child of this node gets offset by a tiny amount.
       data._state = data._state->compose(get_depth_offset_state());
@@ -195,22 +227,15 @@ traverse_below(CullTraverserData &data) {
     }
   }
 
+  _nodes_pcollector.add_level(1);
+
   // Now visit all the node's children.
   PandaNode::Children children = node_reader->get_children();
   node_reader->release();
   int num_children = children.get_num_children();
-  if (!node->has_selective_visibility()) {
-    for (int i = 0; i < num_children; ++i) {
-      CullTraverserData next_data(data, children.get_child(i));
-      do_traverse(next_data);
-    }
-  } else {
-    int i = node->get_first_visible_child();
-    while (i < num_children) {
-      CullTraverserData next_data(data, children.get_child(i));
-      do_traverse(next_data);
-      i = node->get_next_visible_child(i);
-    }
+  for (int i = 0; i < num_children; ++i) {
+    const PandaNode::DownConnection &child = children.get_child_connection(i);
+    traverse_down(data, child, data._state);
   }
 }
 
@@ -246,13 +271,28 @@ draw_bounding_volume(const BoundingVolume *vol,
 }
 
 /**
- * Returns true if the current node is fully or partially within the viewing
- * area and should be drawn, or false if it (and all of its children) should
- * be pruned.
+ * Implements fake-view-frustum-cull.
  */
-bool CullTraverser::
-is_in_view(CullTraverserData &data) {
-  return data.is_in_view(_camera_mask);
+void CullTraverser::
+do_fake_cull(const CullTraverserData &data, PandaNode *child,
+             const TransformState *net_transform, const RenderState *state) {
+#ifndef NDEBUG
+  // Once someone asks for this pointer, we hold its reference count and never
+  // free it.
+  static CPT(RenderState) fake_view_frustum_cull_state;
+  if (fake_view_frustum_cull_state == nullptr) {
+    fake_view_frustum_cull_state = RenderState::make
+      (ColorAttrib::make_flat(LColor(1.0f, 0.0f, 0.0f, 1.0f)),
+       TextureAttrib::make_all_off(),
+       RenderModeAttrib::make(RenderModeAttrib::M_wireframe),
+       RenderState::get_max_priority());
+  }
+
+  CullTraverserData next_data(data, child, net_transform,
+                              state->compose(fake_view_frustum_cull_state),
+                              nullptr);
+  do_traverse(next_data);
+#endif
 }
 
 /**
@@ -503,7 +543,7 @@ compute_point(const BoundingSphere *sphere,
  * Returns a RenderState for rendering the outside surfaces of the bounding
  * volume visualizations.
  */
-CPT(RenderState) CullTraverser::
+const RenderState *CullTraverser::
 get_bounds_outer_viz_state() {
   // Once someone asks for this pointer, we hold its reference count and never
   // free it.
@@ -521,7 +561,7 @@ get_bounds_outer_viz_state() {
  * Returns a RenderState for rendering the inside surfaces of the bounding
  * volume visualizations.
  */
-CPT(RenderState) CullTraverser::
+const RenderState *CullTraverser::
 get_bounds_inner_viz_state() {
   // Once someone asks for this pointer, we hold its reference count and never
   // free it.
@@ -538,7 +578,7 @@ get_bounds_inner_viz_state() {
 /**
  * Returns a RenderState for increasing the DepthOffset by one.
  */
-CPT(RenderState) CullTraverser::
+const RenderState *CullTraverser::
 get_depth_offset_state() {
   // Once someone asks for this pointer, we hold its reference count and never
   // free it.

+ 23 - 11
panda/src/pgraph/cullTraverser.h

@@ -76,22 +76,33 @@ PUBLISHED:
   INLINE PortalClipper *get_portal_clipper() const;
 
   INLINE bool get_effective_incomplete_render() const;
+  INLINE bool get_fake_view_frustum_cull() const;
+
+  INLINE static void flush_level();
 
   void traverse(const NodePath &root);
-  void traverse(CullTraverserData &data);
-  virtual void traverse_below(CullTraverserData &data);
+  void do_traverse(CullTraverserData &data);
+  INLINE void traverse_below(CullTraverserData &data);
 
   virtual void end_traverse();
 
-  INLINE static void flush_level();
-
   void draw_bounding_volume(const BoundingVolume *vol,
                             const TransformState *internal_transform) const;
 
-protected:
-  INLINE void do_traverse(CullTraverserData &data);
-
-  virtual bool is_in_view(CullTraverserData &data);
+public:
+  INLINE void traverse_down(const CullTraverserData &data, PandaNode *child);
+  INLINE void traverse_down(const CullTraverserData &data, PandaNode *child,
+                            const TransformState *net_transform,
+                            const RenderState *state);
+  INLINE void traverse_down(const CullTraverserData &data,
+                            const PandaNode::DownConnection &child);
+  INLINE void traverse_down(const CullTraverserData &data,
+                            const PandaNode::DownConnection &child,
+                            const RenderState *state);
+
+  void do_fake_cull(const CullTraverserData &data, PandaNode *child,
+                    const TransformState *net_transform,
+                    const RenderState *state);
 
 public:
   // Statistics
@@ -106,14 +117,15 @@ private:
   PT(Geom) make_tight_bounds_viz(PandaNode *node) const;
   static LVertex compute_point(const BoundingSphere *sphere,
                                PN_stdfloat latitude, PN_stdfloat longitude);
-  static CPT(RenderState) get_bounds_outer_viz_state();
-  static CPT(RenderState) get_bounds_inner_viz_state();
-  static CPT(RenderState) get_depth_offset_state();
+  static const RenderState *get_bounds_outer_viz_state();
+  static const RenderState *get_bounds_inner_viz_state();
+  static const RenderState *get_depth_offset_state();
 
   GraphicsStateGuardianBase *_gsg;
   Thread *_current_thread;
   PT(SceneSetup) _scene_setup;
   DrawMask _camera_mask;
+  bool _fake_view_frustum_cull;
   bool _has_tag_state_key;
   std::string _tag_state_key;
   CPT(RenderState) _initial_state;

+ 99 - 31
panda/src/pgraph/cullTraverserData.I

@@ -17,22 +17,18 @@
 INLINE CullTraverserData::
 CullTraverserData(const NodePath &start,
                   const TransformState *net_transform,
-                  const RenderState *state,
+                  CPT(RenderState) state,
                   GeometricBoundingVolume *view_frustum,
                   Thread *current_thread) :
   _next(nullptr),
   _start(start._head),
   _node_reader(start.node(), current_thread),
   _net_transform(net_transform),
-  _state(state),
+  _state(std::move(state)),
   _view_frustum(view_frustum),
-  _cull_planes(CullPlanes::make_empty()),
   _draw_mask(DrawMask::all_on()),
   _portal_depth(0)
 {
-  // Only update the bounding volume if we're going to end up needing it.
-  bool check_bounds = (view_frustum != nullptr);
-  _node_reader.check_cached(check_bounds);
 }
 
 /**
@@ -40,24 +36,46 @@ CullTraverserData(const NodePath &start,
  * node down in the traversal.
  */
 INLINE CullTraverserData::
-CullTraverserData(const CullTraverserData &parent, PandaNode *child) :
+CullTraverserData(const CullTraverserData &parent, PandaNode *child,
+                  const TransformState *net_transform,
+                  CPT(RenderState) state,
+                  GeometricBoundingVolume *view_frustum) :
   _next(&parent),
 #ifdef _DEBUG
   _start(nullptr),
 #endif
   _node_reader(child, parent._node_reader.get_current_thread()),
-  _net_transform(parent._net_transform),
-  _state(parent._state),
-  _view_frustum(parent._view_frustum),
-  _cull_planes(parent._cull_planes),
+  _net_transform(net_transform),
+  _state(std::move(state)),
+  _view_frustum(view_frustum),
+  _instances(parent._instances),
+  _draw_mask(parent._draw_mask),
+  _portal_depth(parent._portal_depth)
+{
+}
+
+/**
+ * This constructor creates a CullTraverserData object that reflects the next
+ * node down in the traversal.
+ */
+INLINE CullTraverserData::
+CullTraverserData(const CullTraverserData &parent,
+                  PandaNodePipelineReader &&node_reader,
+                  const TransformState *net_transform,
+                  CPT(RenderState) state,
+                  GeometricBoundingVolume *view_frustum) :
+  _next(&parent),
+#ifdef _DEBUG
+  _start(nullptr),
+#endif
+  _node_reader(std::move(node_reader)),
+  _net_transform(net_transform),
+  _state(std::move(state)),
+  _view_frustum(view_frustum),
   _instances(parent._instances),
   _draw_mask(parent._draw_mask),
   _portal_depth(parent._portal_depth)
 {
-  // Only update the bounding volume if we're going to end up needing it.
-  bool check_bounds = !_cull_planes->is_empty() ||
-                    (_view_frustum != nullptr);
-  _node_reader.check_cached(check_bounds);
 }
 
 /**
@@ -125,32 +143,82 @@ get_net_transform(const CullTraverser *) const {
 }
 
 /**
- * Returns true if the current node is within the view frustum, false
- * otherwise.  If the node's bounding volume falls completely within the view
- * frustum, this will also reset the view frustum pointer, saving some work
- * for future nodes.
+ * Returns intersection flags if the current node may be within the view
+ * frustum, 0 otherwise.
  */
-INLINE bool CullTraverserData::
-is_in_view(const DrawMask &camera_mask) {
+INLINE int CullTraverserData::
+is_in_view(const DrawMask &camera_mask) const {
+  bool check_bounds = (_view_frustum != nullptr || _cull_planes != nullptr);
+  _node_reader.check_cached(check_bounds);
+
+  if (!_node_reader.compare_draw_mask(_draw_mask, camera_mask)) {
+    // If there are no draw bits in common with the camera, the node is out.
+    return BoundingVolume::IF_no_intersection;
+  }
+
   if (_node_reader.get_transform()->is_invalid()) {
     // If the transform is invalid, forget it.
-    return false;
+    return BoundingVolume::IF_no_intersection;
   }
 
-  if (!_node_reader.compare_draw_mask(_draw_mask, camera_mask)) {
+  if (_view_frustum != nullptr) {
+    const GeometricBoundingVolume *node_gbv = _node_reader.get_bounds()->as_geometric_bounding_volume();
+    nassertr(node_gbv != nullptr, BoundingVolume::IF_no_intersection);
+
+    return _view_frustum->contains(node_gbv);
+  }
+
+  return BoundingVolume::IF_possible;
+}
+
+/**
+ * Returns intersection flags if the given child of the current node may be
+ * within the view frustum, 0 otherwise.
+ */
+INLINE int CullTraverserData::
+is_child_in_view(const PandaNodePipelineReader &node_reader, const DrawMask &camera_mask) const {
+  bool check_bounds = (_view_frustum != nullptr || _cull_planes != nullptr);
+  node_reader.check_cached(check_bounds);
+
+  if (!node_reader.compare_draw_mask(_draw_mask, camera_mask)) {
     // If there are no draw bits in common with the camera, the node is out.
-    return false;
+    return BoundingVolume::IF_no_intersection;
   }
 
-  if (_view_frustum == nullptr &&
-      _cull_planes->is_empty()) {
-    // If the transform is valid, but we don't have a frustum or any clip
-    // planes or occluders, it's always in.
-    return true;
+  if (_node_reader.get_transform()->is_invalid()) {
+    // If the transform is invalid, forget it.
+    return BoundingVolume::IF_no_intersection;
   }
 
-  // Otherwise, compare the bounding volume to the frustum.
-  return is_in_view_impl();
+  if (_view_frustum != nullptr) {
+    const GeometricBoundingVolume *node_gbv = node_reader.get_bounds()->as_geometric_bounding_volume();
+    nassertr(node_gbv != nullptr, BoundingVolume::IF_no_intersection);
+
+    return _view_frustum->contains(node_gbv);
+  }
+  return BoundingVolume::IF_possible;
+}
+
+/**
+ * Returns intersection flags if the given child of the current node may be
+ * within the view frustum, 0 otherwise.
+ */
+INLINE int CullTraverserData::
+is_child_in_view(const PandaNode::DownConnection &child, const DrawMask &camera_mask) const {
+  // No need to call check_cached here, since we presumably already called it
+  // on the parent.
+  if (!child.compare_draw_mask(_draw_mask, camera_mask)) {
+    // If there are no draw bits in common with the camera, the node is out.
+    return BoundingVolume::IF_no_intersection;
+  }
+
+  if (_view_frustum != nullptr) {
+    const GeometricBoundingVolume *node_gbv = child.get_bounds();
+    nassertr(node_gbv != nullptr, BoundingVolume::IF_no_intersection);
+
+    return _view_frustum->contains(node_gbv);
+  }
+  return BoundingVolume::IF_possible;
 }
 
 /**

+ 104 - 124
panda/src/pgraph/cullTraverserData.cxx

@@ -15,9 +15,6 @@
 #include "cullTraverser.h"
 #include "config_pgraph.h"
 #include "pandaNode.h"
-#include "colorAttrib.h"
-#include "textureAttrib.h"
-#include "renderModeAttrib.h"
 #include "clipPlaneAttrib.h"
 #include "boundingPlane.h"
 #include "billboardEffect.h"
@@ -64,10 +61,28 @@ apply_transform_and_state(CullTraverser *trav) {
   }
 
   if (clip_plane_cull) {
-    _cull_planes = _cull_planes->apply_state(trav, this,
-      (const ClipPlaneAttrib *)node_state->get_attrib(ClipPlaneAttrib::get_class_slot()),
-      (const ClipPlaneAttrib *)_node_reader.get_off_clip_planes(),
-      (const OccluderEffect *)node_effects->get_effect(OccluderEffect::get_class_type()));
+    const ClipPlaneAttrib *cpa = (const ClipPlaneAttrib *)
+      node_state->get_attrib(ClipPlaneAttrib::get_class_slot());
+    const OccluderEffect *occluders = (const OccluderEffect *)
+      node_effects->get_effect(OccluderEffect::get_class_type());
+
+    if (cpa != nullptr || occluders != nullptr) {
+      CullPlanes::apply_state(_cull_planes, trav, this, cpa,
+        (const ClipPlaneAttrib *)_node_reader.get_off_clip_planes(),
+        occluders);
+    }
+  }
+
+  const FogAttrib *fog_attr;
+  if (node_state->get_attrib(fog_attr)) {
+    Fog *fog = fog_attr->get_fog();
+    if (fog != nullptr) {
+      // If we just introduced a FogAttrib here, call adjust_to_camera()
+      // now.  This maybe isn't the perfect time to call it, but it's good
+      // enough; and at this time we have all the information we need for
+      // it.
+      fog->adjust_to_camera(trav->get_camera_transform());
+    }
   }
 }
 
@@ -88,33 +103,88 @@ apply_transform(const TransformState *node_transform) {
 
     _net_transform = _net_transform->compose(node_transform);
 
-    if ((_view_frustum != nullptr) ||
-        (!_cull_planes->is_empty())) {
+    if (_view_frustum != nullptr || _cull_planes != nullptr) {
       // We need to move the viewing frustums into the node's coordinate space
       // by applying the node's inverse transform.
-      if (node_transform->is_singular()) {
-        // But we can't invert a singular transform!  Instead of trying, we'll
-        // just give up on frustum culling from this point down.
-        _view_frustum = nullptr;
-        _cull_planes = CullPlanes::make_empty();
-
-      } else {
-        CPT(TransformState) inv_transform =
-          node_transform->invert_compose(TransformState::make_identity());
-
+      const LMatrix4 *inverse_mat = node_transform->get_inverse_mat();
+      if (inverse_mat != nullptr) {
         // Copy the bounding volumes for the frustums so we can transform
         // them.
         if (_view_frustum != nullptr) {
           _view_frustum = _view_frustum->make_copy()->as_geometric_bounding_volume();
           nassertv(_view_frustum != nullptr);
 
-          _view_frustum->xform(inv_transform->get_mat());
+          _view_frustum->xform(*inverse_mat);
         }
 
-        _cull_planes = _cull_planes->xform(inv_transform->get_mat());
+        if (_cull_planes != nullptr) {
+          _cull_planes = _cull_planes->xform(*inverse_mat);
+        }
+      }
+      else {
+        // But we can't invert a singular transform!  Instead of trying, we'll
+        // just give up on frustum culling from this point down.
+        pgraph_cat.warning()
+          << "Singular transformation detected on node: " << get_node_path() << "\n";
+        _view_frustum = nullptr;
+        _cull_planes = nullptr;
+      }
+    }
+  }
+}
+
+/**
+ * Returns intersection flags if any of the children under the current node are
+ * in view if first transformed by the given transform, false otherwise.
+ */
+bool CullTraverserData::
+is_instance_in_view(const TransformState *instance_transform, const DrawMask &camera_mask) const {
+  PT(GeometricBoundingVolume) view_frustum_p;
+  const GeometricBoundingVolume *view_frustum = nullptr;
+
+  if (_view_frustum != nullptr) {
+    if (!instance_transform->is_identity()) {
+      // We need to move the viewing frustums into the node's coordinate space
+      // by applying the node's inverse transform.
+      const LMatrix4 *inverse_mat = instance_transform->get_inverse_mat();
+      if (inverse_mat != nullptr) {
+        // Copy the bounding volumes for the frustums so we can transform them.
+        view_frustum_p = _view_frustum->make_copy()->as_geometric_bounding_volume();
+        nassertr(view_frustum_p != nullptr, false);
+
+        view_frustum_p->xform(*inverse_mat);
+        view_frustum = view_frustum_p;
+      } else {
+        // Don't render instances with a singular transformation.
+        return false;
       }
+    } else {
+      view_frustum = _view_frustum;
     }
   }
+
+  PandaNode::Children children = _node_reader.get_children();
+  int num_children = children.get_num_children();
+  for (int i = 0; i < num_children; ++i) {
+    const PandaNode::DownConnection &child = children.get_child_connection(i);
+
+    if (!child.compare_draw_mask(_draw_mask, camera_mask)) {
+      // If there are no draw bits in common with the camera, the node is out.
+      continue;
+    }
+
+    if (view_frustum == nullptr) {
+      return true;
+    }
+
+    const GeometricBoundingVolume *node_gbv = child.get_bounds();
+    nassertd(node_gbv != nullptr) continue;
+
+    if (view_frustum->contains(node_gbv)) {
+      return true;
+    }
+  }
+  return false;
 }
 
 /**
@@ -152,127 +222,37 @@ r_get_node_path() const {
 }
 
 /**
- * The private implementation of is_in_view().
+ * Applies the cull planes.  Returns true if the node should still be rendered,
+ * false if it should be culled.
  */
 bool CullTraverserData::
-is_in_view_impl() {
-  const GeometricBoundingVolume *node_gbv = nullptr;
-
-  if (_view_frustum != nullptr) {
-    node_gbv = _node_reader.get_bounds()->as_geometric_bounding_volume();
-    nassertr(node_gbv != nullptr, false);
-
-    int result = _view_frustum->contains(node_gbv);
-
-    if (pgraph_cat.is_spam()) {
-      pgraph_cat.spam()
-        << get_node_path() << " cull result = " << std::hex << result << std::dec << "\n";
-    }
-
-    if (result == BoundingVolume::IF_no_intersection) {
-      // No intersection at all.  Cull.
-#ifdef NDEBUG
-      return false;
-#else
-      if (!fake_view_frustum_cull) {
-        return false;
-      }
-
-      // If we have fake view-frustum culling enabled, instead of actually
-      // culling an object we simply force it to be drawn in red wireframe.
-      _view_frustum = nullptr;
-      _state = _state->compose(get_fake_view_frustum_cull_state());
-#endif
-
-    } else if ((result & BoundingVolume::IF_all) != 0) {
-      // The node and its descendents are completely enclosed within the
-      // frustum.  No need to cull further.
-      _view_frustum = nullptr;
-
-    } else {
-      // The node is partially, but not completely, within the viewing
-      // frustum.
-      if (_node_reader.is_final()) {
-        // Normally we'd keep testing child bounding volumes as we continue
-        // down.  But this node has the "final" flag, so the user is claiming
-        // that there is some important reason we should consider everything
-        // visible at this point.  So be it.
-        _view_frustum = nullptr;
-      }
-    }
-  }
-
-  if (!_cull_planes->is_empty()) {
-    if (node_gbv == nullptr) {
-      node_gbv = _node_reader.get_bounds()->as_geometric_bounding_volume();
-      nassertr(node_gbv != nullptr, false);
-    }
-
+apply_cull_planes(const CullPlanes *planes, const GeometricBoundingVolume *node_gbv) {
+  if (!planes->is_empty()) {
     // Also cull against the current clip planes.
     int result;
-    _cull_planes = _cull_planes->do_cull(result, _state, node_gbv);
+    CPT(CullPlanes) new_planes = planes->do_cull(result, _state, node_gbv);
 
     if (pgraph_cat.is_spam()) {
       pgraph_cat.spam()
         << get_node_path() << " cull planes cull result = " << std::hex
         << result << std::dec << "\n";
-      _cull_planes->write(pgraph_cat.spam(false));
-    }
-
-    if (_node_reader.is_final()) {
-      // Even though the node may be partially within the clip planes, do no
-      // more culling against them below this node.
-      _cull_planes = CullPlanes::make_empty();
-
-      if (pgraph_cat.is_spam()) {
-        pgraph_cat.spam()
-          << get_node_path() << " is_final, cull planes disabled, state:\n";
-        _state->write(pgraph_cat.spam(false), 2);
-      }
+      new_planes->write(pgraph_cat.spam(false));
     }
 
     if (result == BoundingVolume::IF_no_intersection) {
       // No intersection at all.  Cull.
-#ifdef NDEBUG
       return false;
-#else
-      if (!fake_view_frustum_cull) {
-        return false;
-      }
-      _cull_planes = CullPlanes::make_empty();
-      _state = _state->compose(get_fake_view_frustum_cull_state());
-#endif
-
-    } else if ((result & BoundingVolume::IF_all) != 0) {
-      // The node and its descendents are completely in front of all of the
+    }
+    else if ((result & BoundingVolume::IF_all) != 0) {
+      // The node and its descendants are completely in front of all of the
       // clip planes and occluders.  The do_cull() call should therefore have
       // removed all of the clip planes and occluders.
-      nassertr(_cull_planes->is_empty(), true);
+      nassertr(new_planes->is_empty(), true);
+    }
+    else if (!_node_reader.is_final() && !new_planes->is_empty()) {
+      _cull_planes = std::move(new_planes);
     }
   }
 
   return true;
 }
-
-/**
- * Returns a RenderState for rendering stuff in red wireframe, strictly for
- * the fake_view_frustum_cull effect.
- */
-const RenderState *CullTraverserData::
-get_fake_view_frustum_cull_state() {
-#ifdef NDEBUG
-  return nullptr;
-#else
-  // Once someone asks for this pointer, we hold its reference count and never
-  // free it.
-  static CPT(RenderState) state;
-  if (state == nullptr) {
-    state = RenderState::make
-      (ColorAttrib::make_flat(LColor(1.0f, 0.0f, 0.0f, 1.0f)),
-       TextureAttrib::make_all_off(),
-       RenderModeAttrib::make(RenderModeAttrib::M_wireframe),
-       RenderState::get_max_priority());
-  }
-  return state;
-#endif
-}

+ 18 - 6
panda/src/pgraph/cullTraverserData.h

@@ -42,11 +42,19 @@ class EXPCL_PANDA_PGRAPH CullTraverserData {
 public:
   INLINE CullTraverserData(const NodePath &start,
                            const TransformState *net_transform,
-                           const RenderState *state,
+                           CPT(RenderState) state,
                            GeometricBoundingVolume *view_frustum,
                            Thread *current_thread);
   INLINE CullTraverserData(const CullTraverserData &parent,
-                           PandaNode *child);
+                           PandaNode *child,
+                           const TransformState *net_transform,
+                           CPT(RenderState) state,
+                           GeometricBoundingVolume *view_frustum);
+  INLINE CullTraverserData(const CullTraverserData &parent,
+                           PandaNodePipelineReader &&node_reader,
+                           const TransformState *net_transform,
+                           CPT(RenderState) state,
+                           GeometricBoundingVolume *view_frustum);
 
 PUBLISHED:
   INLINE PandaNode *node() const;
@@ -62,14 +70,21 @@ PUBLISHED:
   INLINE CPT(TransformState) get_internal_transform(const CullTraverser *trav) const;
   INLINE const TransformState *get_net_transform(const CullTraverser *trav) const;
 
-  INLINE bool is_in_view(const DrawMask &camera_mask);
+  INLINE int is_in_view(const DrawMask &camera_mask) const;
   INLINE bool is_this_node_hidden(const DrawMask &camera_mask) const;
 
+  bool apply_cull_planes(const CullPlanes *planes, const GeometricBoundingVolume *node_gbv);
+
   void apply_transform_and_state(CullTraverser *trav);
   void apply_transform(const TransformState *node_transform);
 
   MAKE_PROPERTY(node_path, get_node_path);
 
+public:
+  bool is_instance_in_view(const TransformState *instance_transform, const DrawMask &camera_mask) const;
+  INLINE int is_child_in_view(const PandaNodePipelineReader &node_reader, const DrawMask &camera_mask) const;
+  INLINE int is_child_in_view(const PandaNode::DownConnection &child, const DrawMask &camera_mask) const;
+
 private:
   // We store a chain leading all the way to the root, so that we can compose
   // a NodePath.  We may be able to eliminate this requirement in the future.
@@ -88,9 +103,6 @@ public:
 
 private:
   PT(NodePathComponent) r_get_node_path() const;
-
-  bool is_in_view_impl();
-  static const RenderState *get_fake_view_frustum_cull_state();
 };
 
 /* okcircular */

+ 3 - 12
panda/src/pgraph/geomNode.cxx

@@ -59,6 +59,8 @@ GeomNode(const std::string &name) :
 
   // GeomNodes have a certain set of bits on by default.
   set_into_collide_mask(get_default_collide_mask());
+
+  set_renderable();
 }
 
 /**
@@ -490,17 +492,6 @@ calc_tight_bounds(LPoint3 &min_point, LPoint3 &max_point, bool &found_any,
   return next_transform;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool GeomNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes
@@ -569,7 +560,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) {
         // Cull this Geom.
         continue;
       }
-      if (!data._cull_planes->is_empty()) {
+      if (data._cull_planes != nullptr) {
         // Also cull the Geom against the cull planes.
         CPT(BoundingVolume) geom_volume = geom->get_bounds(current_thread);
         const GeometricBoundingVolume *geom_gbv =

+ 0 - 1
panda/src/pgraph/geomNode.h

@@ -50,7 +50,6 @@ public:
                       bool &found_any,
                       const TransformState *transform,
                       Thread *current_thread) const;
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
   virtual CollideMask get_legal_collide_mask() const;
 

+ 15 - 27
panda/src/pgraph/instancedNode.cxx

@@ -218,32 +218,29 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     instances = new_list;
   }
 
-  if (data._view_frustum != nullptr || !data._cull_planes->is_empty()) {
-    // Culling is on, so we need to figure out which instances are visible.
-    Children children = data.node_reader()->get_children();
-    data.node_reader()->release();
-
-    // Keep track of which instances should be culled away.
+  if (data._view_frustum != nullptr || data._cull_planes != nullptr) {
+    // Culling is on, so we need to figure out which instances should be culled.
     BitArray culled_instances;
     culled_instances.set_range(0, instances->size());
 
     for (size_t ii = 0; ii < instances->size(); ++ii) {
-      CullTraverserData instance_data(data);
-      instance_data.apply_transform((*instances)[ii].get_transform());
-
-      for (size_t ci = 0; ci < children.size(); ++ci) {
-        CullTraverserData child_data(instance_data, children.get_child(ci));
-        if (child_data.is_in_view(trav->get_camera_mask())) {
-          // Yep, the instance is in view.
-          culled_instances.clear_bit(ii);
-          break;
-        }
+      if (data.is_instance_in_view((*instances)[ii].get_transform(), trav->get_camera_mask())) {
+        culled_instances.clear_bit(ii);
+      }
+    }
+
+    if (!culled_instances.is_zero() && trav->get_fake_view_frustum_cull()) {
+      // The culled instances are drawn with the fake-view-frustum-cull effect.
+      data._instances = instances->without(culled_instances ^ BitArray::range(0, instances->size()));
+
+      Children children = data.node_reader()->get_children();
+      int num_children = children.get_num_children();
+      for (int i = 0; i < num_children; ++i) {
+        trav->do_fake_cull(data, children.get_child(i), data._net_transform, data._state);
       }
     }
 
     instances = instances->without(culled_instances);
-  } else {
-    data.node_reader()->release();
   }
 
   if (instances->empty()) {
@@ -259,15 +256,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   data._cull_planes = CullPlanes::make_empty();
 
   return true;
-
-  /*
-  for (const InstanceList::Instance &instance : *instances) {
-    CullTraverserData instance_data(data);
-    instance_data.apply_transform(instance.get_transform());
-    trav->traverse_below(instance_data);
-  }
-  return false;
-  */
 }
 
 /**

+ 1 - 12
panda/src/pgraph/occluderNode.cxx

@@ -55,6 +55,7 @@ OccluderNode(const std::string &name) :
   PandaNode(name)
 {
   set_cull_callback();
+  set_renderable();
   // OccluderNodes are hidden by default.
   set_overall_hidden(true);
   set_double_sided(false);
@@ -156,18 +157,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool OccluderNode::
-is_renderable() const {
-  return true;
-}
-
-
 /**
  * Writes a brief description of the node to the indicated output stream.
  * This is invoked by the << operator.  It may be overridden in derived

+ 0 - 1
panda/src/pgraph/occluderNode.h

@@ -42,7 +42,6 @@ public:
   virtual void xform(const LMatrix4 &mat);
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
   virtual void output(std::ostream &out) const;
 

+ 65 - 0
panda/src/pgraph/pandaNode.I

@@ -773,6 +773,59 @@ get_sort() const {
   return _sort;
 }
 
+/**
+ * Returns the net collide mask of this child.  Requires that update_cached()
+ * has been called on the parent.
+ */
+INLINE CollideMask PandaNode::DownConnection::
+get_net_collide_mask() const {
+  return _net_collide_mask;
+}
+
+/**
+ * Returns the external bounding volume of this child.  Requires that
+ * update_cached() has been called on the parent.
+ */
+INLINE const GeometricBoundingVolume *PandaNode::DownConnection::
+get_bounds() const {
+  return _external_bounds;
+}
+
+/**
+ * Compares the running draw mask computed during a traversal with this child's
+ * net draw masks.  Returns true if the child should be traversed into, or
+ * false if there is nothing at this level or below that will be visible to
+ * the indicated camera_mask.
+ */
+INLINE bool PandaNode::DownConnection::
+compare_draw_mask(DrawMask running_draw_mask, DrawMask camera_mask) const {
+  // As a special case, if net_draw_show_mask is all 0, it means either that
+  // all nodes under this node are hidden to all cameras, or that none of them
+  // are renderable nodes (or some combination). In either case, we might as
+  // well short-circuit.
+  if (_net_draw_show_mask.is_zero()) {
+    return false;
+  }
+
+  DrawMask net_draw_control_mask, net_draw_show_mask;
+  net_draw_control_mask = _net_draw_control_mask;
+  net_draw_show_mask = _net_draw_show_mask;
+
+  // Now the bits that are not in net_draw_control_mask--that is, those bits
+  // that are not changed by any of the nodes at this level and below--are
+  // taken from running_draw_mask, which is inherited from above.  On the
+  // other hand, the bits that *are* in net_draw_control_mask--those bits that
+  // are changed by any of the nodes at this level and below--are taken from
+  // net_draw_show_mask, which is propagated upwards from below.
+
+  // This way, we will traverse into this node if it has any children which
+  // want to be visited by the traversal, but we will avoid traversing into it
+  // if all of its children are hidden to this camera.
+  DrawMask compare_mask = (running_draw_mask & ~net_draw_control_mask) | (net_draw_show_mask & net_draw_control_mask);
+
+  return !((compare_mask & PandaNode::_overall_bit).is_zero()) && !((compare_mask & camera_mask).is_zero());
+}
+
 /**
  *
  */
@@ -1351,6 +1404,18 @@ get_child(int n) const {
   return (*down)[n].get_child();
 }
 
+/**
+ * Returns the nth child node of this node.  See get_num_children().  Also see
+ * get_children(), if your intention is to iterate through the complete list
+ * of children; get_children() is preferable in this case.
+ */
+INLINE const PandaNode::DownConnection &PandaNodePipelineReader::
+get_child_connection(int n) const {
+  CPT(PandaNode::Down) down = _cdata->get_down();
+  //nassertr(n >= 0 && n < (int)down->size(), nullptr);
+  return (*down)[n];
+}
+
 /**
  * Returns the sort index of the nth child node of this node (that is, the
  * number that was passed to add_child()).  See get_num_children().

+ 75 - 51
panda/src/pgraph/pandaNode.cxx

@@ -27,6 +27,8 @@
 #include "config_mathutil.h"
 #include "lightReMutexHolder.h"
 #include "graphicsStateGuardianBase.h"
+#include "decalEffect.h"
+#include "showBoundsEffect.h"
 
 using std::ostream;
 using std::ostringstream;
@@ -387,46 +389,6 @@ cull_callback(CullTraverser *, CullTraverserData &) {
   return true;
 }
 
-/**
- * Should be overridden by derived classes to return true if this kind of node
- * has some restrictions on the set of children that should be rendered.  Node
- * with this property include LODNodes, SwitchNodes, and SequenceNodes.
- *
- * If this function returns true, get_first_visible_child() and
- * get_next_visible_child() will be called to walk through the list of
- * children during cull, instead of iterating through the entire list.  This
- * method is called after cull_callback(), so cull_callback() may be
- * responsible for the decisions as to which children are visible at the
- * moment.
- */
-bool PandaNode::
-has_selective_visibility() const {
-  return false;
-}
-
-/**
- * Returns the index number of the first visible child of this node, or a
- * number >= get_num_children() if there are no visible children of this node.
- * This is called during the cull traversal, but only if
- * has_selective_visibility() has already returned true.  See
- * has_selective_visibility().
- */
-int PandaNode::
-get_first_visible_child() const {
-  return 0;
-}
-
-/**
- * Returns the index number of the next visible child of this node following
- * the indicated child, or a number >= get_num_children() if there are no more
- * visible children of this node.  See has_selective_visibility() and
- * get_first_visible_child().
- */
-int PandaNode::
-get_next_visible_child(int n) const {
-  return n + 1;
-}
-
 /**
  * Should be overridden by derived classes to return true if this kind of node
  * has the special property that just one of its children is visible at any
@@ -460,7 +422,8 @@ get_visible_child() const {
  */
 bool PandaNode::
 is_renderable() const {
-  return false;
+  CDReader cdata(_cycler);
+  return (cdata->_fancy_bits & FB_renderable) != 0;
 }
 
 /**
@@ -1003,7 +966,15 @@ set_effect(const RenderEffect *effect) {
   OPEN_ITERATE_CURRENT_AND_UPSTREAM(_cycler, current_thread) {
     CDStageWriter cdata(_cycler, pipeline_stage, current_thread);
     cdata->_effects = cdata->_effects->add_effect(effect);
+
     cdata->set_fancy_bit(FB_effects, true);
+    if (effect->get_type() == DecalEffect::get_class_type()) {
+      cdata->set_fancy_bit(FB_decal, true);
+    }
+    else if (effect->get_type() == ShowBoundsEffect::get_class_type()) {
+      cdata->set_fancy_bit(FB_show_bounds, true);
+      cdata->set_fancy_bit(FB_show_tight_bounds, ((const ShowBoundsEffect *)effect)->get_tight());
+    }
   }
   CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler);
   mark_bam_modified();
@@ -1018,7 +989,14 @@ clear_effect(TypeHandle type) {
   OPEN_ITERATE_CURRENT_AND_UPSTREAM(_cycler, current_thread) {
     CDStageWriter cdata(_cycler, pipeline_stage, current_thread);
     cdata->_effects = cdata->_effects->remove_effect(type);
+
     cdata->set_fancy_bit(FB_effects, !cdata->_effects->is_empty());
+    if (type == DecalEffect::get_class_type()) {
+      cdata->set_fancy_bit(FB_decal, false);
+    }
+    else if (type == ShowBoundsEffect::get_class_type()) {
+      cdata->set_fancy_bit(FB_show_bounds | FB_show_tight_bounds, false);
+    }
   }
   CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler);
   mark_bam_modified();
@@ -1067,6 +1045,9 @@ set_effects(const RenderEffects *effects, Thread *current_thread) {
     CDStageWriter cdata(_cycler, pipeline_stage, current_thread);
     cdata->_effects = effects;
     cdata->set_fancy_bit(FB_effects, !effects->is_empty());
+    cdata->set_fancy_bit(FB_decal, effects->has_decal());
+    cdata->set_fancy_bit(FB_show_bounds, effects->has_show_bounds());
+    cdata->set_fancy_bit(FB_show_tight_bounds, effects->has_show_tight_bounds());
   }
   CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler);
   mark_bam_modified();
@@ -1402,7 +1383,8 @@ copy_all_properties(PandaNode *other) {
     }
 
     static const int change_bits = (FB_transform | FB_state | FB_effects |
-                                    FB_tag | FB_draw_mask);
+                                    FB_tag | FB_draw_mask | FB_decal |
+                                    FB_show_bounds | FB_show_tight_bounds);
     cdataw->_fancy_bits =
       (cdataw->_fancy_bits & ~change_bits) |
       (cdatar->_fancy_bits & change_bits);
@@ -2536,6 +2518,24 @@ disable_cull_callback() {
   mark_bam_modified();
 }
 
+/**
+ * Called by a derived class to indicate that there is some value to visiting
+ * this particular node during the cull traversal for any camera.  This will be
+ * used to optimize the result of get_net_draw_show_mask(), so that any subtrees
+ * that contain only nodes for which is_renderable() is false need not be
+ * visited.  It also indicates that add_for_draw() should be called if the
+ * object is determined to be in view of the camera.
+ */
+void PandaNode::
+set_renderable() {
+  Thread *current_thread = Thread::get_current_thread();
+  OPEN_ITERATE_CURRENT_AND_UPSTREAM(_cycler, current_thread) {
+    CDStageWriter cdata(_cycler, pipeline_stage, current_thread);
+    cdata->set_fancy_bit(FB_renderable, true);
+  }
+  CLOSE_ITERATE_CURRENT_AND_UPSTREAM(_cycler);
+}
+
 /**
  * The private implementation of remove_child(), for a particular pipeline
  * stage.
@@ -3272,7 +3272,7 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
     // Start with a clean slate.
     CollideMask net_collide_mask = cdata->_into_collide_mask;
     DrawMask net_draw_control_mask, net_draw_show_mask;
-    bool renderable = is_renderable();
+    bool renderable = (cdata->_fancy_bits & FB_renderable) != 0;
 
     if (renderable) {
       // If this node is itself renderable, it contributes to the net draw
@@ -3292,16 +3292,22 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
       off_clip_planes = ClipPlaneAttrib::make();
     }
 
-    // Also get the list of the node's children.
-    Children children(cdata);
-
     int num_vertices = cdata->_internal_vertices;
 
+    // Also get the list of the node's children.  When the cdataw destructs, it
+    // will also release the lock, since we've got all the data we need from the
+    // node.
+    PT(Down) down;
+    {
+      CDStageWriter cdataw(_cycler, pipeline_stage, cdata);
+      down = cdataw->modify_down();
+    }
+
     // Now that we've got all the data we need from the node, we can release
     // the lock.
-    _cycler.release_read_stage(pipeline_stage, cdata.take_pointer());
+    //_cycler.release_read_stage(pipeline_stage, cdata.take_pointer());
 
-    int num_children = children.get_num_children();
+    int num_children = down->size();
 
     // We need to keep references to the bounding volumes, since in a threaded
     // environment the pointers might go away while we're working (since we're
@@ -3334,7 +3340,8 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
     // Now expand those contents to include all of our children.
 
     for (int i = 0; i < num_children; ++i) {
-      PandaNode *child = children.get_child(i);
+      DownConnection &connection = (*down)[i];
+      PandaNode *child = connection.get_child();
 
       const ClipPlaneAttrib *orig_cp = DCAST(ClipPlaneAttrib, off_clip_planes);
 
@@ -3348,7 +3355,9 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
         // Child needs update.
         CDStageWriter child_cdataw = child->update_cached(update_bounds, pipeline_stage, child_cdata);
 
-        net_collide_mask |= child_cdataw->_net_collide_mask;
+        CollideMask child_collide_mask = child_cdataw->_net_collide_mask;
+        net_collide_mask |= child_collide_mask;
+        connection._net_collide_mask = child_collide_mask;
 
         if (drawmask_cat.is_debug()) {
           drawmask_cat.debug(false)
@@ -3425,11 +3434,18 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
             child_volumes[child_volumes_i++] = child_cdataw->_external_bounds;
           }
           num_vertices += child_cdataw->_nested_vertices;
+
+          connection._external_bounds = child_cdataw->_external_bounds->as_geometric_bounding_volume();
         }
 
+        connection._net_draw_control_mask = child_control_mask;
+        connection._net_draw_show_mask = child_show_mask;
+
       } else {
         // Child is good.
-        net_collide_mask |= child_cdata->_net_collide_mask;
+        CollideMask child_collide_mask = child_cdata->_net_collide_mask;
+        net_collide_mask |= child_collide_mask;
+        connection._net_collide_mask = child_collide_mask;
 
         // See comments in similar block above.
         if (drawmask_cat.is_debug()) {
@@ -3477,7 +3493,12 @@ update_cached(bool update_bounds, int pipeline_stage, PandaNode::CDLockedStageRe
             child_volumes[child_volumes_i++] = child_cdata->_external_bounds;
           }
           num_vertices += child_cdata->_nested_vertices;
+
+          connection._external_bounds = child_cdata->_external_bounds->as_geometric_bounding_volume();
         }
+
+        connection._net_draw_control_mask = child_control_mask;
+        connection._net_draw_show_mask = child_show_mask;
       }
     }
 
@@ -3865,6 +3886,9 @@ complete_pointers(TypedWritable **p_list, BamReader *manager) {
   set_fancy_bit(FB_state, !_state->is_empty());
   set_fancy_bit(FB_effects, !_effects->is_empty());
   set_fancy_bit(FB_tag, !_tag_data.is_empty());
+  set_fancy_bit(FB_decal, _effects->has_decal());
+  set_fancy_bit(FB_show_bounds, _effects->has_show_bounds());
+  set_fancy_bit(FB_show_tight_bounds, _effects->has_show_tight_bounds());
 
   // Mark the bounds stale.
   ++_next_update;

+ 28 - 4
panda/src/pgraph/pandaNode.h

@@ -46,6 +46,7 @@
 #include "lightReMutex.h"
 #include "extension.h"
 #include "simpleHashMap.h"
+#include "geometricBoundingVolume.h"
 
 class NodePathComponent;
 class CullTraverser;
@@ -97,12 +98,9 @@ public:
                       Thread *current_thread = Thread::get_current_thread()) const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool has_selective_visibility() const;
-  virtual int get_first_visible_child() const;
-  virtual int get_next_visible_child(int n) const;
   virtual bool has_single_child_visibility() const;
   virtual int get_visible_child() const;
-  virtual bool is_renderable() const;
+  virtual bool is_renderable() const final; //CHANGED: see set_renderable()
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
 PUBLISHED:
@@ -326,6 +324,10 @@ PUBLISHED:
     FB_tag                  = 0x0010,
     FB_draw_mask            = 0x0020,
     FB_cull_callback        = 0x0040,
+    FB_renderable           = 0x0080,
+    FB_decal                = 0x0100,
+    FB_show_bounds          = 0x0200,
+    FB_show_tight_bounds    = 0x0400,
   };
   INLINE int get_fancy_bits(Thread *current_thread = Thread::get_current_thread()) const;
 
@@ -371,6 +373,8 @@ protected:
 
   void set_cull_callback();
   void disable_cull_callback();
+  void set_renderable();
+
 public:
   virtual void r_prepare_scene(GraphicsStateGuardianBase *gsg,
                                const RenderState *node_state,
@@ -459,11 +463,26 @@ public:
     INLINE void set_child(PandaNode *child);
     INLINE int get_sort() const;
 
+    INLINE CollideMask get_net_collide_mask() const;
+    INLINE const GeometricBoundingVolume *get_bounds() const;
+
+    INLINE bool compare_draw_mask(DrawMask running_draw_mask,
+                                  DrawMask camera_mask) const;
+
   private:
     // Child pointers are reference counted.  That way, holding a pointer to
     // the root of a subgraph keeps the entire subgraph around.
     PT(PandaNode) _child;
     int _sort;
+
+    // These values are cached here so that a traverser can efficiently check
+    // whether a node is in view before recursing.
+    CollideMask _net_collide_mask;
+    CPT(GeometricBoundingVolume) _external_bounds;
+    DrawMask _net_draw_control_mask;
+    DrawMask _net_draw_show_mask;
+
+    friend class PandaNode;
   };
 
 private:
@@ -718,6 +737,10 @@ PUBLISHED:
     INLINE PandaNode *get_child(size_t n) const;
     INLINE int get_child_sort(size_t n) const;
 
+    INLINE const DownConnection &get_child_connection(size_t n) const {
+      return (*_down)[n];
+    }
+
   PUBLISHED:
     INLINE PandaNode *operator [](size_t n) const { return get_child(n); }
     INLINE size_t size() const { return get_num_children(); }
@@ -865,6 +888,7 @@ public:
 
   INLINE int get_num_children() const;
   INLINE PandaNode *get_child(int n) const;
+  INLINE const PandaNode::DownConnection &get_child_connection(int n) const;
   INLINE int get_child_sort(int n) const;
   INLINE int find_child(PandaNode *node) const;
 

+ 1 - 11
panda/src/pgraph/planeNode.cxx

@@ -65,6 +65,7 @@ PlaneNode(const std::string &name, const LPlane &plane) :
   _clip_effect(~0)
 {
   set_cull_callback();
+  set_renderable();
 
   // PlaneNodes are hidden by default.
   set_overall_hidden(true);
@@ -148,17 +149,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool PlaneNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Returns a newly-allocated BoundingVolume that represents the internal
  * contents of the node.  Should be overridden by PandaNode classes that

+ 0 - 1
panda/src/pgraph/planeNode.h

@@ -46,7 +46,6 @@ public:
   virtual void xform(const LMatrix4 &mat);
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
 PUBLISHED:
   INLINE void set_plane(const LPlane &plane);

+ 1 - 12
panda/src/pgraph/portalNode.cxx

@@ -48,6 +48,7 @@ PortalNode(const std::string &name) :
   _flags(0)
 {
   set_cull_callback();
+  set_renderable();
 
   _visible = false;
   _open = true;
@@ -307,18 +308,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool PortalNode::
-is_renderable() const {
-  return true;
-}
-
-
 /**
  * Writes a brief description of the node to the indicated output stream.
  * This is invoked by the << operator.  It may be overridden in derived

+ 0 - 1
panda/src/pgraph/portalNode.h

@@ -45,7 +45,6 @@ public:
   virtual void enable_clipping_planes();
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
   virtual void output(std::ostream &out) const;
 

+ 1 - 1
panda/src/pgraph/showBoundsEffect.h

@@ -25,7 +25,7 @@ class FactoryParams;
  * this node.  This is generally used only during development to help identify
  * bounding volume issues.
  */
-class EXPCL_PANDA_PGRAPH ShowBoundsEffect : public RenderEffect {
+class EXPCL_PANDA_PGRAPH ShowBoundsEffect final : public RenderEffect {
 private:
   INLINE ShowBoundsEffect();
 

+ 14 - 0
panda/src/pgraph/transformState.I

@@ -472,6 +472,20 @@ get_mat() const {
   return _mat;
 }
 
+/**
+ * Returns the inverse of the matrix, or nullptr if it's singular.
+ */
+INLINE const LMatrix4 *TransformState::
+get_inverse_mat() const {
+  if (!is_singular()) {
+    nassertr(_inv_mat != nullptr, nullptr);
+    return _inv_mat;
+  }
+  else {
+    return nullptr;
+  }
+}
+
 /**
  * Returns the pos component of the 2-d transform.  It is an error to call
  * this if has_pos() or is_2d() returned false.

+ 1 - 0
panda/src/pgraph/transformState.h

@@ -142,6 +142,7 @@ PUBLISHED:
   INLINE PN_stdfloat get_uniform_scale() const;
   INLINE const LVecBase3 &get_shear() const;
   INLINE const LMatrix4 &get_mat() const;
+  INLINE const LMatrix4 *get_inverse_mat() const;
 
   INLINE LVecBase2 get_pos2d() const;
   INLINE PN_stdfloat get_rotate2d() const;

+ 1 - 11
panda/src/pgraphnodes/callbackNode.cxx

@@ -30,6 +30,7 @@ CallbackNode(const std::string &name) :
   PandaNode(name)
 {
   PandaNode::set_cull_callback();
+  PandaNode::set_renderable();
 
   // Set up a default, infinite bounding volume, unless the user tells us
   // otherwise.  Not sure if this is a great idea, because it means a naive
@@ -102,17 +103,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool CallbackNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes

+ 0 - 1
panda/src/pgraphnodes/callbackNode.h

@@ -44,7 +44,6 @@ public:
   virtual bool safe_to_combine() const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
   virtual void output(std::ostream &out) const;

+ 1 - 11
panda/src/pgraphnodes/computeNode.cxx

@@ -32,6 +32,7 @@ ComputeNode(const std::string &name) :
   _dispatcher(new ComputeNode::Dispatcher)
 {
   set_internal_bounds(new OmniBoundingVolume);
+  set_renderable();
 }
 
 /**
@@ -65,17 +66,6 @@ safe_to_combine() const {
   return false;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool ComputeNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Adds the node's contents to the CullResult we are building up during the
  * cull traversal, so that it will be drawn at render time.  For most nodes

+ 0 - 1
panda/src/pgraphnodes/computeNode.h

@@ -47,7 +47,6 @@ public:
   virtual PandaNode *make_copy() const;
   virtual bool safe_to_combine() const;
 
-  virtual bool is_renderable() const;
   virtual void add_for_draw(CullTraverser *trav, CullTraverserData &data);
 
   virtual void output(std::ostream &out) const;

+ 23 - 42
panda/src/pgraphnodes/fadeLodNode.cxx

@@ -164,54 +164,37 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
 
         nassertr(elapsed >= 0.0f && elapsed <= _fade_time, false);
 
+        Children children = get_children();
         if (elapsed < half_fade_time) {
           // FIRST HALF OF FADE Fade the new LOD in with z writing off Keep
           // drawing the old LOD opaque with z writing on
-          if (out_child >= 0 && out_child < get_num_children()) {
-            PandaNode *child = get_child(out_child);
-            if (child != nullptr) {
-              CullTraverserData next_data_out(data, child);
-              next_data_out._state =
-                next_data_out._state->compose(get_fade_1_old_state());
-              trav->traverse(next_data_out);
-            }
+          if (out_child >= 0 && out_child < children.get_num_children()) {
+            const PandaNode::DownConnection &child = children.get_child_connection(out_child);
+            trav->traverse_down(data, child,
+              data._state->compose(get_fade_1_old_state()));
           }
 
-          if (in_child >= 0 && in_child < get_num_children()) {
-            PandaNode *child = get_child(in_child);
-            if (child != nullptr) {
-              CullTraverserData next_data_in(data, child);
-
-              PN_stdfloat in_alpha = elapsed / half_fade_time;
-              next_data_in._state =
-                next_data_in._state->compose(get_fade_1_new_state(in_alpha));
-              trav->traverse(next_data_in);
-            }
+          if (in_child >= 0 && in_child < children.get_num_children()) {
+            const PandaNode::DownConnection &child = children.get_child_connection(in_child);
+            PN_stdfloat in_alpha = elapsed / half_fade_time;
+            trav->traverse_down(data, child,
+              data._state->compose(get_fade_1_new_state(in_alpha)));
           }
 
         } else {
           // SECOND HALF OF FADE: Fade out the old LOD with z write off and
           // draw the opaque new LOD with z write on
-          if (in_child >= 0 && in_child < get_num_children()) {
-            PandaNode *child = get_child(in_child);
-            if (child != nullptr) {
-              CullTraverserData next_data_in(data, child);
-              next_data_in._state =
-                next_data_in._state->compose(get_fade_2_new_state());
-              trav->traverse(next_data_in);
-            }
+          if (in_child >= 0 && in_child < children.get_num_children()) {
+            const PandaNode::DownConnection &child = children.get_child_connection(in_child);
+            trav->traverse_down(data, child,
+              data._state->compose(get_fade_2_new_state()));
           }
 
-          if (out_child >= 0 && out_child < get_num_children()) {
-            PandaNode *child = get_child(out_child);
-            if (child != nullptr) {
-              CullTraverserData next_data_out(data, child);
-
-              PN_stdfloat out_alpha = 1.0f - (elapsed - half_fade_time) / half_fade_time;
-              next_data_out._state =
-                next_data_out._state->compose(get_fade_2_old_state(out_alpha));
-              trav->traverse(next_data_out);
-            }
+          if (out_child >= 0 && out_child < children.get_num_children()) {
+            const PandaNode::DownConnection &child = children.get_child_connection(out_child);
+            PN_stdfloat out_alpha = 1.0f - (elapsed - half_fade_time) / half_fade_time;
+            trav->traverse_down(data, child,
+              data._state->compose(get_fade_2_old_state(out_alpha)));
           }
         }
       }
@@ -222,12 +205,10 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     // This is the normal case: we're not in the middle of a transition; we're
     // just drawing one child of the LOD.
     int index = ldata->_fade_in;
-    if (index >= 0 && index < get_num_children()) {
-      PandaNode *child = get_child(index);
-      if (child != nullptr) {
-        CullTraverserData next_data(data, child);
-        trav->traverse(next_data);
-      }
+    Children children = get_children();
+    if (index >= 0 && index < children.get_num_children()) {
+      const PandaNode::DownConnection &child = children.get_child_connection(index);
+      trav->traverse_down(data, child, data._state);
     }
   }
 

+ 16 - 25
panda/src/pgraphnodes/lodNode.cxx

@@ -146,7 +146,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   PN_stdfloat lod_scale = cdata->_lod_scale *
     trav->get_scene()->get_camera_node()->get_lod_scale();
 
-  int num_children = std::min(get_num_children(), (int)cdata->_switch_vector.size());
+  Children children = get_children();
+  int num_children = std::min(children.get_num_children(), cdata->_switch_vector.size());
 
   if (data._instances == nullptr || cdata->_got_force_switch) {
     LPoint3 center = cdata->_center * rel_transform->get_mat();
@@ -163,11 +164,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
 
       if (in_range) {
         // This switch level is in range.  Draw its children.
-        PandaNode *child = get_child(index);
-        if (child != nullptr) {
-          CullTraverserData next_data(data, child);
-          trav->traverse(next_data);
-        }
+        const PandaNode::DownConnection &child = children.get_child_connection(index);
+        trav->traverse_down(data, child);
       }
     }
   }
@@ -193,12 +191,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
       CPT(InstanceList) instances = data._instances->without(in_range[index]);
       if (!instances->empty()) {
         // At least one instance is visible in this switch level.
-        PandaNode *child = get_child(index);
-        if (child != nullptr) {
-          CullTraverserData next_data(data, child);
-          next_data._instances = instances;
-          trav->traverse(next_data);
-        }
+        const PandaNode::DownConnection &child = children.get_child_connection(index);
+        trav->traverse_down(data, child);
       }
     }
   }
@@ -435,26 +429,23 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) {
       if (in_range) {
         // This switch level is in range.  Draw its children in the funny
         // wireframe mode.
-        if (index < get_num_children()) {
-          PandaNode *child = get_child(index);
-          if (child != nullptr) {
-            CullTraverserData next_data3(data, child);
-            next_data3._state = next_data3._state->compose(sw.get_viz_model_state());
-            trav->traverse(next_data3);
-          }
+        Children children = get_children();
+        if (index < children.get_num_children()) {
+          const PandaNode::DownConnection &child = children.get_child_connection(index);
+          trav->traverse_down(data, child, data._state->compose(sw.get_viz_model_state()));
         }
 
         // And draw the spindle in this color.
-        CullTraverserData next_data2(data, sw.get_spindle_viz());
-        next_data2.apply_transform(viz_transform);
-        trav->traverse(next_data2);
+        //CullTraverserData next_data2(data, sw.get_spindle_viz());
+        //next_data2.apply_transform(viz_transform);
+        //trav->traverse(next_data2);
       }
 
       // Draw the rings for this switch level.  We do this after we have drawn
       // the geometry and the spindle.
-      CullTraverserData next_data(data, sw.get_ring_viz());
-      next_data.apply_transform(viz_transform);
-      trav->traverse(next_data);
+      //CullTraverserData next_data(data, sw.get_ring_viz());
+      //next_data.apply_transform(viz_transform);
+      //trav->traverse(next_data);
     }
   }
 

+ 2 - 9
panda/src/pgraphnodes/nodeCullCallbackData.cxx

@@ -56,13 +56,6 @@ upcall() {
     }
   }
 
-  // Now visit all the node's children.
-  PandaNodePipelineReader *node_reader = _data.node_reader();
-  PandaNode::Children children = node_reader->get_children();
-  node_reader->release();
-  int num_children = children.get_num_children();
-  for (int i = 0; i < num_children; ++i) {
-    CullTraverserData next_data(_data, children.get_child(i));
-    _trav->traverse(next_data);
-  }
+  // Now traverse below.
+  _trav->traverse_below(_data);
 }

+ 2 - 14
panda/src/pgraphnodes/selectiveChildNode.I

@@ -16,8 +16,7 @@
  */
 INLINE SelectiveChildNode::
 SelectiveChildNode(const std::string &name) :
-  PandaNode(name),
-  _selected_child(0)
+  PandaNode(name)
 {
 }
 
@@ -26,17 +25,6 @@ SelectiveChildNode(const std::string &name) :
  */
 INLINE SelectiveChildNode::
 SelectiveChildNode(const SelectiveChildNode &copy) :
-  PandaNode(copy),
-  _selected_child(copy._selected_child)
+  PandaNode(copy)
 {
 }
-
-/**
- * Selects the indicated child for rendering.  This is normally called during
- * the cull_callback() method, but it may be called at any time.
- */
-INLINE void SelectiveChildNode::
-select_child(int n) {
-  nassertv(n >= 0);
-  _selected_child = n;
-}

+ 0 - 41
panda/src/pgraphnodes/selectiveChildNode.cxx

@@ -14,44 +14,3 @@
 #include "selectiveChildNode.h"
 
 TypeHandle SelectiveChildNode::_type_handle;
-
-
-/**
- * Should be overridden by derived classes to return true if this kind of node
- * has some restrictions on the set of children that should be rendered.  Node
- * with this property include LODNodes, SwitchNodes, and SequenceNodes.
- *
- * If this function returns true, get_first_visible_child() and
- * get_next_visible_child() will be called to walk through the list of
- * children during cull, instead of iterating through the entire list.  This
- * method is called after cull_callback(), so cull_callback() may be
- * responsible for the decisions as to which children are visible at the
- * moment.
- */
-bool SelectiveChildNode::
-has_selective_visibility() const {
-  return true;
-}
-
-/**
- * Returns the index number of the first visible child of this node, or a
- * number >= get_num_children() if there are no visible children of this node.
- * This is called during the cull traversal, but only if
- * has_selective_visibility() has already returned true.  See
- * has_selective_visibility().
- */
-int SelectiveChildNode::
-get_first_visible_child() const {
-  return _selected_child;
-}
-
-/**
- * Returns the index number of the next visible child of this node following
- * the indicated child, or a number >= get_num_children() if there are no more
- * visible children of this node.  See has_selective_visibility() and
- * get_first_visible_child().
- */
-int SelectiveChildNode::
-get_next_visible_child(int n) const {
-  return get_num_children();
-}

+ 2 - 15
panda/src/pgraphnodes/selectiveChildNode.h

@@ -21,6 +21,8 @@
 /**
  * A base class for nodes like LODNode and SequenceNode that select only one
  * visible child at a time.
+ *
+ * This class is now vestigial.
  */
 class EXPCL_PANDA_PGRAPHNODES SelectiveChildNode : public PandaNode {
 PUBLISHED:
@@ -29,21 +31,6 @@ PUBLISHED:
 protected:
   INLINE SelectiveChildNode(const SelectiveChildNode &copy);
 
-public:
-  virtual bool has_selective_visibility() const;
-  virtual int get_first_visible_child() const;
-  virtual int get_next_visible_child(int n) const;
-
-protected:
-  INLINE void select_child(int n);
-
-private:
-  // Not sure if this should be cycled or not.  It's not exactly thread-safe
-  // not to cycle it, but it doesn't really need the full pipeline control.
-  // It's probably a problem in the non-thread-safe design; need to rethink
-  // the design a bit.
-  int _selected_child;
-
 public:
   static TypeHandle get_class_type() {
     return _type_handle;

+ 4 - 15
panda/src/pgraphnodes/sequenceNode.cxx

@@ -88,21 +88,10 @@ make_copy() const {
  * should be culled.
  */
 bool SequenceNode::
-cull_callback(CullTraverser *, CullTraverserData &) {
-  select_child(get_frame());
-  return true;
-}
-
-/**
- * Returns the index number of the first visible child of this node, or a
- * number >= get_num_children() if there are no visible children of this node.
- * This is called during the cull traversal, but only if
- * has_selective_visibility() has already returned true.  See
- * has_selective_visibility().
- */
-int SequenceNode::
-get_first_visible_child() const {
-  return get_frame();
+cull_callback(CullTraverser *trav, CullTraverserData &data) {
+  const PandaNode::DownConnection &child = data.node_reader()->get_child_connection(get_frame());
+  trav->traverse_down(data, child);
+  return false;
 }
 
 /**

+ 0 - 1
panda/src/pgraphnodes/sequenceNode.h

@@ -43,7 +43,6 @@ public:
   virtual bool safe_to_combine_children() const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual int get_first_visible_child() const;
   virtual bool has_single_child_visibility() const;
   virtual int get_visible_child() const;
 

+ 4 - 15
panda/src/pgraphnodes/switchNode.cxx

@@ -103,21 +103,10 @@ make_copy() const {
  * should be culled.
  */
 bool SwitchNode::
-cull_callback(CullTraverser *, CullTraverserData &) {
-  select_child(get_visible_child());
-  return true;
-}
-
-/**
- * Returns the index number of the first visible child of this node, or a
- * number >= get_num_children() if there are no visible children of this node.
- * This is called during the cull traversal, but only if
- * has_selective_visibility() has already returned true.  See
- * has_selective_visibility().
- */
-int SwitchNode::
-get_first_visible_child() const {
-  return get_visible_child();
+cull_callback(CullTraverser *trav, CullTraverserData &data) {
+  const PandaNode::DownConnection &child = data.node_reader()->get_child_connection(get_visible_child());
+  trav->traverse_down(data, child);
+  return false;
 }
 
 /**

+ 0 - 1
panda/src/pgraphnodes/switchNode.h

@@ -34,7 +34,6 @@ public:
   virtual bool safe_to_combine_children() const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual int get_first_visible_child() const;
   virtual bool has_single_child_visibility() const;
 
 PUBLISHED:

+ 1 - 2
panda/src/pgui/pgEntry.cxx

@@ -184,8 +184,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   }
 
   // Now render the text.
-  CullTraverserData next_data(data, _text_render_root.node());
-  trav->traverse(next_data);
+  trav->traverse_down(data, _text_render_root.node());
 
   // Now continue to render everything else below this node.
   return true;

+ 2 - 13
panda/src/pgui/pgItem.cxx

@@ -67,6 +67,7 @@ PGItem(const string &name) :
   _flags(0)
 {
   set_cull_callback();
+  set_renderable();
 }
 
 /**
@@ -278,25 +279,13 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   if (state_def_root != nullptr) {
     // This item has a current state definition that we should use to render
     // the item.
-    CullTraverserData next_data(data, state_def_root);
-    trav->traverse(next_data);
+    trav->traverse_down(data, state_def_root);
   }
 
   // Now continue to render everything else below this node.
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool PGItem::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Called when needed to recompute the node's _internal_bound object.  Nodes
  * that contain anything of substance should redefine this to do the right

+ 0 - 1
panda/src/pgui/pgItem.h

@@ -65,7 +65,6 @@ protected:
   virtual void draw_mask_changed();
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
   virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
                                        int &internal_vertices,

+ 4 - 13
panda/src/pgui/pgTop.cxx

@@ -29,6 +29,10 @@ PGTop(const std::string &name) :
 {
   set_cull_callback();
 
+  // We flag the PGTop as renderable, even though it technically doesn't have
+  // anything to render, but we do need the traverser to visit it every frame.
+  set_renderable();
+
   _start_sort = 0;
 
   // A PGTop node normally has an infinite bounding volume.  Screw culling.
@@ -111,19 +115,6 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   return false;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool PGTop::
-is_renderable() const {
-  // We flag the PGTop as renderable, even though it technically doesn't have
-  // anything to render, but we do need the traverser to visit it every frame.
-  return true;
-}
-
 /**
  * Sets the MouseWatcher pointer that the PGTop object registers its PG items
  * with.  This must be set before the PG items are active.

+ 0 - 1
panda/src/pgui/pgTop.h

@@ -46,7 +46,6 @@ protected:
 public:
   virtual PandaNode *make_copy() const;
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
 PUBLISHED:
   void set_mouse_watcher(MouseWatcher *watcher);

+ 4 - 0
panda/src/pipeline/cycleDataLockedStageReader.I

@@ -111,7 +111,9 @@ INLINE CycleDataLockedStageReader<CycleDataType>::
 template<class CycleDataType>
 INLINE const CycleDataType *CycleDataLockedStageReader<CycleDataType>::
 operator -> () const {
+#ifdef _DEBUG
   nassertr(_pointer != nullptr, _cycler->cheat());
+#endif
   return _pointer;
 }
 
@@ -122,7 +124,9 @@ operator -> () const {
 template<class CycleDataType>
 INLINE CycleDataLockedStageReader<CycleDataType>::
 operator const CycleDataType * () const {
+#ifdef _DEBUG
   nassertr(_pointer != nullptr, _cycler->cheat());
+#endif
   return _pointer;
 }
 

+ 2 - 13
panda/src/text/textNode.cxx

@@ -61,6 +61,7 @@ PStatCollector TextNode::_text_generate_pcollector("*:Generate Text");
 TextNode::
 TextNode(const string &name) : PandaNode(name) {
   set_cull_callback();
+  set_renderable();
 
   if (text_small_caps) {
     TextProperties::set_small_caps(true);
@@ -487,25 +488,13 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
   PT(PandaNode) internal_geom = do_get_internal_geom();
   if (internal_geom != nullptr) {
     // Render the text with this node.
-    CullTraverserData next_data(data, internal_geom);
-    trav->traverse(next_data);
+    trav->traverse_down(data, internal_geom);
   }
 
   // Now continue to render everything else below this node.
   return true;
 }
 
-/**
- * Returns true if there is some value to visiting this particular node during
- * the cull traversal for any camera, false otherwise.  This will be used to
- * optimize the result of get_net_draw_show_mask(), so that any subtrees that
- * contain only nodes for which is_renderable() is false need not be visited.
- */
-bool TextNode::
-is_renderable() const {
-  return true;
-}
-
 /**
  * Called when needed to recompute the node's _internal_bound object.  Nodes
  * that contain anything of substance should redefine this to do the right

+ 0 - 1
panda/src/text/textNode.h

@@ -286,7 +286,6 @@ public:
                       Thread *current_thread) const;
 
   virtual bool cull_callback(CullTraverser *trav, CullTraverserData &data);
-  virtual bool is_renderable() const;
 
   virtual void compute_internal_bounds(CPT(BoundingVolume) &internal_bounds,
                                        int &internal_vertices,