Add continuous collision checking - #12
Merged
rjoomen merged 117 commits intoApr 8, 2026
Merged
Conversation
…nager The first loop over link2cow_ was adding kinematic objects' regular COW pointers to the dynamic broadphase update, but those objects are not registered in the dynamic manager — only the cast COWs are. This could cause undefined behavior in Coal's broadphase update. Now only static regular COWs are added to static_update_; kinematic cast COWs are handled exclusively by the second loop. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
The early-out compared only pose1 against the stored transform, so changes to pose2 alone were silently ignored — the cast transform and broadphase AABB were never updated. This matches the Bullet continuous collision manager which unconditionally applies both poses. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
Replace linear std::find searches against broadphase manager object lists with std::unordered_set lookups in removeCollisionObject (discrete) and removeObjects (cast). This reduces the complexity of unregistering N collision objects from two broadphase managers containing M objects from O(N*M) to O(N+M). https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…update extractVertices() calls toTriangleMesh() for curved shapes (Sphere, Cylinder, Cone, Capsule), which is an expensive tessellation operation. Previously this ran on every updateCastTransform() call. Now the base vertices are extracted once at construction and cached in base_vertices_, so computeSweptVertices() only needs to apply the transform to the cached points. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
Previously, if an unrecognized shape type was passed to computeBV<AABB, CastHullShape>, none of the dynamic_cast branches would match and bv_original/bv_cast would remain uninitialized, producing a garbage bounding volume (undefined behavior). Add an else branch that builds the AABB from the pre-computed swept vertices, which are available for any shape type. Also add a getSweptVertices() accessor to CastHullShape. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
The CollisionCallback and DistanceCallback are shared between discrete and continuous collision checking. When used by the continuous cast manager (CoalCastBVHManager), the three continuous-collision fields on ContactResult were never populated, leaving them at their defaults (cc_time=-1, cc_type=CCType_None, cc_transform=Identity). Add populateContinuousCollisionFields() which detects CastHullShape geometry on each collision object and computes: - cc_transform: end-of-motion world pose (pose1 * castTransform) - cc_time: linear interpolation parameter by projecting the contact point onto the start-to-end motion trajectory - cc_type: Time0 / Between / Time1 classification The function is a no-op for discrete checks (no CastHullShape present). https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…uousData Replace the linear projection approach for cc_time computation with Bullet's support-function-based approach. This uses GetAverageSupport to find the shape's extreme points along the contact normal at t=0 and t=1, then classifies collision time based on which pose has greater support projection. Key changes: - Add GetAverageSupport helper that averages support vertices for polyhedral shapes (Box, ConvexBase32) and falls back to coal::details::getSupport for smooth shapes - Use average contact point (midpoint of nearest_points) matching Bullet - Normal convention: from current object toward other (negate for obj 1) - In Between case, update nearest_points_local to averaged support point - Add tolerance constants matching Bullet (0.01 support, 0.001 length) https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
Box is a smooth primitive in COAL with a unique support vertex per direction, so vertex averaging is unnecessary. Only ConvexBase32 needs explicit vertex iteration since multiple vertices can share a support plane. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
COAL's getSupport already handles ConvexBase32, so the manual vertex iteration and averaging is unnecessary. The support *value* is identical; only the Between-case cc_time could differ slightly (arbitrary vertex vs face center), but the Time0/Time1/Between classification is unchanged. Rename to GetSupport and remove unused COAL_EPSILON constant. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
updateCastTransform() was not calling computeLocalAABB(), so the geometry's local AABB stayed at its initial (identity cast transform) size. The broadphase then used a tiny AABB that didn't cover the swept path, causing no candidate pairs to be generated and all CastBVH collision tests to return empty results. Also reorder setCollisionObjectsTransform(name, pose1, pose2) to update the cast transform before setting the world transform, so that updateAABB() picks up the correct aabb_local. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
CastHullShape previously extended coal::ShapeBase directly, which meant it reported BV_UNKNOWN as its node type. COAL's collision dispatcher doesn't recognize BV_UNKNOWN and throws: "Collision function between node type GEOM_BOX and node type BV_UNKNOWN is not yet supported." By extending coal::ConvexBase32, CastHullShape: - Reports GEOM_CONVEX as its node type - Is recognized by COAL's GJK-based collision solver - Uses ConvexBase32::points (set to swept vertices) for the support function, giving correct swept hull collision The swept vertices are stored directly in the inherited `points` member, removing the separate `swept_vertices_` field. A new updateConvexMembers() helper keeps num_points and center in sync. clone() builds from scratch to ensure the points alias is maintained. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
ConvexBaseTpl::clone() returns ConvexBaseTpl*, so the override must return a covariant type (CastHullShape*), not ShapeBase*. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…nvex hull CastHullShape previously derived from ConvexBase32 and only stored vertices without populating the neighbors data structure. This caused a crash in coal::details::getShapeSupportLog because the GJK hill-climbing support function requires neighbor adjacency data. Now derives from ConvexTpl<Triangle32> and uses ConvexBase32::convexHull() (which internally uses qhull) to compute a proper convex hull of the swept vertices. The ConvexTpl constructor calls fillNeighbors() and buildSupportWarmStart(), ensuring all data structures are properly populated. Replaces the manual computeSweptVertices()/updateConvexMembers() methods with a single buildConvexHull() method that produces a complete convex polytope with points, triangles, neighbors, and support warm-start data. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
The convex hull approach uses vertex-based support (vertices at z=±0.125 for the box corners) whereas the exact Box support function can return face points at z=0 for horizontal directions. Coal adjusts the contact point by the security_margin (0.1), yielding z = -0.125 + 0.1 = -0.025 instead of the ideal z = 0.0. Widen the Z-component tolerances from 0.001 to 0.03 to accommodate this inherent limitation of vertex-based convex hull collision detection. https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
This reverts commit b12e400.
…t function Bypass coal::ComputeCollision for CastHullShape collisions by injecting a custom support function into MinkowskiDiff that implements the Schulman et al. swept-shape support: max(support_start(d), support_end(d)). This runs GJK/EPA directly, extracting witness points and penetration depth. Also adds explicit template instantiations for getShapeSupport<CastHullShape> so they are available from other translation units. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
…ata API Use default constructor + member assignment instead of the parameterized ContactTestData constructor, making this compatible regardless of whether the 'active' parameter was removed (tesseract PR #1250) or not. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Coal's getSupportTpl computes shape1's support in direction -dir (negated), matching the Minkowski difference convention: w = s_S0(d) - s_S1(-d). Our custom support function was passing dir (positive) for shape1, giving an incorrect Minkowski difference and causing GJK to miss collisions. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
When both collision objects are CastHullShapes (e.g. sphere-sphere cast tests), shape1 must also use the Schulman support function directly. Dispatching a CastHullShape (ConvexBase32) through coal's generic getSupport mishandles the 32-bit index type, causing incorrect support points and missed collisions. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
GJK's distance_upper_bound (set to security_margin ≈ 0.1) caused NoCollisionEarlyStopped for CastHullShape collisions. The swept volumes extend much farther than the security_margin, so the Minkowski difference support values easily exceeded the threshold, triggering early stopping before the simplex could enclose the origin. Set distance_upper_bound to max for CastHullShape to let GJK fully converge. This fixes sphere-sphere and contact manager cast collision tests where both shapes are CastHullShapes with large swept extents. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
The custom castHullCollide function with Schulman support injection had two issues: 1. GJK distance_upper_bound (security_margin ≈ 0.1) triggered premature NoCollisionEarlyStopped for large swept volumes 2. GJK PolyakAcceleration + DualityGap convergence failed to detect collision for primitive sphere underlying shapes Since CastHullShape IS a ConvexBase32 with correctly-built swept hull vertices, coal's standard ComputeCollision functor handles collision detection robustly via its built-in ConvexBase32 GJK support. The continuous-collision fields (cc_time, cc_transform) are still computed by populateContinuousCollisionFields using the underlying shape's exact support function for precise time interpolation. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
The previous commit replaced the custom Schulman support GJK path with coal's standard ComputeCollision, which treated CastHullShape as a plain ConvexBase32. This caused accuracy regressions because the convex hull mesh approximation of spheres is inscribed in the actual sphere, leading to distance, witness point, and cc_time errors. This restores the custom castHullCollide/castHullGetSupportFunc functions that inject the Schulman support function (exact support of underlying shape at t=0 and t=1) into GJK's MinkowskiDiff. The key fix is using DefaultGJK (no acceleration) instead of PolyakAcceleration, whose momentum-based acceleration interferes with the discontinuous Schulman support function and caused convergence failures for shapes with continuous support (Sphere, Cylinder, Capsule). Also keeps distance_upper_bound=max for CastHullShape to prevent GJK early stopping on large swept volumes. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
When the Schulman support function computes equal projections for start and end support points (common for shapes with continuous support like Sphere with pure translational sweep), always returning supportStart caused GJK simplex degeneration - the same vertex was returned for many nearby directions, preventing convergence. Fix: return the average of start/end support points when projections are within epsilon. This is geometrically valid (midpoint lies on the convex hull boundary with the same max projection) and gives GJK distinct vertices for different directions. Also improve the initial GJK guess to use the center difference between shapes instead of a hardcoded (1,0,0) axis. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Add descriptive failure messages to all EXPECT_*/ASSERT_* calls in the box-box and sphere-sphere cast collision test suites. When a test fails, the output now explains: - Which scenario is running (via SCOPED_TRACE with ContactTestType) - Full contact result state dump (distance, normal, nearest_points, cc_time, cc_type) for context on any failure - What each expected value represents geometrically (e.g., "static_box surface at x=-0.5", "collision at 25% of sweep") - Which link is in which result slot when ordering varies Also changes bare EXPECT_TRUE(empty) to ASSERT_FALSE(empty) with messages explaining the geometry that should produce a collision, so missing-contact failures abort early with actionable diagnostics instead of crashing on result_vector[0] access. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
castHullCollide: Log GJK status (Valid/Failed/EarlyStopped/DidNotRun), distance, shape types, and transforms when no collision is found. This helps diagnose why primitive sphere-sphere CastHullShape pairs fail to detect collisions. populateContinuousCollisionFields: Replace Euclidean distance-based cc_time interpolation with projection along the support point sweep trajectory (pt_world0 → pt_world1). The projection is more robust because it isolates motion along the sweep direction and avoids skew from off-axis components of convex mesh support vertices. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Coal's GJK::Status enum uses NoCollision (not Valid) for the converged-but-separated case. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Three fixes for CastHullShape continuous collision detection: 1. Use WithSweptSphere support mode in GJK and cc_time computation: Coal's getSupport<NoSweptSphere> returns zero for primitive shapes (Sphere, Capsule, etc.) because coal treats them as "point + swept sphere radius". Our custom GJK bypasses coal's swept-sphere handling, so we must use WithSweptSphere to include the shape's radius in the support point. This fixes primitive sphere-sphere returning zero contacts. 2. Use center-based cc_time interpolation: Previously cc_time was computed by projecting the contact point onto the support vertex trajectory. For tessellated meshes, the support vertex has off-axis components (e.g., (0.237, -0.077, 0.25) instead of ideal (0.25, 0, 0)), causing cc_time skew. Using the shape center trajectory instead eliminates this tessellation artifact. 3. Project nearest_points_local onto contact normal: The averaged support point used for nearest_points_local had off-axis components from mesh tessellation. Projecting the support distance onto the contact normal direction gives the correct surface point along the normal, matching Bullet's behavior. https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
- Move non-runtime deps (bullet, fcl, yaml-cpp, cereal, tesseract_support) to <test_depend> in package.xml - Extract applyCollisionFilterMask() helper replacing 3 copies of filter mask assignment logic - Define kTransformEpsilon constant and transformChanged() helper replacing hardcoded 1e-8 checks across both managers - Extract collectTransformUpdate() and flushBatchUpdate() private methods to deduplicate setCollisionObjectsTransform overloads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CoalCollisionGeometryCache uses weak_ptr entries that expire when geometries are destroyed, but stale entries were never cleaned up. Call prune() in both discrete and cast manager clone() methods to prevent unbounded cache growth across manager lifecycles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ckends The -gjk_tolerance threshold caused Coal to miss near-zero-distance contacts (e.g. swept volume grazing) that Bullet and FCL report. Other backends do not apply this correction, so Coal shouldn't either. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the static octree voxel expansion workaround now that Coal has native GEOM_CUSTOM <-> GEOM_OCTREE narrowphase support. Static octrees register as raw OcTree in the static broadphase instead of thousands of CastHullShape-wrapped boxes. This eliminates hasNonShapeBaseGeometry(), the dual-path routing, and the identity-transform reset on demotion. Change the default m_collisionFilterGroup from KinematicFilter to StaticFilter so newly added objects go directly to the static broadphase without a wasted round-trip through dynamic. Add swapObjects() compensation for Coal's ShapeOcTreeCollisionTraversalNode which internally swaps arguments without fixing Contact b1/b2 ordering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove CastHullShape-specific GJK branching in CollisionCallback::collide() and use NesterovAcceleration uniformly. This eliminates the dynamic_cast detection, the separate CachedGuess path, and the unbounded distance_upper_bound override — all collision pairs now use BoundingVolumeGuess with DualityGap convergence. NesterovAcceleration produces contact points matching Bullet on tessellated convex hulls. Add coal_vs_bullet_convex_cast_unit test to validate Coal cast results against the Bullet backend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract removeObjects and collectCastTransformUpdate helpers to align discrete and cast manager structure. Replace collision_objects_raw_ vector with appendCollisionObjectsRaw() to eliminate redundant parallel storage. Use invalidateCacheFor() helper instead of inline cache iteration. Move removeObjects from public to private in cast manager. Remove unused CollisionObjectConstPtr and Link2ConstCOW type aliases and sameObject() function. Fix broadphase manager doxygen comments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ects Move function bodies from coal_utils.h to coal_utils.cpp to reduce header weight and compilation coupling. Refactor removeObjects in both managers to use direct filter-group dispatch instead of constructing unordered_sets from broadphase contents. In the cast manager, replace the (filter_group, is_cast) parameters with a BroadPhaseCollisionManager reference, guarding at the call site. Pass pre-computed inverse transforms into populateContinuousCollisionFields to avoid redundant computation across contacts. Fix "CompundMesh" typo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unify NesterovAcceleration + DualityGap/Relative for both cast and discrete pairs (previously discrete used Absolute). Enable GJK warm-start caching for all pairs, not just cast. Add cache invalidation in enable/disableCollisionObject to prevent stale guesses after object state changes. Extract shared setCollisionObjectEnabled helper in both managers. Also adds coal_cast_gradient_quality_unit test and uses dynamic_cast<CastHullShape> instead of GEOM_CUSTOM check for future-proofing against other custom shapes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 tasks
…CastHullShape CastHullShape now explicitly overrides getNodeType() to return GEOM_CUSTOM (previously relied on a ShapeBase default that has been removed) and delegates needNesterovNormalizeHeuristic() to the underlying shape via Coal's getNormalizeSupportDirection, using shape_traits as the single source of truth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace full cache erasure with lazy GJK re-seeding for enable/disable and transform updates. Cache entries now use a CollisionCacheEntry struct with a gjk_guess_valid flag that is cleared on invalidation and re-seeded in collide() where actual transforms are available. Full erasure (invalidateCacheFor) is reserved for object removal. This preserves the ComputeCollision functor and solver settings across transform updates, avoiding repeated dynamic_cast and functor construction. Multi-link batch updates accumulate affected pointers into a CollisionObjectPtrSet and scan the cache once, replacing the previous collision_cache.clear(). Other changes: - CastHullShape::computeLocalAABB uses computeBV<AABB, ShapeBase> - CastHullShape cached members (shapeAABB_, shapeCenter_, shapeHalfExtents_) removed in favor of Coal utility - invalidateCacheFor deduplicated via CollisionObjectPtrSet overload - Transform3s::inverse() now callable on const (Coal fork change) - CLAUDE.md updated to reflect lazy re-seeding architecture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s Coal cast collision checking is now fully functional.
rjoomen
force-pushed
the
claude/update-review-md-8p8Ek
branch
from
March 27, 2026 22:12
d975f41 to
afeaf5f
Compare
The old approach scanned the entire collision cache to mark entries invalid on every transform update or enable/disable — O(N*M) per batch. Replace with a per-COW gjk_generation_ counter: collide() compares cached generation stamps and re-seeds only on mismatch. This eliminates all invalidateCachedGJKGuessFor() functions and the CollisionObjectPtrSet type. Also makes the GJK guess threshold configurable via plugin YAML (gjk_guess_threshold key, default 5mm) instead of hardcoded, and adds a unit test for threshold configuration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Static octrees no longer eagerly expand into per-voxel CastHullShape boxes in makeCastCollisionObject. Instead the raw OcTree is kept in link2castcow_ and expanded only when promoted to active (kinematic). Once expanded the cast COW is cached so re-promotion is free. Also optimizes the expansion loop: pre-reserves vectors using tree->size() and uses the leaf iterator API directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add configurable arc-sagitta swept-sphere inflation for rotational motions in continuous collision detection. When enabled via the d_arc_compensation YAML config key, CastHullShape swept sphere radii are inflated to account for the sagitta of rotational arcs, preventing missed collisions on fast-rotating links. Hoist ContactResultMap lookup out of the per-contact loop in CollisionCallback::collide — the find only needs to run once per collision pair rather than once per contact. Update CLAUDE.md with expanded architecture documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adopt the Bullet pattern: store a persistent ContactTestDataWrapper member on both Coal managers instead of constructing a fresh one per contactTest() call. This avoids copying CollisionMarginData (which contains an unordered_map of per-pair margins) on every call — measured at 1.8% CPU reduction in hvr_planning_se perf profiling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CastHullShape::computeShapeSupport called getSupport(shape, dir, hint) which created a fresh ShapeSupportData every call, forcing getShapeSupportLog to reallocate its visited vector on every support query. Store two persistent ShapeSupportData members (one per pose) and pass them to the new getSupport overload. The visited vector is now allocated once and reused via std::fill on subsequent calls. Measured: getShapeSupportLog self-time dropped from 2.65% to 2.04%. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When half or more of the dynamic collision objects changed, use Coal's no-arg update() (O(n) bottom-up refit) instead of per-object remove+reinsert (O(k*log n)). In trajectory optimization nearly all kinematic objects move each step, making the refit path faster. Measured: insertLeaf+removeLeaf dropped from 1.50% to 0.75% CPU. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ates Remove redundant per-COW setCollisionObjectsTransform and setContactDistanceThreshold calls in CoalCastBVHManager::clone(). COW::clone() already copies transforms and thresholds, and setCollisionMarginData called afterwards re-applies them. Add full-refit threshold to static_manager_ in flushBatchUpdate for both discrete and cast managers, matching the existing dynamic_manager_ logic. When most static objects changed (e.g. margin update), a full refit is O(n) vs O(k*log n) for per-object remove+reinsert.
69 tasks
Preserve clean rotation matrices by avoiding pose * local multiplication when the local rotation is identity so axis alignment stays exact.
Bulk-add collision objects via registerObjects() in clone() to build balanced broadphase trees instead of inserting one-by-one. Extract updateBroadphaseAndCache() to consolidate repeated update+reserve logic. Skip redundant margin threshold updates when value unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This fully implements continuous collision checking using Coal. It needs an updated version of Coal with custom geometry support, PR: coal-library/coal#822.
For benchmark results, see tesseract-robotics/tesseract_collision_benchmarks#5