Skip to content

Commit

Permalink
Fix missing hit events (#808)
Browse files Browse the repository at this point in the history
add test for null pair task
add manifold to contact begin events

#803 #806
  • Loading branch information
erincatto authored Sep 29, 2024
1 parent df7373c commit 67b9835
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 82 deletions.
7 changes: 7 additions & 0 deletions include/box2d/box2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
5 changes: 3 additions & 2 deletions include/box2d/collision.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions include/box2d/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
132 changes: 62 additions & 70 deletions samples/sample_continuous.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@
#include <GLFW/glfw3.h>
#include <imgui.h>

// 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
{
Expand All @@ -30,6 +22,13 @@ class BounceHouse : public Sample
e_boxShape
};

struct HitEvent
{
b2Vec2 point;
float speed;
int stepIndex;
};

explicit BounceHouse( Settings& settings )
: Sample( settings )
{
Expand Down Expand Up @@ -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
Expand All @@ -456,7 +399,7 @@ class GhostCollision : public Sample
e_boxShape
};

explicit GhostCollision( Settings& settings )
explicit GhostBumps( Settings& settings )
: Sample( settings )
{
if ( settings.restart == false )
Expand Down Expand Up @@ -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 ) )
Expand Down Expand Up @@ -720,7 +663,7 @@ class GhostCollision : public Sample

static Sample* Create( Settings& settings )
{
return new GhostCollision( settings );
return new GhostBumps( settings );
}

b2BodyId m_groundId;
Expand All @@ -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 Bumps", GhostBumps::Create );

// Speculative collision failure case suggested by Dirk Gregorius. This uses
// a simple fallback scheme to prevent tunneling.
Expand Down Expand Up @@ -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
Expand Down
51 changes: 48 additions & 3 deletions samples/sample_stacking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
{
Expand All @@ -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<void*>( intptr_t( shapeIndex ) );
shapeIndex += 1;

b2Segment segment = { { -10.0f, 0.0f }, { 10.0f, 0.0f } };
b2CreateSegmentShape( groundId, &shapeDef, &segment );
}
Expand All @@ -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;

Expand All @@ -422,16 +435,48 @@ class CircleStack : public Sample
bodyDef.position.y = y;

b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef );

shapeDef.userData = reinterpret_cast<void*>( 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<int>( reinterpret_cast<intptr_t>( userDataA ) );
int indexB = static_cast<int>( reinterpret_cast<intptr_t>( 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;
}
}

static Sample* Create( Settings& settings )
{
return new CircleStack( settings );
}

std::vector<Event> m_events;
};

static int sampleCircleStack = RegisterSample( "Stacking", "Circle Stack", CircleStack::Create );
Expand Down
7 changes: 5 additions & 2 deletions src/broad_phase.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

Expand Down
2 changes: 1 addition & 1 deletion src/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/solver.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 67b9835

Please sign in to comment.