From 0c05e341a5e22a369a0b8d24f74a05a507e1d14b Mon Sep 17 00:00:00 2001 From: Erin Catto Date: Sat, 28 Sep 2024 23:30:11 -0700 Subject: [PATCH 1/2] fix missing hit events add test for null pair task add manifold to contact begin events --- include/box2d/box2d.h | 7 ++ include/box2d/collision.h | 5 +- include/box2d/types.h | 3 + samples/sample_continuous.cpp | 132 ++++++++++++++++------------------ samples/sample_stacking.cpp | 51 ++++++++++++- src/broad_phase.c | 7 +- src/core.h | 2 +- src/solver.c | 4 +- src/world.c | 19 ++++- 9 files changed, 148 insertions(+), 82 deletions(-) diff --git a/include/box2d/box2d.h b/include/box2d/box2d.h index 8d7634be1..f1dfa75df 100644 --- a/include/box2d/box2d.h +++ b/include/box2d/box2d.h @@ -156,6 +156,13 @@ B2_API void b2World_Explode( b2WorldId worldId, b2Vec2 position, float radius, f /// @note Advanced feature B2_API void b2World_SetContactTuning( b2WorldId worldId, float hertz, float dampingRatio, float pushVelocity ); +/// Adjust joint tuning parameters +/// @param worldId The world id +/// @param hertz The contact stiffness (cycles per second) +/// @param dampingRatio The contact bounciness with 1 being critical damping (non-dimensional) +/// @note Advanced feature +B2_API void b2World_SetJointTuning( b2WorldId worldId, float hertz, float dampingRatio ); + /// Enable/disable constraint warm starting. Advanced feature for testing. Disabling /// sleeping greatly reduces stability and provides no performance gain. B2_API void b2World_EnableWarmStarting( b2WorldId worldId, bool flag ); diff --git a/include/box2d/collision.h b/include/box2d/collision.h index 4b296c0b3..6c2c1628c 100644 --- a/include/box2d/collision.h +++ b/include/box2d/collision.h @@ -488,10 +488,11 @@ typedef struct b2ManifoldPoint b2Vec2 point; /// Location of the contact point relative to bodyA's origin in world space - /// @note When used internally to the Box2D solver, these are relative to the center of mass. + /// @note When used internally to the Box2D solver, this is relative to the center of mass. b2Vec2 anchorA; /// Location of the contact point relative to bodyB's origin in world space + /// @note When used internally to the Box2D solver, this is relative to the center of mass. b2Vec2 anchorB; /// The separation of the contact point, negative if penetrating @@ -504,7 +505,7 @@ typedef struct b2ManifoldPoint float tangentImpulse; /// The maximum normal impulse applied during sub-stepping - /// todo not sure this is needed + /// This could be a bool to indicate the point is confirmed (may be a speculative point) float maxNormalImpulse; /// Relative normal velocity pre-solve. Used for hit events. If the normal impulse is diff --git a/include/box2d/types.h b/include/box2d/types.h index 2bec968ba..b71877914 100644 --- a/include/box2d/types.h +++ b/include/box2d/types.h @@ -928,6 +928,9 @@ typedef struct b2ContactBeginTouchEvent /// Id of the second shape b2ShapeId shapeIdB; + + /// The initial contact manifold + b2Manifold manifold; } b2ContactBeginTouchEvent; /// An end touch event is generated when two shapes stop touching. diff --git a/samples/sample_continuous.cpp b/samples/sample_continuous.cpp index ac35e07c8..705f7f5d8 100644 --- a/samples/sample_continuous.cpp +++ b/samples/sample_continuous.cpp @@ -11,14 +11,6 @@ #include #include -// This tests continuous collision robustness and also demonstrates the speed limits imposed -// by b2_maxTranslation and b2_maxRotation. -struct HitEvent -{ - b2Vec2 point; - float speed; - int stepIndex; -}; class BounceHouse : public Sample { @@ -30,6 +22,13 @@ class BounceHouse : public Sample e_boxShape }; + struct HitEvent + { + b2Vec2 point; + float speed; + int stepIndex; + }; + explicit BounceHouse( Settings& settings ) : Sample( settings ) { @@ -389,64 +388,8 @@ class SkinnyBox : public Sample static int sampleSkinnyBox = RegisterSample( "Continuous", "Skinny Box", SkinnyBox::Create ); -class SpeculativeBug : public Sample -{ -public: - explicit SpeculativeBug( Settings& settings ) - : Sample( settings ) - { - if ( settings.restart == false ) - { - g_camera.m_center = { 1.0f, 5.0f }; - g_camera.m_zoom = 25.0f * 0.25f; - } - - { - b2BodyDef bodyDef = b2DefaultBodyDef(); - b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); - - b2Segment segment = { { -10.0f, 0.0f }, { 10.0f, 0.0f } }; - b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2CreateSegmentShape( groundId, &shapeDef, &segment ); - - shapeDef.friction = 0.0f; - b2Polygon box = b2MakeOffsetBox( 0.05f, 1.0f, { 0.0f, 1.0f }, b2Rot_identity ); - b2CreatePolygonShape( groundId, &shapeDef, &box ); - } - - b2BodyDef bodyDef = b2DefaultBodyDef(); - bodyDef.type = b2_dynamicBody; - for (int i = 0; i < 2; ++i) - { - if (i == 0) - { - bodyDef.position = { -0.8f, 0.25f }; - bodyDef.isAwake = false; - } - else - { - bodyDef.position = { 0.8f, 2.0f }; - bodyDef.isAwake = true; - } - - b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); - - b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2Capsule capsule = { { -0.5f, 0.0f }, { 0.5f, 0.0f }, 0.25f }; - b2CreateCapsuleShape( bodyId, &shapeDef, &capsule ); - } - } - - static Sample* Create( Settings& settings ) - { - return new SpeculativeBug( settings ); - } -}; - -static int sampleSpeculativeBug = RegisterSample( "Continuous", "Speculative Bug", SpeculativeBug::Create ); - -// This sample shows ghost collisions -class GhostCollision : public Sample +// This sample shows ghost bumps +class GhostBumps : public Sample { public: enum ShapeType @@ -456,7 +399,7 @@ class GhostCollision : public Sample e_boxShape }; - explicit GhostCollision( Settings& settings ) + explicit GhostBumps( Settings& settings ) : Sample( settings ) { if ( settings.restart == false ) @@ -671,7 +614,7 @@ class GhostCollision : public Sample ImGui::SetNextWindowPos( ImVec2( 10.0f, g_camera.m_height - height - 50.0f ), ImGuiCond_Once ); ImGui::SetNextWindowSize( ImVec2( 180.0f, height ) ); - ImGui::Begin( "Ghost Collision", nullptr, ImGuiWindowFlags_NoResize ); + ImGui::Begin( "Ghost Bumps", nullptr, ImGuiWindowFlags_NoResize ); ImGui::PushItemWidth( 100.0f ); if ( ImGui::Checkbox( "Chain", &m_useChain ) ) @@ -720,7 +663,7 @@ class GhostCollision : public Sample static Sample* Create( Settings& settings ) { - return new GhostCollision( settings ); + return new GhostBumps( settings ); } b2BodyId m_groundId; @@ -733,7 +676,7 @@ class GhostCollision : public Sample bool m_useChain; }; -static int sampleGhostCollision = RegisterSample( "Continuous", "Ghost Collision", GhostCollision::Create ); +static int sampleGhostCollision = RegisterSample( "Continuous", "Ghost Collision", GhostBumps::Create ); // Speculative collision failure case suggested by Dirk Gregorius. This uses // a simple fallback scheme to prevent tunneling. @@ -786,6 +729,55 @@ class SpeculativeFallback : public Sample static int sampleSpeculativeFallback = RegisterSample( "Continuous", "Speculative Fallback", SpeculativeFallback::Create ); +// This shows that while Box2D uses speculative collision, it does not lead to speculative ghost collisions at small distances +class SpeculativeGhost : public Sample +{ +public: + explicit SpeculativeGhost( Settings& settings ) + : Sample( settings ) + { + if ( settings.restart == false ) + { + g_camera.m_center = { 0.0f, 1.75f }; + g_camera.m_zoom = 2.0f; + } + + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); + + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2Segment segment = { { -10.0f, 0.0f }, { 10.0f, 0.0f } }; + b2CreateSegmentShape( groundId, &shapeDef, &segment ); + + b2Polygon box = b2MakeOffsetBox( 1.0f, 0.1f, { 0.0f, 0.9f }, b2Rot_identity ); + b2CreatePolygonShape( groundId, &shapeDef, &box ); + } + + { + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + + // The speculative distance is 0.02 meters, so this avoid it + bodyDef.position = { 0.015f, 2.515f }; + bodyDef.linearVelocity = { 0.1f * 1.25f * settings.hertz, -0.1f * 1.25f * settings.hertz }; + bodyDef.gravityScale = 0.0f; + b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); + + b2ShapeDef shapeDef = b2DefaultShapeDef(); + b2Polygon box = b2MakeSquare( 0.25f ); + b2CreatePolygonShape( bodyId, &shapeDef, &box ); + } + } + + static Sample* Create( Settings& settings ) + { + return new SpeculativeGhost( settings ); + } +}; + +static int sampleSpeculativeGhost = RegisterSample( "Continuous", "Speculative Ghost", SpeculativeGhost::Create ); + // This shows a fast moving body that uses continuous collision versus static and dynamic bodies. // This is achieved by setting the ball body as a *bullet*. class Pinball : public Sample diff --git a/samples/sample_stacking.cpp b/samples/sample_stacking.cpp index b0e39edda..0323e89b5 100644 --- a/samples/sample_stacking.cpp +++ b/samples/sample_stacking.cpp @@ -383,10 +383,16 @@ class VerticalStack : public Sample static int sampleVerticalStack = RegisterSample( "Stacking", "Vertical Stack", VerticalStack::Create ); -// This shows how to handle high gravity and small shapes using a small time step +// A simple circle stack that also shows how to collect hit events class CircleStack : public Sample { public: + + struct Event + { + int indexA, indexB; + }; + explicit CircleStack( Settings& settings ) : Sample( settings ) { @@ -396,11 +402,16 @@ class CircleStack : public Sample g_camera.m_zoom = 6.0f; } + int shapeIndex = 0; + { b2BodyDef bodyDef = b2DefaultBodyDef(); b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.userData = reinterpret_cast( intptr_t( shapeIndex ) ); + shapeIndex += 1; + b2Segment segment = { { -10.0f, 0.0f }, { 10.0f, 0.0f } }; b2CreateSegmentShape( groundId, &shapeDef, &segment ); } @@ -409,9 +420,11 @@ class CircleStack : public Sample b2World_SetContactTuning( m_worldId, 0.25f * 360.0f, 10.0f, 3.0f ); b2Circle circle = {}; - circle.radius = 0.5f; + circle.radius = 0.25f; b2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.enableHitEvents = true; + b2BodyDef bodyDef = b2DefaultBodyDef(); bodyDef.type = b2_dynamicBody; @@ -422,9 +435,39 @@ class CircleStack : public Sample bodyDef.position.y = y; b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); + + shapeDef.userData = reinterpret_cast( intptr_t( shapeIndex ) ); + shapeIndex += 1; b2CreateCircleShape( bodyId, &shapeDef, &circle ); - y += 1.0f; + y += 2.0f; + } + } + + void Step( Settings& settings ) override + { + Sample::Step( settings ); + + b2ContactEvents events = b2World_GetContactEvents( m_worldId ); + for ( int i = 0; i < events.hitCount; ++i ) + { + b2ContactHitEvent* event = events.hitEvents + i; + + void* userDataA = b2Shape_GetUserData( event->shapeIdA ); + void* userDataB = b2Shape_GetUserData( event->shapeIdB ); + int indexA = static_cast( reinterpret_cast( userDataA ) ); + int indexB = static_cast( reinterpret_cast( userDataB ) ); + + g_draw.DrawPoint( event->point, 10.0f, b2_colorWhite ); + + m_events.push_back( { indexA, indexB } ); + } + + int eventCount = m_events.size(); + for (int i = 0; i < eventCount; ++i) + { + g_draw.DrawString( 5, m_textLine, "%d, %d", m_events[i].indexA, m_events[i].indexB ); + m_textLine += m_textIncrement; } } @@ -432,6 +475,8 @@ class CircleStack : public Sample { return new CircleStack( settings ); } + + std::vector m_events; }; static int sampleCircleStack = RegisterSample( "Stacking", "Circle Stack", CircleStack::Create ); diff --git a/src/broad_phase.c b/src/broad_phase.c index 8043ea13d..7502c2f9f 100644 --- a/src/broad_phase.c +++ b/src/broad_phase.c @@ -360,8 +360,11 @@ void b2UpdateBroadPhasePairs( b2World* world ) int minRange = 64; void* userPairTask = world->enqueueTaskFcn( &b2FindPairsTask, moveCount, minRange, world, world->userTaskContext ); - world->finishTaskFcn( userPairTask, world->userTaskContext ); - world->taskCount += 1; + if (userPairTask != NULL) + { + world->finishTaskFcn( userPairTask, world->userTaskContext ); + world->taskCount += 1; + } b2TracyCZoneNC( create_contacts, "Create Contacts", b2_colorGold, true ); diff --git a/src/core.h b/src/core.h index 283a22429..ea5148cc9 100644 --- a/src/core.h +++ b/src/core.h @@ -139,7 +139,7 @@ extern float b2_lengthUnitsPerMeter; #define b2_maxWorkers 64 // Maximum number of colors in the constraint graph. Constraints that cannot -// find a color are added to the overflow set which are solved single-threaded. +// find a color are added to the overflow set which are solved single-threaded. #define b2_graphColorCount 12 // A small length used as a collision and constraint tolerance. Usually it is diff --git a/src/solver.c b/src/solver.c index 28a4c2b25..bd1a81eef 100644 --- a/src/solver.c +++ b/src/solver.c @@ -1721,7 +1721,9 @@ void b2Solve( b2World* world, b2StepContext* stepContext ) { b2ManifoldPoint* mp = contactSim->manifold.points + k; float approachSpeed = -mp->normalVelocity; - if ( approachSpeed > event.approachSpeed && mp->normalImpulse > 0.0f ) + + // Need to check max impulse because the point may be speculative and not colliding + if ( approachSpeed > event.approachSpeed && mp->maxNormalImpulse > 0.0f ) { event.approachSpeed = approachSpeed; event.point = mp->point; diff --git a/src/world.c b/src/world.c index 53d462dac..a4160ec77 100644 --- a/src/world.c +++ b/src/world.c @@ -587,7 +587,7 @@ static void b2Collide( b2StepContext* context ) // Contact is solid if ( flags & b2_contactEnableContactEvents ) { - b2ContactBeginTouchEvent event = { shapeIdA, shapeIdB }; + b2ContactBeginTouchEvent event = { shapeIdA, shapeIdB, contactSim->manifold }; b2ContactBeginTouchEventArray_Push( &world->contactBeginEvents, event ); } @@ -1670,7 +1670,7 @@ float b2World_GetHitEventThreshold( b2WorldId worldId ) return world->hitEventThreshold; } -void b2World_SetContactTuning( b2WorldId worldId, float hertz, float dampingRatio, float pushOut ) +void b2World_SetContactTuning( b2WorldId worldId, float hertz, float dampingRatio, float pushVelocity ) { b2World* world = b2GetWorldFromId( worldId ); B2_ASSERT( world->locked == false ); @@ -1681,7 +1681,20 @@ void b2World_SetContactTuning( b2WorldId worldId, float hertz, float dampingRati world->contactHertz = b2ClampFloat( hertz, 0.0f, FLT_MAX ); world->contactDampingRatio = b2ClampFloat( dampingRatio, 0.0f, FLT_MAX ); - world->contactPushoutVelocity = b2ClampFloat( pushOut, 0.0f, FLT_MAX ); + world->contactPushoutVelocity = b2ClampFloat( pushVelocity, 0.0f, FLT_MAX ); +} + +void b2World_SetJointTuning(b2WorldId worldId, float hertz, float dampingRatio) +{ + b2World* world = b2GetWorldFromId( worldId ); + B2_ASSERT( world->locked == false ); + if ( world->locked ) + { + return; + } + + world->jointHertz = b2ClampFloat( hertz, 0.0f, FLT_MAX ); + world->jointDampingRatio = b2ClampFloat( dampingRatio, 0.0f, FLT_MAX ); } b2Profile b2World_GetProfile( b2WorldId worldId ) From 596fbce68ca47447aabdf3aed37944252851ce38 Mon Sep 17 00:00:00 2001 From: Erin Catto Date: Sun, 29 Sep 2024 09:42:11 -0700 Subject: [PATCH 2/2] typo --- samples/sample_continuous.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/sample_continuous.cpp b/samples/sample_continuous.cpp index 705f7f5d8..75b79b7c2 100644 --- a/samples/sample_continuous.cpp +++ b/samples/sample_continuous.cpp @@ -676,7 +676,7 @@ class GhostBumps : public Sample bool m_useChain; }; -static int sampleGhostCollision = RegisterSample( "Continuous", "Ghost Collision", GhostBumps::Create ); +static int sampleGhostCollision = RegisterSample( "Continuous", "Ghost Bumps", GhostBumps::Create ); // Speculative collision failure case suggested by Dirk Gregorius. This uses // a simple fallback scheme to prevent tunneling.