Browse Source

LODNode should test its bounding volumes

David Rose 19 years ago
parent
commit
2ddcce6ad2

+ 8 - 0
panda/src/pgraph/config_pgraph.cxx

@@ -219,6 +219,14 @@ ConfigVariableDouble lod_fade_time
  PRC_DESC("The default amount of time (in seconds) over which a FadeLODNode "
  PRC_DESC("The default amount of time (in seconds) over which a FadeLODNode "
           "transitions between its different levels."));
           "transitions between its different levels."));
 
 
+ConfigVariableBool verify_lods
+("verify-lods", true,
+ PRC_DESC("When this is true, LODNodes will test when they are rendered to "
+          "ensure that each child's geometry fits entirely within the radius "
+          "defined by its switch-out distance.  When it is false, LODNodes "
+          "may have any switch in and out distances, regardless of the "
+          "actual size of their geometry.  This test is only made in NDEBUG "
+          "mode (the variable is ignored in a production build)."));
 
 
 ConfigVariableBool show_vertex_animation
 ConfigVariableBool show_vertex_animation
 ("show-vertex-animation", false,
 ("show-vertex-animation", false,

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

@@ -51,6 +51,7 @@ extern ConfigVariableInt max_collect_indices;
 
 
 extern ConfigVariableBool polylight_info;
 extern ConfigVariableBool polylight_info;
 extern ConfigVariableDouble lod_fade_time;
 extern ConfigVariableDouble lod_fade_time;
+extern ConfigVariableBool verify_lods;
 
 
 extern ConfigVariableBool show_vertex_animation;
 extern ConfigVariableBool show_vertex_animation;
 extern ConfigVariableBool show_transparency;
 extern ConfigVariableBool show_transparency;

+ 42 - 25
panda/src/pgraph/fadeLodNode.cxx

@@ -77,6 +77,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     return show_switches_cull_callback(trav, data);
     return show_switches_cull_callback(trav, data);
   }
   }
 
 
+  consider_verify_lods(trav, data);
+
   Camera *camera = trav->get_scene()->get_camera_node();
   Camera *camera = trav->get_scene()->get_camera_node();
   NodePath this_np = data._node_path.get_node_path();
   NodePath this_np = data._node_path.get_node_path();
   FadeLODNodeData *ldata = 
   FadeLODNodeData *ldata = 
