Skip to content

Commit d9b5732

Browse files
authored
Remove cancellation from inertia calculation to improve accuracy (#955)
I was computing the shape inertia about the body origin and then shifting to the center of mass. But this involves quadratic math with bad cancellation effects. I'm now buffering the shape inertias about their centroids and then shifting them to the final body center of mass. This removes the quadratic cancellation. Fixes #949
1 parent 349f8d8 commit d9b5732

File tree

5 files changed

+124
-59
lines changed

5 files changed

+124
-59
lines changed

include/box2d/collision.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ typedef struct b2MassData
9797
/// The position of the shape's centroid relative to the shape's origin.
9898
b2Vec2 center;
9999

100-
/// The rotational inertia of the shape about the local origin.
100+
/// The rotational inertia of the shape about the shape center.
101101
float rotationalInertia;
102102
} b2MassData;
103103

samples/sample_issues.cpp

Lines changed: 87 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
#include "box2d/box2d.h"
21
#include "sample.h"
32

3+
#include "box2d/box2d.h"
4+
45
#include <GLFW/glfw3.h>
56

67
struct PhysicsHitQueryResult2D
@@ -22,11 +23,11 @@ struct CastContext_Single
2223
bool hit = false;
2324
};
2425

25-
static float b2CastResult_Closest(b2ShapeId shapeId, b2Vec2 point, b2Vec2 normal, float fraction, void* c)
26+
static float b2CastResult_Closest( b2ShapeId shapeId, b2Vec2 point, b2Vec2 normal, float fraction, void* c )
2627
{
27-
CastContext_Single* context = static_cast<CastContext_Single*>(c);
28+
CastContext_Single* context = static_cast<CastContext_Single*>( c );
2829

29-
if (b2Dot(context->translation, normal) >= 0.0f)
30+
if ( b2Dot( context->translation, normal ) >= 0.0f )
3031
return -1.0f;
3132

3233
context->result.shapeId = shapeId;
@@ -40,15 +41,15 @@ static float b2CastResult_Closest(b2ShapeId shapeId, b2Vec2 point, b2Vec2 normal
4041
return fraction;
4142
}
4243

43-
static b2ShapeProxy TransformShapeProxy(const b2Transform& t, const b2ShapeProxy& proxy)
44+
static b2ShapeProxy TransformShapeProxy( const b2Transform& t, const b2ShapeProxy& proxy )
4445
{
4546
b2ShapeProxy ret;
4647
ret.count = proxy.count;
4748
ret.radius = proxy.radius;
4849

49-
for (int i = 0; i < proxy.count; ++i)
50+
for ( int i = 0; i < proxy.count; ++i )
5051
{
51-
ret.points[i] = b2TransformPoint(t, proxy.points[i]);
52+
ret.points[i] = b2TransformPoint( t, proxy.points[i] );
5253
}
5354

5455
return ret;
@@ -58,20 +59,14 @@ static b2ShapeProxy TransformShapeProxy(const b2Transform& t, const b2ShapeProxy
5859
class ShapeCastChain : public Sample
5960
{
6061
public:
61-
explicit ShapeCastChain(SampleContext* context)
62-
: Sample(context)
62+
explicit ShapeCastChain( SampleContext* context )
63+
: Sample( context )
6364
{
6465
// World Body & Shape
6566
b2BodyDef worldBodyDef = b2DefaultBodyDef();
66-
const b2BodyId groundId = b2CreateBody(m_worldId, &worldBodyDef);
67+
const b2BodyId groundId = b2CreateBody( m_worldId, &worldBodyDef );
6768

68-
b2Vec2 points[4] =
69-
{
70-
{ 1.0f, 0.0f },
71-
{ -1.0f, 0.0f },
72-
{ -1.0f, -1.0f },
73-
{ 1.0f, -1.0f }
74-
};
69+
b2Vec2 points[4] = { { 1.0f, 0.0f }, { -1.0f, 0.0f }, { -1.0f, -1.0f }, { 1.0f, -1.0f } };
7570

7671
b2ChainDef worldChainDef = b2DefaultChainDef();
7772
worldChainDef.userData = nullptr;
@@ -82,12 +77,12 @@ class ShapeCastChain : public Sample
8277
worldChainDef.filter.groupIndex = 0;
8378
worldChainDef.isLoop = true;
8479
worldChainDef.enableSensorEvents = false;
85-
b2CreateChain(groundId, &worldChainDef);
80+
b2CreateChain( groundId, &worldChainDef );
8681

8782
// "Character" Body & Shape
8883
b2BodyDef characterBodyDef = b2DefaultBodyDef();
8984
characterBodyDef.position = { 0.0f, 0.103f };
90-
characterBodyDef.rotation = b2MakeRot(0.0f);
85+
characterBodyDef.rotation = b2MakeRot( 0.0f );
9186
characterBodyDef.linearDamping = 0.0f;
9287
characterBodyDef.angularDamping = 0.0f;
9388
characterBodyDef.gravityScale = 1.0f;
@@ -98,7 +93,7 @@ class ShapeCastChain : public Sample
9893
characterBodyDef.userData = nullptr;
9994
characterBodyDef.type = b2_kinematicBody;
10095

101-
characterBodyId_ = b2CreateBody(m_worldId, &characterBodyDef);
96+
characterBodyId_ = b2CreateBody( m_worldId, &characterBodyDef );
10297

10398
b2ShapeDef characterShapeDef = b2DefaultShapeDef();
10499

@@ -114,8 +109,8 @@ class ShapeCastChain : public Sample
114109
characterShapeDef.invokeContactCreation = false;
115110
characterShapeDef.updateBodyMass = false;
116111

117-
characterBox_ = b2MakeBox(0.1f, 0.1f);
118-
b2CreatePolygonShape(characterBodyId_, &characterShapeDef, &characterBox_);
112+
characterBox_ = b2MakeBox( 0.1f, 0.1f );
113+
b2CreatePolygonShape( characterBodyId_, &characterShapeDef, &characterBox_ );
119114

120115
context->camera.m_center = b2Vec2_zero;
121116
}
@@ -125,32 +120,32 @@ class ShapeCastChain : public Sample
125120
const float timeStep = m_context->hertz > 0.0f ? 1.0f / m_context->hertz : 0.0f;
126121

127122
bool noXInput = true;
128-
if (glfwGetKey(m_context->window, GLFW_KEY_A))
123+
if ( glfwGetKey( m_context->window, GLFW_KEY_A ) )
129124
{
130125
characterVelocity_.x -= timeStep * 5.0f;
131126
noXInput = false;
132127
}
133-
if (glfwGetKey(m_context->window, GLFW_KEY_D))
128+
if ( glfwGetKey( m_context->window, GLFW_KEY_D ) )
134129
{
135130
characterVelocity_.x += timeStep * 5.0f;
136131
noXInput = false;
137132
}
138133

139134
bool noYInput = true;
140-
if (glfwGetKey(m_context->window, GLFW_KEY_S))
135+
if ( glfwGetKey( m_context->window, GLFW_KEY_S ) )
141136
{
142137
characterVelocity_.y -= timeStep * 5.0f;
143138
noYInput = false;
144139
}
145-
if (glfwGetKey(m_context->window, GLFW_KEY_W))
140+
if ( glfwGetKey( m_context->window, GLFW_KEY_W ) )
146141
{
147142
characterVelocity_.y += timeStep * 5.0f;
148143
noYInput = false;
149144
}
150145

151-
if (noXInput)
146+
if ( noXInput )
152147
{
153-
if (b2AbsFloat(characterVelocity_.x) > 0.01f)
148+
if ( b2AbsFloat( characterVelocity_.x ) > 0.01f )
154149
{
155150
const float decel = characterVelocity_.x > 0.0f ? 5.0f : -5.0f;
156151
if ( b2AbsFloat( decel ) < characterVelocity_.x )
@@ -168,7 +163,7 @@ class ShapeCastChain : public Sample
168163
}
169164
}
170165

171-
if (noYInput)
166+
if ( noYInput )
172167
{
173168
if ( b2AbsFloat( characterVelocity_.y ) > 0.01f )
174169
{
@@ -188,42 +183,39 @@ class ShapeCastChain : public Sample
188183
}
189184
}
190185

191-
b2Vec2 characterPos = b2Body_GetPosition(characterBodyId_);
186+
b2Vec2 characterPos = b2Body_GetPosition( characterBodyId_ );
192187
b2Vec2 newCharacterPos = characterPos;
193188
newCharacterPos.x += characterVelocity_.x * timeStep;
194189
newCharacterPos.y += characterVelocity_.y * timeStep;
195-
b2Body_SetTransform(characterBodyId_, newCharacterPos, b2Rot_identity);
190+
b2Body_SetTransform( characterBodyId_, newCharacterPos, b2Rot_identity );
196191

197192
PhysicsHitQueryResult2D hitResult;
198-
const b2ShapeProxy shapeProxy = b2MakeProxy(characterBox_.vertices, characterBox_.count, characterBox_.radius);
199-
if (ShapeCastSingle(hitResult, characterPos, newCharacterPos, 0.0f, shapeProxy))
193+
const b2ShapeProxy shapeProxy = b2MakeProxy( characterBox_.vertices, characterBox_.count, characterBox_.radius );
194+
if ( ShapeCastSingle( hitResult, characterPos, newCharacterPos, 0.0f, shapeProxy ) )
200195
{
201196
hitPos = hitResult.point;
202197
hitNormal = hitResult.normal;
203198
}
204199

205-
m_draw->DrawLine(hitPos, { hitPos.x + hitNormal.x, hitPos.y + hitNormal.y }, b2_colorRed);
200+
m_draw->DrawLine( hitPos, { hitPos.x + hitNormal.x, hitPos.y + hitNormal.y }, b2_colorRed );
206201

207202
Sample::Step();
208203
}
209204

210-
bool ShapeCastSingle(PhysicsHitQueryResult2D& outResult, b2Vec2 start, b2Vec2 end, float rotation, const b2ShapeProxy& shape) const
205+
bool ShapeCastSingle( PhysicsHitQueryResult2D& outResult, b2Vec2 start, b2Vec2 end, float rotation,
206+
const b2ShapeProxy& shape ) const
211207
{
212-
const b2Transform transform =
213-
{
214-
start,
215-
b2MakeRot(rotation)
216-
};
217-
const b2ShapeProxy transformedShape = TransformShapeProxy(transform, shape);
208+
const b2Transform transform = { start, b2MakeRot( rotation ) };
209+
const b2ShapeProxy transformedShape = TransformShapeProxy( transform, shape );
218210

219211
const b2Vec2 translation = { end.x - start.x, end.y - start.y };
220212
const b2QueryFilter filter = { 0x1, 0x1 };
221213
CastContext_Single context;
222214
context.startPos = start;
223215
context.translation = { translation.x, translation.y };
224-
b2World_CastShape(m_worldId, &transformedShape, translation, filter, &b2CastResult_Closest, &context);
216+
b2World_CastShape( m_worldId, &transformedShape, translation, filter, &b2CastResult_Closest, &context );
225217

226-
if (context.hit)
218+
if ( context.hit )
227219
{
228220
outResult = context.result;
229221
return true;
@@ -232,9 +224,9 @@ class ShapeCastChain : public Sample
232224
return false;
233225
}
234226

235-
static Sample* Create(SampleContext* context)
227+
static Sample* Create( SampleContext* context )
236228
{
237-
return new ShapeCastChain(context);
229+
return new ShapeCastChain( context );
238230
}
239231

240232
private:
@@ -245,4 +237,53 @@ class ShapeCastChain : public Sample
245237
b2Vec2 hitNormal = b2Vec2_zero;
246238
};
247239

248-
static int sampleShapeCastChain = RegisterSample("Issues", "Shape Cast Chain", ShapeCastChain::Create );
240+
static int sampleShapeCastChain = RegisterSample( "Issues", "Shape Cast Chain", ShapeCastChain::Create );
241+
242+
class BadSteiner : public Sample
243+
{
244+
public:
245+
explicit BadSteiner( SampleContext* context )
246+
: Sample( context )
247+
{
248+
if ( m_context->restart == false )
249+
{
250+
m_context->camera.m_center = { 0.0f, 1.75f };
251+
m_context->camera.m_zoom = 2.5f;
252+
}
253+
254+
{
255+
b2BodyDef bodyDef = b2DefaultBodyDef();
256+
b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef );
257+
258+
b2ShapeDef shapeDef = b2DefaultShapeDef();
259+
b2Segment segment = { { -100.0f, 0.0f }, { 100.0f, 0.0f } };
260+
b2CreateSegmentShape( groundId, &shapeDef, &segment );
261+
}
262+
263+
{
264+
b2BodyDef bodyDef = b2DefaultBodyDef();
265+
bodyDef.type = b2_dynamicBody;
266+
bodyDef.position = { -48.0f, 62.0f };
267+
b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef );
268+
269+
b2ShapeDef shapeDef = b2DefaultShapeDef();
270+
271+
b2Vec2 points[3] = {
272+
{ 48.7599983f, -60.5699997f },
273+
{ 48.7400017f, -60.5400009f },
274+
{ 48.6800003f, -60.5600014f },
275+
};
276+
277+
b2Hull hull = b2ComputeHull( points, 3 );
278+
b2Polygon poly = b2MakePolygon( &hull, 0.0f );
279+
b2CreatePolygonShape( bodyId, &shapeDef, &poly );
280+
}
281+
}
282+
283+
static Sample* Create( SampleContext* context )
284+
{
285+
return new BadSteiner( context );
286+
}
287+
};
288+
289+
static int sampleBadSteiner = RegisterSample( "Issues", "Bad Steiner", BadSteiner::Create );

src/body.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -572,23 +572,30 @@ void b2UpdateBodyMassData( b2World* world, b2Body* body )
572572
return;
573573
}
574574

