diff --git a/include/box2d/box2d.h b/include/box2d/box2d.h index f1dfa75df..acc980d19 100644 --- a/include/box2d/box2d.h +++ b/include/box2d/box2d.h @@ -143,10 +143,8 @@ B2_API b2Vec2 b2World_GetGravity( b2WorldId worldId ); /// Apply a radial explosion /// @param worldId The world id -/// @param position The center of the explosion -/// @param radius The radius of the explosion -/// @param impulse The impulse of the explosion, typically in kg * m / s or N * s. -B2_API void b2World_Explode( b2WorldId worldId, b2Vec2 position, float radius, float impulse ); +/// @param explosionDef The explosion definition +B2_API void b2World_Explode( b2WorldId worldId, const b2ExplosionDef* explosionDef ); /// Adjust contact tuning parameters /// @param worldId The world id @@ -1006,6 +1004,12 @@ B2_API float b2RevoluteJoint_GetMaxMotorTorque( b2JointId jointId ); /// @see b2WeldJointDef for details B2_API b2JointId b2CreateWeldJoint( b2WorldId worldId, const b2WeldJointDef* def ); +/// Get the weld joint reference angle in radians +B2_API float b2WeldJoint_GetReferenceAngle( b2JointId jointId ); + +/// Set the weld joint reference angle in radians, must be in [-pi,pi]. +B2_API void b2WeldJoint_SetReferenceAngle( b2JointId jointId, float angleInRadians ); + /// Set the weld joint linear stiffness in Hertz. 0 is rigid. B2_API void b2WeldJoint_SetLinearHertz( b2JointId jointId, float hertz ); diff --git a/include/box2d/types.h b/include/box2d/types.h index b71877914..6d324ed04 100644 --- a/include/box2d/types.h +++ b/include/box2d/types.h @@ -11,7 +11,10 @@ #include #include -/// Task interface +#define B2_DEFAULT_CATEGORY_BITS 0x0001ULL +#define B2_DEFAULT_MASK_BITS UINT64_MAX + + /// Task interface /// This is prototype for a Box2D task. Your task system is expected to invoke the Box2D task with these arguments. /// The task spans a range of the parallel-for: [startIndex, endIndex) /// The worker index must correctly identify each worker in the user thread pool, expected in [0, workerCount). @@ -391,6 +394,9 @@ typedef struct b2ChainDef /// Contact filtering data. b2Filter filter; + /// Custom debug draw color. + uint32_t customColor; + /// Indicates a closed chain formed by connecting the first and last points bool isLoop; @@ -865,6 +871,33 @@ typedef struct b2WheelJointDef /// @ingroup wheel_joint B2_API b2WheelJointDef b2DefaultWheelJointDef( void ); +/// The explosion definition is used to configure options for explosions. Explosions +/// consider shape geometry when computing the impulse. +/// @ingroup world +typedef struct b2ExplosionDef +{ + /// Mask bits to filter shapes + uint64_t maskBits; + + /// The center of the explosion in world space + b2Vec2 position; + + /// The radius of the explosion + float radius; + + /// The falloff distance beyond the radius. Impulse is reduced to zero at this distance. + float falloff; + + /// Impulse per unit length. This applies an impulse according to the shape perimeter that + /// is facing the explosion. Explosions only apply to circles, capsules, and polygons. This + /// may be negative for implosions. + float impulsePerLength; +} b2ExplosionDef; + +/// Use this to initialize your explosion definition +/// @ingroup world +B2_API b2ExplosionDef b2DefaultExplosionDef( void ); + /** * @defgroup events Events * World event types. diff --git a/samples/sample_bodies.cpp b/samples/sample_bodies.cpp index cc441a505..55efbc2b9 100644 --- a/samples/sample_bodies.cpp +++ b/samples/sample_bodies.cpp @@ -533,7 +533,12 @@ class Weeble : public Sample if ( ImGui::Button( "Explode" ) ) { - b2World_Explode( m_worldId, m_explosionPosition, m_explosionRadius, m_explosionMagnitude ); + b2ExplosionDef def = b2DefaultExplosionDef(); + def.position = m_explosionPosition; + def.radius = m_explosionRadius; + def.falloff = 0.1f; + def.impulsePerLength = m_explosionMagnitude; + b2World_Explode( m_worldId, &def ); } ImGui::PushItemWidth( 100.0f ); diff --git a/samples/sample_events.cpp b/samples/sample_events.cpp index e538ff2fd..94d03e5a6 100644 --- a/samples/sample_events.cpp +++ b/samples/sample_events.cpp @@ -990,7 +990,7 @@ class BodyMove : public Sample m_explosionPosition = { 0.0f, -5.0f }; m_explosionRadius = 10.0f; - m_explosionMagnitude = 6.0f; + m_explosionMagnitude = 10.0f; } void CreateBodies() @@ -1046,10 +1046,15 @@ class BodyMove : public Sample if ( ImGui::Button( "Explode" ) ) { - b2World_Explode( m_worldId, m_explosionPosition, m_explosionRadius, m_explosionMagnitude ); + b2ExplosionDef def = b2DefaultExplosionDef(); + def.position = m_explosionPosition; + def.radius = m_explosionRadius; + def.falloff = 0.1f; + def.impulsePerLength = m_explosionMagnitude; + b2World_Explode( m_worldId, &def ); } - ImGui::SliderFloat( "Magnitude", &m_explosionMagnitude, -8.0f, 8.0f, "%.1f" ); + ImGui::SliderFloat( "Magnitude", &m_explosionMagnitude, -20.0f, 20.0f, "%.1f" ); ImGui::End(); } diff --git a/samples/sample_shapes.cpp b/samples/sample_shapes.cpp index 91df17c43..9721cc6b9 100644 --- a/samples/sample_shapes.cpp +++ b/samples/sample_shapes.cpp @@ -107,6 +107,7 @@ class ChainShape : public Sample b2ChainDef chainDef = b2DefaultChainDef(); chainDef.points = points; chainDef.count = count; + chainDef.customColor = b2_colorSteelBlue; chainDef.isLoop = true; chainDef.friction = 0.2f; @@ -1302,3 +1303,118 @@ class OffsetShapes : public Sample }; static int sampleOffsetShapes = RegisterSample( "Shapes", "Offset", OffsetShapes::Create ); + +// This shows how to use explosions and demonstrates the projected perimeter +class Explosion : public Sample +{ +public: + + explicit Explosion( Settings& settings ) + : Sample( settings ) + { + if ( settings.restart == false ) + { + g_camera.m_center = { 0.0f, 0.0f }; + g_camera.m_zoom = 14.0f; + } + + b2BodyDef bodyDef = b2DefaultBodyDef(); + b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef ); + + bodyDef.type = b2_dynamicBody; + bodyDef.gravityScale = 0.0f; + b2ShapeDef shapeDef = b2DefaultShapeDef(); + + m_referenceAngle = 0.0f; + + b2WeldJointDef weldDef = b2DefaultWeldJointDef(); + weldDef.referenceAngle = m_referenceAngle; + weldDef.angularHertz = 0.5f; + weldDef.angularDampingRatio = 0.7f; + weldDef.linearHertz = 0.5f; + weldDef.linearDampingRatio = 0.7f; + weldDef.bodyIdA = groundId; + weldDef.localAnchorB = b2Vec2_zero; + + float r = 8.0f; + for (float angle = 0.0f; angle < 360.0f; angle += 30.0f) + { + b2CosSin cosSin = b2ComputeCosSin( angle * b2_pi / 180.0f ); + bodyDef.position = { r * cosSin.cosine, r * cosSin.sine }; + b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); + + b2Polygon box = b2MakeBox( 1.0f, 0.1f ); + b2CreatePolygonShape( bodyId, &shapeDef, &box ); + + weldDef.localAnchorA = bodyDef.position; + weldDef.bodyIdB = bodyId; + b2JointId jointId = b2CreateWeldJoint( m_worldId, &weldDef ); + m_jointIds.push_back( jointId ); + } + + m_radius = 7.0f; + m_falloff = 3.0f; + m_impulse = 10.0f; + } + + void UpdateUI() override + { + float height = 160.0f; + ImGui::SetNextWindowPos( ImVec2( 10.0f, g_camera.m_height - height - 50.0f ), ImGuiCond_Once ); + ImGui::SetNextWindowSize( ImVec2( 240.0f, height ) ); + + ImGui::Begin( "Explosion", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize ); + + if ( ImGui::Button( "Explode" ) ) + { + b2ExplosionDef def = b2DefaultExplosionDef(); + def.position = b2Vec2_zero; + def.radius = m_radius; + def.falloff = m_falloff; + def.impulsePerLength = m_impulse; + b2World_Explode( m_worldId, &def ); + } + + ImGui::SliderFloat( "radius", &m_radius, 0.0f, 20.0f, "%.1f" ); + ImGui::SliderFloat( "falloff", &m_falloff, 0.0f, 20.0f, "%.1f" ); + ImGui::SliderFloat( "impulse", &m_impulse, -20.0f, 20.0f, "%.1f" ); + + ImGui::End(); + } + + void Step( Settings& settings ) override + { + if (settings.pause == false || settings.singleStep == true) + { + m_referenceAngle += settings.hertz > 0.0f ? 60.0f * b2_pi / 180.0f / settings.hertz : 0.0f; + m_referenceAngle = b2UnwindAngle( m_referenceAngle ); + + int count = m_jointIds.size(); + for (int i = 0; i < count; ++i) + { + b2WeldJoint_SetReferenceAngle( m_jointIds[i], m_referenceAngle ); + } + } + + Sample::Step( settings ); + + g_draw.DrawString( 5, m_textLine, "reference angle = %g", m_referenceAngle ); + m_textLine += m_textIncrement; + + g_draw.DrawCircle( b2Vec2_zero, m_radius + m_falloff, b2_colorBox2DBlue ); + g_draw.DrawCircle( b2Vec2_zero, m_radius, b2_colorBox2DYellow ); + } + + static Sample* Create( Settings& settings ) + { + return new Explosion( settings ); + } + + std::vector m_jointIds; + float m_radius; + float m_falloff; + float m_impulse; + float m_referenceAngle; +}; + +static int sampleBodyMove = RegisterSample( "Shapes", "Explosion", Explosion::Create ); diff --git a/samples/sample_world.cpp b/samples/sample_world.cpp index 1e5ad87be..1b6d44b97 100644 --- a/samples/sample_world.cpp +++ b/samples/sample_world.cpp @@ -193,7 +193,14 @@ class LargeWorld : public Sample if ( ( m_stepCount & 0x1 ) == 0x1 && m_explode ) { m_explosionPosition.x = ( 0.5f + m_cycleIndex ) * m_period - span; - b2World_Explode( m_worldId, m_explosionPosition, radius, 1.0f ); + + b2ExplosionDef def = b2DefaultExplosionDef(); + def.position = m_explosionPosition; + def.radius = radius; + def.falloff = 0.1f; + def.impulsePerLength = 1.0f; + b2World_Explode( m_worldId, &def ); + m_cycleIndex = ( m_cycleIndex + 1 ) % m_cycleCount; } diff --git a/src/joint.c b/src/joint.c index d1af86f83..fe14ac9a4 100644 --- a/src/joint.c +++ b/src/joint.c @@ -83,6 +83,13 @@ b2WheelJointDef b2DefaultWheelJointDef( void ) return def; } +b2ExplosionDef b2DefaultExplosionDef(void) +{ + b2ExplosionDef def = { 0 }; + def.maskBits = B2_DEFAULT_MASK_BITS; + return def; +} + static b2Joint* b2GetJointFullId( b2World* world, b2JointId jointId ) { int id = jointId.index1 - 1; diff --git a/src/prismatic_joint.c b/src/prismatic_joint.c index ba6836600..8530a8f5f 100644 --- a/src/prismatic_joint.c +++ b/src/prismatic_joint.c @@ -254,6 +254,7 @@ void b2PreparePrismaticJoint( b2JointSim* base, b2StepContext* context ) joint->axisA = b2RotateVector( qA, joint->localAxisA ); joint->deltaCenter = b2Sub( bodySimB->center, bodySimA->center ); joint->deltaAngle = b2RelativeAngle( qB, qA ) - joint->referenceAngle; + joint->deltaAngle = b2UnwindAngle( joint->deltaAngle ); b2Vec2 rA = joint->anchorA; b2Vec2 rB = joint->anchorB; diff --git a/src/shape.c b/src/shape.c index 9d4cb8bac..b63602253 100644 --- a/src/shape.c +++ b/src/shape.c @@ -320,6 +320,7 @@ b2ChainId b2CreateChain( b2BodyId bodyId, const b2ChainDef* def ) shapeDef.restitution = def->restitution; shapeDef.friction = def->friction; shapeDef.filter = def->filter; + shapeDef.customColor = def->customColor; shapeDef.enableContactEvents = false; shapeDef.enableHitEvents = false; shapeDef.enableSensorEvents = false; @@ -546,6 +547,58 @@ float b2GetShapePerimeter( const b2Shape* shape ) } } +// This projects the the shape perimeter onto an infinite line +float b2GetShapeProjectedPerimeter( const b2Shape* shape, b2Vec2 line ) +{ + switch ( shape->type ) + { + case b2_capsuleShape: + { + b2Vec2 axis = b2Sub( shape->capsule.center2, shape->capsule.center1 ); + float projectedLength = b2AbsFloat( b2Dot( axis, line ) ); + return projectedLength + 2.0f * shape->capsule.radius; + } + + case b2_circleShape: + return 2.0f * shape->circle.radius; + + case b2_polygonShape: + { + const b2Vec2* points = shape->polygon.vertices; + int count = shape->polygon.count; + B2_ASSERT( count > 0 ); + float value = b2Dot( points[0], line ); + float lower = value; + float upper = value; + for ( int i = 1; i < count; ++i ) + { + value = b2Dot( points[i], line ); + lower = b2MinFloat( lower, value ); + upper = b2MaxFloat( upper, value ); + } + + return (upper - lower) + 2.0f * shape->polygon.radius; + } + + case b2_segmentShape: + { + float value1 = b2Dot( shape->segment.point1, line ); + float value2 = b2Dot( shape->segment.point2, line ); + return b2AbsFloat( value2 - value1 ); + } + + case b2_chainSegmentShape: + { + float value1 = b2Dot( shape->chainSegment.segment.point1, line ); + float value2 = b2Dot( shape->chainSegment.segment.point2, line ); + return b2AbsFloat( value2 - value1 ); + } + + default: + return 0.0f; + } +} + b2MassData b2ComputeShapeMass( const b2Shape* shape ) { switch ( shape->type ) diff --git a/src/shape.h b/src/shape.h index e5efe65e4..8e9ce037d 100644 --- a/src/shape.h +++ b/src/shape.h @@ -73,6 +73,7 @@ b2ShapeExtent b2ComputeShapeExtent( const b2Shape* shape, b2Vec2 localCenter ); b2AABB b2ComputeShapeAABB( const b2Shape* shape, b2Transform transform ); b2Vec2 b2GetShapeCentroid( const b2Shape* shape ); float b2GetShapePerimeter( const b2Shape* shape ); +float b2GetShapeProjectedPerimeter( const b2Shape* shape, b2Vec2 line ); b2DistanceProxy b2MakeShapeDistanceProxy( const b2Shape* shape ); diff --git a/src/types.c b/src/types.c index 69614cf2e..227945968 100644 --- a/src/types.c +++ b/src/types.c @@ -42,13 +42,13 @@ b2BodyDef b2DefaultBodyDef( void ) b2Filter b2DefaultFilter( void ) { - b2Filter filter = { 0x0001ULL, UINT64_MAX, 0 }; + b2Filter filter = { B2_DEFAULT_CATEGORY_BITS, B2_DEFAULT_MASK_BITS, 0 }; return filter; } b2QueryFilter b2DefaultQueryFilter( void ) { - b2QueryFilter filter = { 0x0001ULL, UINT64_MAX }; + b2QueryFilter filter = { B2_DEFAULT_CATEGORY_BITS, B2_DEFAULT_MASK_BITS }; return filter; } diff --git a/src/weld_joint.c b/src/weld_joint.c index ea8b5a096..e772fde22 100644 --- a/src/weld_joint.c +++ b/src/weld_joint.c @@ -11,6 +11,19 @@ // needed for dll export #include "box2d/box2d.h" +float b2WeldJoint_GetReferenceAngle( b2JointId jointId ) +{ + b2JointSim* joint = b2GetJointSimCheckType( jointId, b2_weldJoint ); + return joint->weldJoint.referenceAngle; +} + +void b2WeldJoint_SetReferenceAngle( b2JointId jointId, float angleInRadians ) +{ + B2_ASSERT( b2IsValid( angleInRadians ) ); + b2JointSim* joint = b2GetJointSimCheckType( jointId, b2_weldJoint ); + joint->weldJoint.referenceAngle = b2ClampFloat(angleInRadians, -b2_pi, b2_pi); +} + void b2WeldJoint_SetLinearHertz( b2JointId jointId, float hertz ) { B2_ASSERT( b2IsValid( hertz ) && hertz >= 0.0f ); @@ -132,6 +145,7 @@ void b2PrepareWeldJoint( b2JointSim* base, b2StepContext* context ) joint->anchorB = b2RotateVector( qB, b2Sub( base->localOriginAnchorB, bodySimB->localCenter ) ); joint->deltaCenter = b2Sub( bodySimB->center, bodySimA->center ); joint->deltaAngle = b2RelativeAngle( qB, qA ) - joint->referenceAngle; + joint->deltaAngle = b2UnwindAngle( joint->deltaAngle ); float ka = iA + iB; joint->axialMass = ka > 0.0f ? 1.0f / ka : 0.0f; diff --git a/src/world.c b/src/world.c index a4160ec77..9494b10a0 100644 --- a/src/world.c +++ b/src/world.c @@ -2381,7 +2381,8 @@ struct ExplosionContext b2World* world; b2Vec2 position; float radius; - float magnitude; + float falloff; + float impulsePerLength; }; static bool ExplosionCallback( int proxyId, int shapeId, void* context ) @@ -2394,17 +2395,7 @@ static bool ExplosionCallback( int proxyId, int shapeId, void* context ) b2Shape* shape = b2ShapeArray_Get( &world->shapes, shapeId ); b2Body* body = b2BodyArray_Get( &world->bodies, shape->bodyId ); - if ( body->type == b2_kinematicBody ) - { - return true; - } - - b2WakeBody( world, body ); - - if ( body->setIndex != b2_awakeSet ) - { - return true; - } + B2_ASSERT( body->type == b2_dynamicBody ); b2Transform transform = b2GetBodyTransformQuick( world, body ); @@ -2418,24 +2409,46 @@ static bool ExplosionCallback( int proxyId, int shapeId, void* context ) b2DistanceCache cache = { 0 }; b2DistanceOutput output = b2ShapeDistance( &cache, &input, NULL, 0 ); - if ( output.distance > explosionContext->radius ) + float radius = explosionContext->radius; + float falloff = explosionContext->falloff; + if ( output.distance > radius + falloff ) { return true; } - b2Vec2 closestPoint = output.pointA; + b2WakeBody( world, body ); + if ( body->setIndex != b2_awakeSet ) + { + return true; + } + + b2Vec2 closestPoint = output.pointA; if ( output.distance == 0.0f ) { b2Vec2 localCentroid = b2GetShapeCentroid( shape ); closestPoint = b2TransformPoint( transform, localCentroid ); } - float falloff = 0.4f; - float perimeter = b2GetShapePerimeter( shape ); - float magnitude = explosionContext->magnitude * perimeter * ( 1.0f - falloff * output.distance / explosionContext->radius ); + b2Vec2 direction = b2Sub( closestPoint, explosionContext->position ); + if (b2LengthSquared(direction) > 100.0f * FLT_EPSILON * FLT_EPSILON) + { + direction = b2Normalize( direction ); + } + else + { + direction = ( b2Vec2 ){ 1.0f, 0.0f }; + } + + b2Vec2 localLine = b2InvRotateVector( transform.q, b2LeftPerp(direction) ); + float perimeter = b2GetShapeProjectedPerimeter( shape, localLine ); + float scale = 1.0f; + if ( output.distance > radius && falloff > 0.0f ) + { + scale = b2ClampFloat((radius + falloff - output.distance) / falloff, 0.0f, 1.0f); + } - b2Vec2 direction = b2Normalize( b2Sub( closestPoint, explosionContext->position ) ); + float magnitude = explosionContext->impulsePerLength * perimeter * scale; b2Vec2 impulse = b2MulSV( magnitude, direction ); int localIndex = body->localIndex; @@ -2448,11 +2461,18 @@ static bool ExplosionCallback( int proxyId, int shapeId, void* context ) return true; } -void b2World_Explode( b2WorldId worldId, b2Vec2 position, float radius, float magnitude ) +void b2World_Explode( b2WorldId worldId, const b2ExplosionDef* explosionDef ) { + uint64_t maskBits = explosionDef->maskBits; + b2Vec2 position = explosionDef->position; + float radius = explosionDef->radius; + float falloff = explosionDef->falloff; + float impulsePerLength = explosionDef->impulsePerLength; + B2_ASSERT( b2Vec2_IsValid( position ) ); - B2_ASSERT( b2IsValid( radius ) && radius > 0.0f ); - B2_ASSERT( b2IsValid( magnitude ) ); + B2_ASSERT( b2IsValid( radius ) && radius >= 0.0f ); + B2_ASSERT( b2IsValid( falloff ) && falloff >= 0.0f ); + B2_ASSERT( b2IsValid( impulsePerLength ) ); b2World* world = b2GetWorldFromId( worldId ); B2_ASSERT( world->locked == false ); @@ -2461,15 +2481,16 @@ void b2World_Explode( b2WorldId worldId, b2Vec2 position, float radius, float ma return; } - struct ExplosionContext explosionContext = { world, position, radius, magnitude }; + struct ExplosionContext explosionContext = { world, position, radius, falloff, + impulsePerLength }; b2AABB aabb; - aabb.lowerBound.x = position.x - radius; - aabb.lowerBound.y = position.y - radius; - aabb.upperBound.x = position.x + radius; - aabb.upperBound.y = position.y + radius; + aabb.lowerBound.x = position.x - (radius + falloff); + aabb.lowerBound.y = position.y - (radius + falloff); + aabb.upperBound.x = position.x + (radius + falloff); + aabb.upperBound.y = position.y + (radius + falloff); - b2DynamicTree_Query( world->broadPhase.trees + b2_dynamicBody, aabb, b2_defaultMaskBits, ExplosionCallback, + b2DynamicTree_Query( world->broadPhase.trees + b2_dynamicBody, aabb, maskBits, ExplosionCallback, &explosionContext ); }