@@ -126,21 +128,27 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
         // Fade the new LOD in with z writing off
         // Fade the new LOD in with z writing off
         // Keep drawing the old LOD opaque with z writing on
         // Keep drawing the old LOD opaque with z writing on
         if (ldata->_fade_out >= 0 && ldata->_fade_out < get_num_children()) {
         if (ldata->_fade_out >= 0 && ldata->_fade_out < get_num_children()) {
-          CullTraverserData next_data_out(data, get_child(ldata->_fade_out));
-          trav->traverse(next_data_out);
+          PandaNode *child = get_child(ldata->_fade_out);
+          if (child != (PandaNode *)NULL) {
+            CullTraverserData next_data_out(data, child);
+            trav->traverse(next_data_out);
+          }
         }
         }
         
         
         if (ldata->_fade_in >= 0 && ldata->_fade_in < get_num_children()) {
         if (ldata->_fade_in >= 0 && ldata->_fade_in < get_num_children()) {
-          CullTraverserData next_data_in(data, get_child(ldata->_fade_in));
-          
-          float in_alpha = elapsed / half_fade_time;
-          LVecBase4f alpha_scale(1.0f, 1.0f, 1.0f, in_alpha);
-          
-          next_data_in._state = 
-            next_data_in._state->compose(get_fade_out_state())->compose
-            (RenderState::make(ColorScaleAttrib::make(alpha_scale)));
-          
-          trav->traverse(next_data_in);
+          PandaNode *child = get_child(ldata->_fade_in);
+          if (child != (PandaNode *)NULL) {
+            CullTraverserData next_data_in(data, child);
+            
+            float in_alpha = elapsed / half_fade_time;
+            LVecBase4f alpha_scale(1.0f, 1.0f, 1.0f, in_alpha);
+            
+            next_data_in._state = 
+              next_data_in._state->compose(get_fade_out_state())->compose
+              (RenderState::make(ColorScaleAttrib::make(alpha_scale)));
+            
+            trav->traverse(next_data_in);
+          }
         }
         }
         
         
       } else if (elapsed < _fade_time) {
       } else if (elapsed < _fade_time) {
@@ -148,21 +156,27 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
         //Fade out the old LOD with z write off and 
         //Fade out the old LOD with z write off and 
         //draw the opaque new LOD with z write on
         //draw the opaque new LOD with z write on
         if (ldata->_fade_in >= 0 && ldata->_fade_in < get_num_children()) {
         if (ldata->_fade_in >= 0 && ldata->_fade_in < get_num_children()) {
-          CullTraverserData next_data_in(data, get_child(ldata->_fade_in));
-          trav->traverse(next_data_in);
+          PandaNode *child = get_child(ldata->_fade_in);
+          if (child != (PandaNode *)NULL) {
+            CullTraverserData next_data_in(data, child);
+            trav->traverse(next_data_in);
+          }
         }
         }
         
         
         if (ldata->_fade_out >= 0 && ldata->_fade_out < get_num_children()) {
         if (ldata->_fade_out >= 0 && ldata->_fade_out < get_num_children()) {
-          CullTraverserData next_data_out(data, get_child(ldata->_fade_out));
+          PandaNode *child = get_child(ldata->_fade_out);
+          if (child != (PandaNode *)NULL) {
+            CullTraverserData next_data_out(data, child);
           
           
-          float out_alpha = 1.0f - (elapsed - half_fade_time) / half_fade_time;  
-          LVecBase4f alpha_scale(1.0f, 1.0f, 1.0f, out_alpha);
-          
-          next_data_out._state = 
-            next_data_out._state->compose(get_fade_out_state())->compose
-            (RenderState::make(ColorScaleAttrib::make(alpha_scale)));
-          
-          trav->traverse(next_data_out);
+            float out_alpha = 1.0f - (elapsed - half_fade_time) / half_fade_time;  
+            LVecBase4f alpha_scale(1.0f, 1.0f, 1.0f, out_alpha);
+            
+            next_data_out._state = 
+              next_data_out._state->compose(get_fade_out_state())->compose
+              (RenderState::make(ColorScaleAttrib::make(alpha_scale)));
+            
+            trav->traverse(next_data_out);
+          }
         }
         }
         
         
       } else {
       } else {
@@ -177,8 +191,11 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     // transition; we're just drawing one child of the LOD.
     // transition; we're just drawing one child of the LOD.
     int index = ldata->_fade_in;
     int index = ldata->_fade_in;
     if (index >= 0 && index < get_num_children()) {
     if (index >= 0 && index < get_num_children()) {
-      CullTraverserData next_data(data, get_child(index));
-      trav->traverse(next_data);
+      PandaNode *child = get_child(index);
+      if (child != (PandaNode *)NULL) {
+        CullTraverserData next_data(data, child);
+        trav->traverse(next_data);
+      }
     }
     }
   }
   }
 
 

+ 21 - 0
panda/src/pgraph/lodNode.I