575+
int shapeCount = body->shapeCount;
576+
b2MassData* masses = b2AllocateArenaItem( &world->arena, shapeCount * sizeof( b2MassData ), "mass data" );
577+
575578
// Accumulate mass over all shapes.
576579
b2Vec2 localCenter = b2Vec2_zero;
577580
int shapeId = body->headShapeId;
581+
int shapeIndex = 0;
578582
while ( shapeId != B2_NULL_INDEX )
579583
{
580584
const b2Shape* s = b2ShapeArray_Get( &world->shapes, shapeId );
581585
shapeId = s->nextShapeId;
582586

583587
if ( s->density == 0.0f )
584588
{
589+
masses[shapeIndex] = (b2MassData){ 0 };
585590
continue;
586591
}
587592

588593
b2MassData massData = b2ComputeShapeMass( s );
589594
body->mass += massData.mass;
590595
localCenter = b2MulAdd( localCenter, massData.mass, massData.center );
591-
body->inertia += massData.rotationalInertia;
596+
597+
masses[shapeIndex] = massData;
598+
shapeIndex += 1;
592599
}
593600

594601
// Compute center of mass.
@@ -598,11 +605,28 @@ void b2UpdateBodyMassData( b2World* world, b2Body* body )
598605
localCenter = b2MulSV( bodySim->invMass, localCenter );
599606
}
600607

