Skip to content

Commit 617d32a

Browse files
authored
Tunable ccd (#1093)
- Added `b2BodyDef::safetyFactor` to allow continuous collision to engage at lower speeds. - Contact recycling is now disabled on fast bodies. - Implements #1040
1 parent d710ba7 commit 617d32a

19 files changed

Lines changed: 404 additions & 45 deletions

include/box2d/box2d.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,12 @@ B2_API void b2Body_SetSleepThreshold( b2BodyId bodyId, float sleepThreshold );
566566
/// Get the sleep threshold, usually in meters per second.
567567
B2_API float b2Body_GetSleepThreshold( b2BodyId bodyId );
568568

569+
/// Set the continuous collision safety factor. Smaller is safer but can lead to hitching. Recommended range [0.01, 0.5]. Non-dimensional.
570+
B2_API void b2Body_SetSafetyFactor( b2BodyId bodyId, float safetyFactor );
571+
572+
/// Get the continuous collision safety factor. Non-dimensional.
573+
B2_API float b2Body_GetSafetyFactor( b2BodyId bodyId );
574+
569575
/// Returns true if this body is enabled
570576
B2_API bool b2Body_IsEnabled( b2BodyId bodyId );
571577

include/box2d/types.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ typedef struct b2BodyDef
237237
/// Sleep speed threshold, default is 0.05 meters per second
238238
float sleepThreshold;
239239

240+
/// Continuous collision safety factor. The solver only uses continuous collision if there is a
241+
/// risk of tunneling. If the body is moving fast enough to risk tunneling then it is considered a "fast body".
242+
/// This improves performance and prevents movement hitches. If a body moving N meter risks tunneling, then the
243+
/// body will be considered fast if it moves more than a safetyFactor times N meters over one full time step.
244+
/// Non-dimensional. Recommended range [0.01, 0.5]. Default is 0.5 for high performance with low tunneling risk.
245+
float safetyFactor;
246+
240247
/// Optional body name for debugging. Up to B2_NAME_LENGTH characters
241248
const char* name;
242249

samples/dynamic_mover.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ bool DynamicMover::Jump()
174174
}
175175

176176
float surfaceVelocity = 0.0f;
177-
if (b2Body_IsValid(m_castResult.bodyId))
177+
if ( b2Body_IsValid( m_castResult.bodyId ) )
178178
{
179179
b2Vec2 v = b2Body_GetWorldPointVelocity( m_castResult.bodyId, m_castResult.point );
180180
surfaceVelocity = v.y;
@@ -243,15 +243,15 @@ void DynamicMover::Update( float timeStep, float throttle )
243243
m_onGround = m_jumping == false;
244244

245245
// Is the ground too steep to walk?
246-
m_walkable = m_castResult.normal.y > m_minGroundNormalY;
246+
m_walkable = m_castResult.normal.y >= m_minGroundNormalY;
247247
}
248248
else
249249
{
250250
m_onGround = false;
251251
m_walkable = false;
252252
}
253253

254-
m_velocity = b2Body_GetLinearVelocity( m_moverId );
254+
m_velocity = b2Body_GetLinearVelocity( m_moverId );
255255

256256
// Friction
257257
if ( m_onGround )

samples/sample_character.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,8 @@ class DynamicMoverSample : public Sample
756756
DrawScreenTextLine( "pogo: impulse %.3f, velocity %.3f", m_mover.m_pogoImpulse, m_mover.m_pogoVelocity );
757757
DrawScreenTextLine( "pogo: length = %.2f/%.2f", m_mover.m_pogoLength, m_mover.m_pogoRestLength );
758758
DrawScreenTextLine( "on ground: %d", (int)m_mover.m_onGround );
759-
DrawScreenTextLine( "walkable: %d", (int)m_mover.m_walkable );
759+
float normalY = m_mover.m_castResult.hit ? m_mover.m_castResult.normal.y : 0.0f;
760+
DrawScreenTextLine( "walkable: %d, normal y: %.3f/%.3f", (int)m_mover.m_walkable, normalY, m_mover.m_minGroundNormalY );
760761
DrawScreenTextLine( "jumping/ticks: %d/%d", (int)m_mover.m_jumping, m_mover.m_jumpTicks );
761762

762763
if ( m_lockCamera )

samples/sample_continuous.cpp

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,3 +1756,121 @@ class Wedge : public Sample
17561756
};
17571757