@@ -56,6 +56,8 @@ LODNode(const LODNode &copy) :
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 INLINE void LODNode::
 INLINE void LODNode::
 add_switch(float in, float out) {
 add_switch(float in, float out) {
+  nassertv(in >= out);
+
   CDWriter cdata(_cycler);
   CDWriter cdata(_cycler);
   cdata->_switch_vector.push_back(Switch(in, out));
   cdata->_switch_vector.push_back(Switch(in, out));
   cdata->check_limits();
   cdata->check_limits();
@@ -73,6 +75,8 @@ add_switch(float in, float out) {
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 INLINE bool LODNode::
 INLINE bool LODNode::
 set_switch(int index, float in, float out) {
 set_switch(int index, float in, float out) {
+  nassertr(in >= out, false);
+
   CDWriter cdata(_cycler);
   CDWriter cdata(_cycler);
   nassertr(index >= 0 && index < (int)cdata->_switch_vector.size(), false);
   nassertr(index >= 0 && index < (int)cdata->_switch_vector.size(), false);
   cdata->_switch_vector[index].set_range(in, out);
   cdata->_switch_vector[index].set_range(in, out);
@@ -244,6 +248,23 @@ is_any_shown() const {
   return (cdata->_num_shown != 0);
   return (cdata->_num_shown != 0);
 }
 }
 
 
+////////////////////////////////////////////////////////////////////
+//     Function: LODNode::consider_verify_lods
+//       Access: Protected
+//  Description: To be called internally when the node is rendered,
+//               this will raise an assertion if verify-lods is
+//               configured true, and verify_child_bounds() returns
+//               false.
+////////////////////////////////////////////////////////////////////
+INLINE void LODNode::
+consider_verify_lods(CullTraverser *trav, CullTraverserData &data) {
+#ifndef NDEBUG
+  if (verify_lods) {
+    do_auto_verify_lods(trav, data);
+  }
+#endif  // NDEBUG
+}
+
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 //     Function: LODNode::CData::Constructor
 //     Function: LODNode::CData::Constructor
 //       Access: Public
 //       Access: Public

+ 238 - 76
panda/src/pgraph/lodNode.cxx

@@ -35,7 +35,9 @@
 #include "cullFaceAttrib.h"
 #include "cullFaceAttrib.h"
 #include "textureAttrib.h"
 #include "textureAttrib.h"
 #include "boundingSphere.h"
 #include "boundingSphere.h"
+#include "geometricBoundingVolume.h"
 #include "look_at.h"
 #include "look_at.h"
+#include "nodePath.h"
 
 
 TypeHandle LODNode::_type_handle;
 TypeHandle LODNode::_type_handle;
 
 
@@ -132,10 +134,32 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) {
     return show_switches_cull_callback(trav, data);
     return show_switches_cull_callback(trav, data);
   }
   }
 
 
-  int index = compute_child(trav, data);
-  if (index >= 0 && index < get_num_children()) {
-    CullTraverserData next_data(data, get_child(index));
-    trav->traverse(next_data);
+  consider_verify_lods(trav, data);
+
+  CDReader cdata(_cycler);
+
+  CPT(TransformState) rel_transform = get_rel_transform(trav, data);
+  LPoint3f center = cdata->_center * rel_transform->get_mat();
+  float dist2 = center.dot(center);
+
+  int num_children = min(get_num_children(), (int)cdata->_switch_vector.size());
+  for (int index = 0; index < num_children; ++index) {
+    const Switch &sw = cdata->_switch_vector[index];
+    bool in_range;
+    if (cdata->_got_force_switch) {
+      in_range = (cdata->_force_switch == index);
+    } else {
+      in_range = sw.in_range_2(dist2);
+    }
+    
+    if (in_range) {
+      // This switch level is in range.  Draw its children.
+      PandaNode *child = get_child(index);
+      if (child != (PandaNode *)NULL) {
+        CullTraverserData next_data(data, child);
+        trav->traverse(next_data);
+      }
+    }
   }
   }
 
 
   // Now return false indicating that we have already taken care of
   // Now return false indicating that we have already taken care of
@@ -269,6 +293,35 @@ hide_all_switches() {
   mark_internal_bounds_stale();
   mark_internal_bounds_stale();
 }
 }
 
 
+////////////////////////////////////////////////////////////////////
+//     Function: LODNode::verify_child_bounds
+//       Access: Published
+//  Description: Returns true if the bounding volumes for the geometry
+//               of each fhild node entirely fits within the
+//               switch_in radius for that child, or false otherwise.
+//               It is almost always a mistake for the geometry of an
+//               LOD level to be larger than its switch_in radius.
+////////////////////////////////////////////////////////////////////
+bool LODNode::
+verify_child_bounds() const {
+  bool okflag = true;
+  CDReader cdata(_cycler);
+
+  for (int index = 0; index < (int)cdata->_switch_vector.size(); ++index) {
+    float suggested_radius;
+    if (!do_verify_child_bounds(cdata, index, suggested_radius)) { 
+      const Switch &sw = cdata->_switch_vector[index];
+      pgraph_cat.warning()
+        << "Level " << index << " geometry of " << *this
+        << " is larger than its switch radius; suggest radius of "
+        << suggested_radius << " instead of " << sw.get_in() << "\n";
+      okflag = false;
+    }
+  }
+
+  return okflag;
+}
+
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 //     Function: LODNode::compute_child
 //     Function: LODNode::compute_child
 //       Access: Protected
 //       Access: Protected
@@ -290,36 +343,11 @@ compute_child(CullTraverser *trav, CullTraverserData &data) {
     return cdata->_force_switch;
     return cdata->_force_switch;
   }
   }
 
 