608+
// Second loop to accumulate the rotational inertia about the center of mass
609+
for ( shapeIndex = 0; shapeIndex < shapeCount; ++shapeIndex )
610+
{
611+
b2MassData massData = masses[shapeIndex];
612+
if (massData.mass == 0.0f)
613+
{
614+
continue;
615+
}
616+
617+
// Shift to center of mass. This is safe because it can only increase.
618+
b2Vec2 offset = b2Sub( localCenter, massData.center );
619+
float inertia = massData.rotationalInertia + massData.mass * b2Dot( offset, offset );
620+
body->inertia += inertia;
621+
}
622+
623+
b2FreeArenaItem( &world->arena, masses );
624+
masses = NULL;
625+
626+
B2_ASSERT( body->inertia >= 0.0f );
627+
601628
if ( body->inertia > 0.0f )
602629
{
603-
// Center the inertia about the center of mass.
604-
body->inertia -= body->mass * b2Dot( localCenter, localCenter );
605-
B2_ASSERT( body->inertia > 0.0f );
606630
bodySim->invInertia = 1.0f / body->inertia;
607631
}
608632
else

src/geometry.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ b2MassData b2ComputeCircleMass( const b2Circle* shape, float density )
225225
massData.mass = density * B2_PI * rr;
226226
massData.center = shape->center;
227227