17581758
static int sampleWedge = RegisterSample( "Continuous", "Wedge", Wedge::Create );
1759+
1760+
// This shows how adjusting the CCD safety factor can engage continuous collision at
1761+
// lower speeds to avoid the slight overlap that happens with discrete collision detection.
1762+
// The best way to run this sample is:
1763+
// 1. press pause (P)
1764+
// 2. restart (R) or press the Drop button
1765+
// 3. then single step (O)
1766+
// Then look at the metrics and overlap. The color of the shape gets darker when it is using
1767+
// continuous collision detection.
1768+
class SafetyFactor : public Sample
1769+
{
1770+
public:
1771+
explicit SafetyFactor( SampleContext* context )
1772+
: Sample( context )
1773+
{
1774+
if ( m_context->restart == false )
1775+
{
1776+
m_context->camera.center = { 0.0f, 3.0f };
1777+
m_context->camera.zoom = 6.0f;
1778+
}
1779+
1780+
{
1781+
b2BodyDef bodyDef = b2DefaultBodyDef();
1782+
bodyDef.position = { 0.0f, -1.0f };
1783+
b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef );
1784+
1785+
b2ShapeDef shapeDef = b2DefaultShapeDef();
1786+
b2Polygon box = b2MakeBox( 20.0f, 1.0f );
1787+
b2CreatePolygonShape( groundId, &shapeDef, &box );
1788+
}
1789+
1790+
m_bodyId = b2_nullBodyId;
1791+
m_extent = 0.5f;
1792+
m_height = 0.3f;
1793+
m_safetyFactor = 0.1f;
1794+
m_overlap = 0.0f;
1795+
1796+
Launch();
1797+
}
1798+
1799+
void Launch()
1800+
{
1801+
if ( B2_IS_NON_NULL( m_bodyId ) )
1802+
{
1803+
b2DestroyBody( m_bodyId );
1804+
}
1805+
1806+
b2BodyDef bodyDef = b2DefaultBodyDef();
1807+
bodyDef.type = b2_dynamicBody;
1808+
bodyDef.position = { 0.0f, m_height + m_extent };
1809+
bodyDef.safetyFactor = m_safetyFactor;
1810+
m_bodyId = b2CreateBody( m_worldId, &bodyDef );
1811+
1812+
b2ShapeDef shapeDef = b2DefaultShapeDef();
1813+
b2Polygon box = b2MakeSquare( m_extent );
1814+
b2CreatePolygonShape( m_bodyId, &shapeDef, &box );
1815+
1816+
m_overlap = 0.0f;
1817+
}
1818+
1819+
void Step() override
1820+
{
1821+
Sample::Step();
1822+
1823+
if ( m_didStep )
1824+
{
1825+
// Contacts are computed at the beginning of the step, so the manifold holds the initial separation.
1826+
b2ContactData data;
1827+
int count = b2Body_GetContactData( m_bodyId, &data, 1 );
1828+
if ( count == 1 )
1829+
{
1830+
for ( int j = 0; j < data.manifold.pointCount; ++j )
1831+
{
1832+
m_overlap = b2MaxFloat( m_overlap, -data.manifold.points[j].separation );
1833+
}
1834+
}
1835+
}
1836+
1837+
DrawLine( m_draw, b2ToPos( b2Vec2{ -3.0f, -m_overlap } ), b2ToPos( b2Vec2{ 3.0f, -m_overlap } ), b2_colorRed );
1838+
1839+
float timeStep = m_context->hertz > 0.0f ? 1.0f / m_context->hertz : 0.0f;
1840+
float motion = timeStep * b2Length( b2Body_GetLinearVelocity( m_bodyId ) );
1841+
1842+
DrawScreenTextLine( "actual movement = %.3f m", motion );
1843+
DrawScreenTextLine( "fast movement = %.3f m", m_safetyFactor * m_extent );
1844+
DrawScreenTextLine( "overlap = %.4f m", m_overlap );
1845+
}
1846+
1847+
bool DrawControls() override
1848+
{
1849+
bool changed = false;
1850+
1851+
ImGui::PushItemWidth( 10.0f * ImGui::GetFontSize() );
1852+
changed |= ImGui::SliderFloat( "Height", &m_height, 0.0f, 2.0f, "%.2f" );
1853+
changed |= ImGui::SliderFloat( "Safety", &m_safetyFactor, 0.0f, 1.0f, "%.2f" );
1854+
ImGui::PopItemWidth();
1855+
1856+
if ( ImGui::Button( "Drop" ) || changed )
1857+
{
1858+
Launch();
1859+
}
1860+
1861+
return true;
1862+
}
1863+
1864+
static Sample* Create( SampleContext* context )
1865+
{
1866+
return new SafetyFactor( context );
1867+
}
1868+
1869+
b2BodyId m_bodyId;
1870+
float m_extent;
1871+
float m_height;
1872+
float m_safetyFactor;
1873+
float m_overlap;
1874+
};
1875+
1876+
static int sampleSafetyFactor = RegisterSample( "Continuous", "Safety Factor", SafetyFactor::Create );

0 commit comments

Comments
 (0)