-  // Get a pointer to the camera node.
-  Camera *camera = trav->get_scene()->get_camera_node();
-  
-  // Get the LOD center in camera space.  If the camera has a special
-  // LOD center defined, use that; otherwise, if it has a cull center,
-  // use that; otherwise, use the modelview transform (which is camera
-  // space).
-  CPT(TransformState) rel_transform;
-
-  NodePath lod_center = camera->get_lod_center();
-  if (!lod_center.is_empty()) {
-    rel_transform = 
-      lod_center.get_net_transform()->invert_compose(data.get_net_transform(trav));
-  } else {
-    NodePath cull_center = camera->get_cull_center();
-    if (!cull_center.is_empty()) {
-      rel_transform = 
-        cull_center.get_net_transform()->invert_compose(data.get_net_transform(trav));
-    } else {
-      rel_transform = data.get_modelview_transform(trav);
-    }
-  }
-
+  CPT(TransformState) rel_transform = get_rel_transform(trav, data);
   LPoint3f center = cdata->_center * rel_transform->get_mat();
   LPoint3f center = cdata->_center * rel_transform->get_mat();
-
-  // Now measure the distance to the LOD center, and use that to
-  // determine which child to display.
   float dist2 = center.dot(center);
   float dist2 = center.dot(center);
 
 