228-
// inertia about the local origin
229-
massData.rotationalInertia = massData.mass * ( 0.5f * rr + b2Dot( shape->center, shape->center ) );
228+
// inertia about the center of mass
229+
massData.rotationalInertia = massData.mass * 0.5f * rr ;
230230

231231
return massData;
232232
}
@@ -267,9 +267,6 @@ b2MassData b2ComputeCapsuleMass( const b2Capsule* shape, float density )
267267
float boxInertia = boxMass * ( 4.0f * rr + ll ) / 12.0f;
268268
massData.rotationalInertia = circleInertia + boxInertia;
269269

270-
// inertia about the local origin
271-
massData.rotationalInertia += massData.mass * b2Dot( massData.center, massData.center );
272-
273270
return massData;
274271
}
275272

@@ -392,8 +389,11 @@ b2MassData b2ComputePolygonMass( const b2Polygon* shape, float density )
392389
// Inertia tensor relative to the local origin (point s).
393390
massData.rotationalInertia = density * rotationalInertia;
394391

395-
// Shift to center of mass then to original body origin.
396-
massData.rotationalInertia += massData.mass * ( b2Dot( massData.center, massData.center ) - b2Dot( center, center ) );
392+
// Shift inertia to center of mass
393+
massData.rotationalInertia -= massData.mass * b2Dot( center, center );
394+
395+
// If this goes negative we are hosed
396+
B2_ASSERT( massData.rotationalInertia >= 0.0f );
397397

398398
return massData;
399399
}

test/test_shape.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ static int ShapeMassTest( void )
2121
b2MassData md = b2ComputeCircleMass( &circle, 1.0f );
2222
ENSURE_SMALL( md.mass - B2_PI, FLT_EPSILON );
2323
ENSURE( md.center.x == 1.0f && md.center.y == 0.0f );
24-
ENSURE_SMALL( md.rotationalInertia - 1.5f * B2_PI, FLT_EPSILON );
24+
ENSURE_SMALL( md.rotationalInertia - 0.5f * B2_PI, FLT_EPSILON );
2525
}
2626

2727
{

0 commit comments

Comments
 (0)