-  for (int index = 0; index < (int)cdata->_switch_vector.size(); index++) {
+  for (int index = 0; index < (int)cdata->_switch_vector.size(); ++index) {
     if (cdata->_switch_vector[index].in_range_2(dist2)) { 
     if (cdata->_switch_vector[index].in_range_2(dist2)) { 
       if (pgraph_cat.is_debug()) {
       if (pgraph_cat.is_debug()) {
         pgraph_cat.debug()
         pgraph_cat.debug()
@@ -353,27 +381,7 @@ bool LODNode::
 show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) {
 show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) {
   CDReader cdata(_cycler);
   CDReader cdata(_cycler);
 
 
-  // Get a pointer to the camera node.
-  Camera *camera = trav->get_scene()->get_camera_node();
-  
-  // Get the camera space transform.  This bit is the same as the code
-  // in compute_child(), above.
-  CPT(TransformState) rel_transform;
-
-  NodePath lod_center = camera->get_lod_center();
-  if (!lod_center.is_empty()) {
-    rel_transform = 
-      lod_center.get_net_transform()->invert_compose(data.get_net_transform(trav));
-  } else {
-    NodePath cull_center = camera->get_cull_center();
-    if (!cull_center.is_empty()) {
-      rel_transform = 
-        cull_center.get_net_transform()->invert_compose(data.get_net_transform(trav));
-    } else {
-      rel_transform = data.get_modelview_transform(trav);
-    }
-  }
-
+  CPT(TransformState) rel_transform = get_rel_transform(trav, data);
   LPoint3f center = cdata->_center * rel_transform->get_mat();
   LPoint3f center = cdata->_center * rel_transform->get_mat();
   float dist2 = center.dot(center);
   float dist2 = center.dot(center);
 
 
@@ -382,22 +390,12 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) {
   LMatrix4f mat;
   LMatrix4f mat;
   look_at(mat, -center, LVector3f(0.0f, 0.0f, 1.0f));
   look_at(mat, -center, LVector3f(0.0f, 0.0f, 1.0f));
   mat.set_row(3, center);
   mat.set_row(3, center);
-  CPT(TransformState) viz_transform = TransformState::make_mat(mat);
-
-  viz_transform = rel_transform->invert_compose(viz_transform);
-  viz_transform = data.get_net_transform(trav)->compose(viz_transform);
+  CPT(TransformState) viz_transform = 
+    rel_transform->invert_compose(TransformState::make_mat(mat));
 
 
-  SwitchVector::const_iterator si;
-  for (si = cdata->_switch_vector.begin(); 
-       si != cdata->_switch_vector.end(); 
-       ++si) {
-    const Switch &sw = (*si);
+  for (int index = 0; index < (int)cdata->_switch_vector.size(); ++index) {
+    const Switch &sw = cdata->_switch_vector[index];
     if (sw.is_shown()) {
     if (sw.is_shown()) {
-      CullTraverserData next_data(data, sw.get_ring_viz());
-      next_data._net_transform = viz_transform;
-      trav->traverse(next_data);
-
-      int index = (si - cdata->_switch_vector.begin());
       bool in_range;
       bool in_range;
       if (cdata->_got_force_switch) {
       if (cdata->_got_force_switch) {
         in_range = (cdata->_force_switch == index);
         in_range = (cdata->_force_switch == index);
@@ -406,19 +404,34 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) {
       }
       }
 
 
       if (in_range) {
       if (in_range) {
-        // This switch level is in range.  Draw the spindle in this
-        // color.
-        CullTraverserData next_data2(data, sw.get_spindle_viz());
-        next_data2._net_transform = viz_transform;
-        trav->traverse(next_data2);
-
-        // And draw its children in the funny wireframe mode.
+        // This switch level is in range.  Draw its children in the
+        // funny wireframe mode.
         if (index < get_num_children()) {
         if (index < get_num_children()) {
-          CullTraverserData next_data3(data, get_child(index));
-          next_data3._state = next_data3._state->compose(sw.get_viz_model_state());
-          trav->traverse(next_data3);
+          PandaNode *child = get_child(index);
+          if (child != (PandaNode *)NULL) {
+            CullTraverserData next_data3(data, child);
+            next_data3._state = next_data3._state->compose(sw.get_viz_model_state());
+            trav->traverse(next_data3);
+          }
         }
         }
+
+        // And draw the spindle in this color.
+        CullTraverserData next_data2(data, sw.get_spindle_viz());
+        next_data2.apply_transform_and_state(trav, viz_transform,
+                                             RenderState::make_empty(),
+                                             RenderEffects::make_empty(),
+                                             ClipPlaneAttrib::make());
+        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_and_state(trav, viz_transform,
+                                          RenderState::make_empty(),
+                                          RenderEffects::make_empty(),
+                                          ClipPlaneAttrib::make());
+      trav->traverse(next_data);
     }
     }
   }
   }
 
 
@@ -471,6 +484,37 @@ compute_internal_bounds(int pipeline_stage, Thread *current_thread) const {
   return bound;
   return bound;
 }
 }
 
 
+////////////////////////////////////////////////////////////////////
+//     Function: LODNode::get_rel_transform
+//       Access: Protected
+//  Description: Returns the relative transform to convert from the
+//               LODNode space to the camera space.
+////////////////////////////////////////////////////////////////////
+CPT(TransformState) LODNode::
+get_rel_transform(CullTraverser *trav, CullTraverserData &data) {
+  // Get a pointer to the camera node.
+  Camera *camera = trav->get_scene()->get_camera_node();
+  
+  // Get the camera space transform.
+  CPT(TransformState) rel_transform;
+
+  NodePath lod_center = camera->get_lod_center();
+  if (!lod_center.is_empty()) {
+    rel_transform = 
+      lod_center.get_net_transform()->invert_compose(data.get_net_transform(trav));
+  } else {
+    NodePath cull_center = camera->get_cull_center();
+    if (!cull_center.is_empty()) {
+      rel_transform = 
+        cull_center.get_net_transform()->invert_compose(data.get_net_transform(trav));
+    } else {
+      rel_transform = data.get_modelview_transform(trav);
+    }
+  }
+
+  return rel_transform;
+}
+
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 //     Function: LODNode::do_show_switch
 //     Function: LODNode::do_show_switch
 //       Access: Private
 //       Access: Private
@@ -501,6 +545,124 @@ do_hide_switch(LODNode::CData *cdata, int index) {
   cdata->_switch_vector[index].hide();
   cdata->_switch_vector[index].hide();
 }
 }
 
 
+////////////////////////////////////////////////////////////////////
+//     Function: LODNode::do_verify_child_bounds
+//       Access: Private
+//  Description: The private implementation of verify_child_bounds(),
+//               this checks the bounding volume of just one child.
+//
+//               If the return value is false, suggested_radius is
+//               filled with a radius that ought to be large enough to
+//               include the child.
+////////////////////////////////////////////////////////////////////
+bool LODNode::
+do_verify_child_bounds(const LODNode::CData *cdata, int index,
+                       float &suggested_radius) const {
+  suggested_radius = 0.0f;
+
+  if (index < get_num_children()) {
+    PandaNode *child = get_child(index);
+    if (child != (PandaNode *)NULL) {
+      CPT(BoundingVolume) bv = child->get_bounds();
+      if (bv->is_empty()) {
+        // This child has no geometry, so no one cares anyway.
+        return true;
+      }
+      
+      const Switch &sw = cdata->_switch_vector[index];
+      
+      const GeometricBoundingVolume *gbv;
+      DCAST_INTO_R(gbv, bv, false);
+      BoundingSphere sphere(cdata->_center, sw.get_in());
+      sphere.local_object();
+      
+      int flags = sphere.contains(gbv);
+      if ((flags & BoundingVolume::IF_all) != 0) {
+        // This child's radius completely encloses its bounding volume.
+        // Perfect.  (And this is the most common case.)
+        return true;
+      }
+      
+      if (flags == 0) {
+        // This child's radius doesn't even come close to containing
+        // its volume.
+        sphere.extend_by(gbv);
+        suggested_radius = sphere.get_radius();
+        return false;
+      }
+      
+      // This child's radius partially encloses its (loose) bounding
+      // volume.  We have to look closer to determine whether it, in
+      // fact, fully encloses its geometry.
+      LPoint3f min_point(0.0f, 0.0f, 0.0f);
+      LPoint3f max_point(0.0f, 0.0f, 0.0f);
+      
+      bool found_any = false;
+      child->calc_tight_bounds(min_point, max_point, found_any, 
+                               TransformState::make_identity(),
+                               Thread::get_current_thread());
+      if (!found_any) {
+        // Hmm, the child has no geometry after all.
+        return true;
+      }
+      
+      // Now we have a bounding box.  Define the largest sphere we can
+      // that fits within this box.  All we can say about this sphere
+      // is that it should definitely fit entirely within a bounding
+      // sphere that contains all the points of the child.
+      LPoint3f box_center = (min_point + max_point) / 2.0f;
+      float box_radius = min(min(max_point[0] - box_center[0],
+                                 max_point[1] - box_center[1]),
+                             max_point[2] - box_center[2]);
+      
+      BoundingSphere box_sphere(box_center, box_radius);
+      box_sphere.local_object();
+      
+      // So if any part of this inscribed sphere is outside of the
+      // radius, then the radius is bad.
+      flags = sphere.contains(&box_sphere);
+      if (flags != BoundingVolume::IF_all) {
+        // No good.  
+        sphere.extend_by(gbv);
+        suggested_radius = sphere.get_radius();
+        return false;
+      }
+    }
+  }    
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: LODNode::do_auto_verify_lods
+//       Access: Private
+//  Description: Called internally by consider_verify_lods().
+////////////////////////////////////////////////////////////////////
+void LODNode::
+do_auto_verify_lods(CullTraverser *trav, CullTraverserData &data) {
+  UpdateSeq seq;
+  get_bounds(seq);
+  CDLockedReader cdata(_cycler);
+  if (seq != cdata->_bounds_seq) {
+    // Time to validate the children again.
+    for (int index = 0; index < (int)cdata->_switch_vector.size(); ++index) {
+      float suggested_radius;
+      if (!do_verify_child_bounds(cdata, index, suggested_radius)) { 
+        const Switch &sw = cdata->_switch_vector[index];
+        ostringstream strm;
+        strm
+          << "Level " << index << " geometry of " << data._node_path
+          << " is larger than its switch radius; suggest radius of "
+          << suggested_radius << " instead of " << sw.get_in() 
+          << " (configure verify-lods 0 to ignore this error)";
+        nassert_raise(strm.str());
+      }
+    }
+    CDWriter cdataw(_cycler, cdata);
+    cdataw->_bounds_seq = seq;
+  }
+}
+
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 //     Function: LODNode::get_default_show_color
 //     Function: LODNode::get_default_show_color
 //       Access: Private, Static
 //       Access: Private, Static

+ 11 - 0
panda/src/pgraph/lodNode.h

@@ -79,16 +79,25 @@ PUBLISHED:
   void hide_all_switches();
   void hide_all_switches();
   INLINE bool is_any_shown() const;
   INLINE bool is_any_shown() const;
 
 
+  bool verify_child_bounds() const;
+
 protected:
 protected:
   int compute_child(CullTraverser *trav, CullTraverserData &data);
   int compute_child(CullTraverser *trav, CullTraverserData &data);
 
 
   bool show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data);
   bool show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data);
   virtual PT(BoundingVolume) compute_internal_bounds(int pipeline_stage, Thread *current_thread) const;
   virtual PT(BoundingVolume) compute_internal_bounds(int pipeline_stage, Thread *current_thread) const;
 
 
+  INLINE void consider_verify_lods(CullTraverser *trav, CullTraverserData &data);
+
+  CPT(TransformState) get_rel_transform(CullTraverser *trav, CullTraverserData &data);
+
 private:
 private:
   class CData;
   class CData;
   void do_show_switch(CData *cdata, int index, const Colorf &color);
   void do_show_switch(CData *cdata, int index, const Colorf &color);
   void do_hide_switch(CData *cdata, int index);
   void do_hide_switch(CData *cdata, int index);
+  bool do_verify_child_bounds(const CData *cdata, int index,
+                              float &suggested_radius) const;
+  void do_auto_verify_lods(CullTraverser *trav, CullTraverserData &data);
 
 
   static const Colorf &get_default_show_color(int index);
   static const Colorf &get_default_show_color(int index);
 
 
@@ -152,6 +161,7 @@ private:
     LPoint3f _center;
     LPoint3f _center;
     SwitchVector _switch_vector;
     SwitchVector _switch_vector;
     size_t _lowest, _highest;
     size_t _lowest, _highest;
+    UpdateSeq _bounds_seq;
 
 
     bool _got_force_switch;
     bool _got_force_switch;
     int _force_switch;
     int _force_switch;
@@ -161,6 +171,7 @@ private:
   PipelineCycler<CData> _cycler;
   PipelineCycler<CData> _cycler;
   typedef CycleDataReader<CData> CDReader;
   typedef CycleDataReader<CData> CDReader;
   typedef CycleDataWriter<CData> CDWriter;
   typedef CycleDataWriter<CData> CDWriter;
+  typedef CycleDataLockedReader<CData> CDLockedReader;
   typedef CycleDataStageReader<CData> CDStageReader;
   typedef CycleDataStageReader<CData> CDStageReader;
   typedef CycleDataStageWriter<CData> CDStageWriter;
   typedef CycleDataStageWriter<CData> CDStageWriter;
 
 

+ 36 - 0
panda/src/pgraph/pandaNode.cxx

@@ -1821,6 +1821,41 @@ get_bounds(Thread *current_thread) const {
   return cdata->_external_bounds;
   return cdata->_external_bounds;
 }
 }
 
 
+////////////////////////////////////////////////////////////////////
+//     Function: PandaNode::get_bounds
+//       Access: Published
+//  Description: This flavor of get_bounds() return the external
+//               bounding volume, and also fills in seq with the
+//               bounding volume's current sequence number.  When this
+//               sequence number changes, it indicates that the
+//               bounding volume might have changed, e.g. because some
+//               nested child's bounding volume has changed.
+//
+//               Although this might occasionally increment without
+//               changing the bounding volume, the bounding volume
+//               will never change without incrementing this counter,
+//               so as long as this counter remains unchanged you can
+//               be confident the bounding volume is also unchanged.
+////////////////////////////////////////////////////////////////////
+CPT(BoundingVolume) PandaNode::
+get_bounds(UpdateSeq &seq, Thread *current_thread) const {
+  int pipeline_stage = current_thread->get_pipeline_stage();
+  CDLockedStageReader cdata(_cycler, pipeline_stage, current_thread);
+  if (cdata->_last_update != cdata->_next_update) {
+    // The cache is stale; it needs to be rebuilt.
+    CPT(BoundingVolume) result;
+    {
+      CDStageWriter cdataw = 
+	((PandaNode *)this)->update_bounds(pipeline_stage, cdata); 
+      result = cdataw->_external_bounds;
+      seq = cdataw->_last_update;
+    }
+    return result;
+  }
+  seq = cdata->_last_update;
+  return cdata->_external_bounds;
+}
+
 ////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////
 //     Function: PandaNode::mark_bounds_stale
 //     Function: PandaNode::mark_bounds_stale
 //       Access: Published
 //       Access: Published
@@ -3079,6 +3114,7 @@ update_bounds(int pipeline_stage, PandaNode::CDLockedStageReader &cdata) {
           drawmask_cat.debug(false)
           drawmask_cat.debug(false)
             << "} " << *this << "::update_bounds();\n";
             << "} " << *this << "::update_bounds();\n";
         }
         }
+
 	return cdataw;
 	return cdataw;
       }
       }
       
       

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

@@ -240,6 +240,7 @@ PUBLISHED:
   void set_bound(const BoundingVolume *volume);
   void set_bound(const BoundingVolume *volume);
   INLINE void clear_bounds();
   INLINE void clear_bounds();
   CPT(BoundingVolume) get_bounds(Thread *current_thread = Thread::get_current_thread()) const;
   CPT(BoundingVolume) get_bounds(Thread *current_thread = Thread::get_current_thread()) const;
+  CPT(BoundingVolume) get_bounds(UpdateSeq &seq, Thread *current_thread = Thread::get_current_thread()) const;
   INLINE CPT(BoundingVolume) get_internal_bounds() const;
   INLINE CPT(BoundingVolume) get_internal_bounds() const;
 
 
   void mark_bounds_stale(Thread *current_thread = Thread::get_current_thread()) const;
   void mark_bounds_stale(Thread *current_thread = Thread::get_current_thread()) const;