diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a77b7564e..bf62e4652 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,7 +86,7 @@ jobs: run: ./bin/${{env.BUILD_TYPE}}/test samples-windows: - name: windows + name: samples-windows runs-on: windows-latest steps: @@ -102,4 +102,16 @@ jobs: - name: Build run: cmake --build ${{github.workspace}}/build --config Release - \ No newline at end of file + + samples-macos: + name: samples-macos + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release -DBOX2D_SAMPLES=ON -DBUILD_SHARED_LIBS=OFF -DBOX2D_UNIT_TESTS=OFF + + - name: Build + run: cmake --build ${{github.workspace}}/build --config Release diff --git a/docs/FAQ.md b/docs/FAQ.md index c842b800e..2244f919a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -94,7 +94,7 @@ Box2D also does not have exact collision. There is no continuous collision betwe Making a worms clone requires arbitrarily destructible terrain. This is beyond the scope of Box2D, so you will have to figure out how to do this on your own. ### Tile Based Environment -Using many boxes for your terrain may not work well because box-like characters can get snagged on internal corners. Box2D proves chain shapes for smooth collision, see `b2ChainDef`. In general you should avoid using a rectangular character because collision tolerances will still lead to undesirable snagging. Box2D provides capsules and rounded polygons that may work better for characters. +Using many boxes for your terrain may not work well because box-like characters can get snagged on internal corners. Box2D provides chain shapes for smooth collision, see `b2ChainDef`. In general you should avoid using a rectangular character because collision tolerances will still lead to undesirable snagging. Box2D provides capsules and rounded polygons that may work better for characters. ### Asteroid Type Coordinate Systems Box2D does not have any support for coordinate frame wrapping. You would likely need to customize Box2D for this purpose. You may need to use a different broad-phase for this to work. diff --git a/include/box2d/collision.h b/include/box2d/collision.h index 3a9cf24e7..49a59d022 100644 --- a/include/box2d/collision.h +++ b/include/box2d/collision.h @@ -249,17 +249,17 @@ B2_API bool b2PointInCapsule( b2Vec2 point, const b2Capsule* shape ); /// Test a point for overlap with a convex polygon in local space B2_API bool b2PointInPolygon( b2Vec2 point, const b2Polygon* shape ); -/// Ray cast versus circle in shape local space. Initial overlap is treated as a miss. +/// Ray cast versus circle shape in local space. Initial overlap is treated as a miss. B2_API b2CastOutput b2RayCastCircle( const b2RayCastInput* input, const b2Circle* shape ); -/// Ray cast versus capsule in shape local space. Initial overlap is treated as a miss. +/// Ray cast versus capsule shape in local space. Initial overlap is treated as a miss. B2_API b2CastOutput b2RayCastCapsule( const b2RayCastInput* input, const b2Capsule* shape ); -/// Ray cast versus segment in shape local space. Optionally treat the segment as one-sided with hits from +/// Ray cast versus segment shape in local space. Optionally treat the segment as one-sided with hits from /// the left side being treated as a miss. B2_API b2CastOutput b2RayCastSegment( const b2RayCastInput* input, const b2Segment* shape, bool oneSided ); -/// Ray cast versus polygon in shape local space. Initial overlap is treated as a miss. +/// Ray cast versus polygon shape in local space. Initial overlap is treated as a miss. B2_API b2CastOutput b2RayCastPolygon( const b2RayCastInput* input, const b2Polygon* shape ); /// Shape cast versus a circle. Initial overlap is treated as a miss. @@ -348,8 +348,10 @@ typedef struct b2DistanceProxy float radius; } b2DistanceProxy; -/// Used to warm start b2Distance. Set count to zero on first call or -/// use zero initialization. +/// Used to warm start the GJK simplex. If you call this function multiple times with nearby +/// transforms this might improve performance. Otherwise you can zero initialize this. +/// The distance cache must be initialized to zero on the first call. +/// Users should generally just zero initialize this structure for each call. typedef struct b2DistanceCache { /// The number of stored simplex points diff --git a/include/box2d/math_functions.h b/include/box2d/math_functions.h index 0b0c01d6d..7adbe016a 100644 --- a/include/box2d/math_functions.h +++ b/include/box2d/math_functions.h @@ -342,6 +342,9 @@ B2_INLINE b2Rot b2MakeRot( float angle ) return B2_LITERAL( b2Rot ){ cs.cosine, cs.sine }; } +/// Compute the rotation between two unit vectors +B2_API b2Rot b2ComputeRotationBetweenUnitVectors( b2Vec2 v1, b2Vec2 v2 ); + /// Is this rotation normalized? B2_INLINE bool b2IsNormalized( b2Rot q ) { @@ -352,6 +355,7 @@ B2_INLINE bool b2IsNormalized( b2Rot q ) /// Normalized linear interpolation /// https://fgiesen.wordpress.com/2012/08/15/linear-interpolation-past-present-and-future/ +/// https://web.archive.org/web/20170825184056/http://number-none.com/product/Understanding%20Slerp,%20Then%20Not%20Using%20It/ B2_INLINE b2Rot b2NLerp( b2Rot q1, b2Rot q2, float t ) { float omt = 1.0f - t; @@ -499,6 +503,9 @@ B2_INLINE b2Vec2 b2InvTransformPoint( b2Transform t, const b2Vec2 p ) return B2_LITERAL( b2Vec2 ){ t.q.c * vx + t.q.s * vy, -t.q.s * vx + t.q.c * vy }; } +/// Multiply two transforms. If the result is applied to a point p local to frame B, +/// the transform would first convert p to a point local to frame A, then into a point +/// in the world frame. /// v2 = A.q.Rot(B.q.Rot(v1) + B.p) + A.p /// = (A.q * B.q).Rot(v1) + A.q.Rot(B.p) + A.p B2_INLINE b2Transform b2MulTransforms( b2Transform A, b2Transform B ) @@ -509,6 +516,7 @@ B2_INLINE b2Transform b2MulTransforms( b2Transform A, b2Transform B ) return C; } +/// Creates a transform that converts a local point in frame B to a local point in frame A. /// v2 = A.q' * (B.q * v1 + B.p - A.p) /// = A.q' * B.q * v1 + A.q' * (B.p - A.p) B2_INLINE b2Transform b2InvMulTransforms( b2Transform A, b2Transform B ) diff --git a/include/box2d/types.h b/include/box2d/types.h index 18fe8d4c5..edf682b00 100644 --- a/include/box2d/types.h +++ b/include/box2d/types.h @@ -1005,6 +1005,9 @@ typedef struct b2SensorBeginTouchEvent } b2SensorBeginTouchEvent; /// An end touch event is generated when a shape stops overlapping a sensor shape. +/// You will not get an end event if you do anything that destroys contacts outside +/// of the world step. These include things like setting the transform, destroying a body +/// or shape, or changing a filter or body type. typedef struct b2SensorEndTouchEvent { /// The id of the sensor shape @@ -1046,6 +1049,9 @@ typedef struct b2ContactBeginTouchEvent } b2ContactBeginTouchEvent; /// An end touch event is generated when two shapes stop touching. +/// You will not get an end event if you do anything that destroys contacts outside +/// of the world step. These include things like setting the transform, destroying a body +/// or shape, or changing a filter or body type. typedef struct b2ContactEndTouchEvent { /// Id of the first shape diff --git a/samples/draw.cpp b/samples/draw.cpp index 54d7949ab..63ff379a8 100644 --- a/samples/draw.cpp +++ b/samples/draw.cpp @@ -633,11 +633,6 @@ struct GLTriangles GLint m_projectionUniform; }; -struct Transform -{ - float x, y, c, s; -}; - struct CircleData { b2Vec2 position; @@ -781,7 +776,7 @@ struct GLCircles struct SolidCircleData { - Transform transform; + b2Transform transform; float radius; RGBA8 rgba; }; @@ -926,7 +921,7 @@ struct GLSolidCircles struct CapsuleData { - Transform transform; + b2Transform transform; float radius; float length; RGBA8 rgba; diff --git a/samples/main.cpp b/samples/main.cpp index 9e4ad4133..a1e92ad13 100644 --- a/samples/main.cpp +++ b/samples/main.cpp @@ -111,7 +111,7 @@ static inline int CompareSamples( const void* a, const void* b ) return result; } -static void SortTests() +static void SortSamples() { qsort( g_sampleEntries, g_sampleCount, sizeof( SampleEntry ), CompareSamples ); } @@ -462,7 +462,7 @@ static void UpdateUI() ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; - if ( ImGui::BeginTabItem( "Tests" ) ) + if ( ImGui::BeginTabItem( "Samples" ) ) { int categoryIndex = 0; const char* category = g_sampleEntries[categoryIndex].category; @@ -538,7 +538,7 @@ int main( int, char** ) s_settings.Load(); s_settings.workerCount = b2MinInt( 8, (int)enki::GetNumHardwareThreads() / 2 ); - SortTests(); + SortSamples(); glfwSetErrorCallback( glfwErrorCallback ); diff --git a/samples/sample_benchmark.cpp b/samples/sample_benchmark.cpp index 453f236f7..e00c6f1ad 100644 --- a/samples/sample_benchmark.cpp +++ b/samples/sample_benchmark.cpp @@ -1250,159 +1250,29 @@ class BenchmarkSmash : public Sample b2CreatePolygonShape( bodyId, &shapeDef, &box ); } - m_created = false; - } - - void CreateScene1() - { - ImGuiIO& io = ImGui::GetIO(); - if ( io.Fonts->Fonts.size() == 0 ) { - return; - } - - const ImFont* font = io.Fonts->Fonts[0]; - const unsigned char* pixels = font->ContainerAtlas->TexPixelsAlpha8; - int width = font->ContainerAtlas->TexWidth; - int height = font->ContainerAtlas->TexHeight; + float d = 0.4f; + b2Polygon box = b2MakeSquare( 0.5f * d ); - float scale = 0.1f; + b2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = b2_dynamicBody; + bodyDef.isAwake = false; - b2BodyDef bodyDef = b2DefaultBodyDef(); - bodyDef.type = b2_dynamicBody; - bodyDef.isAwake = false; + b2ShapeDef shapeDef = b2DefaultShapeDef(); - b2ShapeDef shapeDef = b2DefaultShapeDef(); + int columns = g_sampleDebug ? 20 : 120; + int rows = g_sampleDebug ? 10 : 80; - for ( int i = 0; i < height; ++i ) - { - for ( int j = 0; j < width; ++j ) + for ( int i = 0; i < columns; ++i ) { - unsigned char value = pixels[i * width + j]; - if ( value != 0 && value != 0xFF ) - { - value += 0; - } - - if ( value > 50 ) + for ( int j = 0; j < rows; ++j ) { - b2Polygon square = b2MakeSquare( 0.95f * scale * ( value / 255.0f ) ); - bodyDef.position = { 2.0f * j * scale, 2.0f * ( height - i ) * scale - 10.0f }; + bodyDef.position.x = i * d + 30.0f; + bodyDef.position.y = ( j - rows / 2.0f ) * d; b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); - b2CreatePolygonShape( bodyId, &shapeDef, &square ); - } - } - } - - m_created = true; - } - - void CreateScene2() - { - ImGuiIO& io = ImGui::GetIO(); - if ( io.Fonts->Fonts.size() == 0 ) - { - return; - } - - const ImFont* font = io.Fonts->Fonts.back(); - const unsigned char* pixels = font->ContainerAtlas->TexPixelsAlpha8; - int width = font->ContainerAtlas->TexWidth; - int height = font->ContainerAtlas->TexHeight; - int fontSize = font->Ascent; - - float scale = 0.1f; - - b2BodyDef bodyDef = b2DefaultBodyDef(); - bodyDef.type = b2_dynamicBody; - bodyDef.isAwake = false; - - b2ShapeDef shapeDef = b2DefaultShapeDef(); - - const char* text = "I"; - int n = (int)strlen( text ); - float zoom = 1.0f; - - float x = 0.0f; - for ( int k = 0; k < n; ++k ) - { - const ImFontGlyph* glyph = font->FindGlyph( text[k] ); - float x1 = glyph->X0; - float x2 = glyph->X1; - float y1 = glyph->Y0; - float y2 = glyph->Y1; - float u1 = glyph->U0; - float v1 = glyph->V0; - float u2 = glyph->U1; - float v2 = glyph->V1; - - float w = zoom * ( x2 - x1 ); - float h = zoom * ( y2 - y1 ); - - int gridx = int( w ); - int gridy = int( h ); - for ( int i = 0; i < gridy; ++i ) - { - float v = v1 + i / h * ( v2 - v1 ); - int iy = int( v * height ); - - for ( int j = 0; j < gridx; ++j ) - { - float u = u1 + j / w * ( u2 - u1 ); - int ix = int( u * width ); - - unsigned char value = pixels[iy * width + ix]; - if ( value > 50 ) - { - b2Polygon square = b2MakeSquare( 0.9f * scale * value / 255.0f ); - bodyDef.position = { x + 2.0f * ( zoom * x1 + j ) * scale, -2.0f * ( zoom * y1 + i ) * scale + 13.0f }; - b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); - b2CreatePolygonShape( bodyId, &shapeDef, &square ); - } + b2CreatePolygonShape( bodyId, &shapeDef, &box ); } } - - x += 2.0f * zoom * scale * glyph->AdvanceX; - } - - m_created = true; - } - - void CreateScene3() - { - float d = 0.4f; - b2Polygon box = b2MakeSquare( 0.5f * d ); - - b2BodyDef bodyDef = b2DefaultBodyDef(); - bodyDef.type = b2_dynamicBody; - bodyDef.isAwake = false; - - b2ShapeDef shapeDef = b2DefaultShapeDef(); - - int columns = g_sampleDebug ? 20 : 120; - int rows = g_sampleDebug ? 10 : 80; - - for ( int i = 0; i < columns; ++i ) - { - for ( int j = 0; j < rows; ++j ) - { - bodyDef.position.x = i * d + 30.0f; - bodyDef.position.y = ( j - rows / 2.0f ) * d; - b2BodyId bodyId = b2CreateBody( m_worldId, &bodyDef ); - b2CreatePolygonShape( bodyId, &shapeDef, &box ); - } - } - - m_created = true; - } - - void Step( Settings& settings ) override - { - Sample::Step( settings ); - - if ( m_created == false ) - { - CreateScene3(); } } @@ -1410,8 +1280,6 @@ class BenchmarkSmash : public Sample { return new BenchmarkSmash( settings ); } - - bool m_created; }; static int sampleSmash = RegisterSample( "Benchmark", "Smash", BenchmarkSmash::Create ); @@ -1589,7 +1457,7 @@ class BenchmarkCast : public Sample { g_camera.m_center = { 500.0f, 500.0f }; g_camera.m_zoom = 25.0f * 21.0f; - settings.drawShapes = g_sampleDebug; + // settings.drawShapes = g_sampleDebug; } m_queryType = e_circleCast; @@ -1977,7 +1845,6 @@ class BenchmarkCast : public Sample static int sampleCast = RegisterSample( "Benchmark", "Cast", BenchmarkCast::Create ); - class BenchmarkSpinner : public Sample { public: @@ -2072,17 +1939,17 @@ class BenchmarkSpinner : public Sample int remainder = i % 3; if ( remainder == 0 ) { - //shapeDef.customColor = b2_colorYellow; + // shapeDef.customColor = b2_colorYellow; b2CreateCapsuleShape( bodyId, &shapeDef, &capsule ); } else if ( remainder == 1 ) { - //shapeDef.customColor = b2_colorYellowGreen; + // shapeDef.customColor = b2_colorYellowGreen; b2CreateCircleShape( bodyId, &shapeDef, &circle ); } else if ( remainder == 2 ) { - //shapeDef.customColor = b2_colorGreenYellow; + // shapeDef.customColor = b2_colorGreenYellow; b2CreatePolygonShape( bodyId, &shapeDef, &square ); } diff --git a/samples/sample_events.cpp b/samples/sample_events.cpp index aa3a29484..3dbbc6eb3 100644 --- a/samples/sample_events.cpp +++ b/samples/sample_events.cpp @@ -925,6 +925,11 @@ class Platformer : public Sample Sample::Step( settings ); + b2ContactData contactData = {}; + int contactCount = b2Body_GetContactData( m_platformId, &contactData, 1 ); + g_draw.DrawString( 5, m_textLine, "Platform contact count = %d, point count = %d", contactCount, contactData.manifold.pointCount ); + m_textLine += m_textIncrement; + g_draw.DrawString( 5, m_textLine, "Movement: A/D/Space" ); m_textLine += m_textIncrement; diff --git a/src/contact.c b/src/contact.c index 245a70570..06641a1aa 100644 --- a/src/contact.c +++ b/src/contact.c @@ -516,11 +516,11 @@ bool b2UpdateContact( b2World* world, b2ContactSim* contactSim, b2Shape* shapeA, } else { + // Save old manifold b2Manifold oldManifold = contactSim->manifold; - // Compute TOI + // Compute new manifold b2ManifoldFcn* fcn = s_registers[shapeA->type][shapeB->type].fcn; - contactSim->manifold = fcn( shapeA, transformA, shapeB, transformB, &contactSim->cache ); int pointCount = contactSim->manifold.pointCount; @@ -536,6 +536,7 @@ bool b2UpdateContact( b2World* world, b2ContactSim* contactSim, b2Shape* shapeA, if ( touching == false ) { // disable contact + pointCount = 0; contactSim->manifold.pointCount = 0; } } diff --git a/src/core.h b/src/core.h index cd881837a..c76ad4ce1 100644 --- a/src/core.h +++ b/src/core.h @@ -159,13 +159,9 @@ extern float b2_lengthUnitsPerMeter; #define b2_speculativeDistance ( 4.0f * b2_linearSlop ) // This is used to fatten AABBs in the dynamic tree. This allows proxies -// to move by a small amount without triggering a tree adjustment. -// This is in meters. +// to move by a small amount without triggering a tree adjustment. This is in meters. // @warning modifying this can have a significant impact on performance -#define b2_aabbMargin ( 0.1f * b2_lengthUnitsPerMeter ) - -// todo testing -#define b2_aabbVelocityScale 0.0f +#define b2_aabbMargin ( 0.05f * b2_lengthUnitsPerMeter ) // The time that a body must be still before it will go to sleep. In seconds. #define b2_timeToSleep 0.5f diff --git a/src/geometry.c b/src/geometry.c index 09445a2b5..3ef31775f 100644 --- a/src/geometry.c +++ b/src/geometry.c @@ -540,7 +540,7 @@ b2CastOutput b2RayCastCircle( const b2RayCastInput* input, const b2Circle* shape return output; } - // Pythagorus + // Pythagoras float h = sqrtf( rr - cc ); float fraction = t - h; diff --git a/src/math_functions.c b/src/math_functions.c index 60f62a16b..2ee07a2ae 100644 --- a/src/math_functions.c +++ b/src/math_functions.c @@ -141,3 +141,14 @@ b2CosSin b2ComputeCosSin( float angle ) q = b2NormalizeRot( q ); return ( b2CosSin ){ q.c, q.s }; } + +b2Rot b2ComputeRotationBetweenUnitVectors(b2Vec2 v1, b2Vec2 v2) +{ + B2_ASSERT( b2AbsFloat( 1.0f - b2Length( v1 ) ) < 100.0f * FLT_EPSILON ); + B2_ASSERT( b2AbsFloat( 1.0f - b2Length( v2 ) ) < 100.0f * FLT_EPSILON ); + + b2Rot rot; + rot.c = b2Dot( v1, v2 ); + rot.s = b2Cross( v1, v2 ); + return b2NormalizeRot( rot ); +} diff --git a/src/shape.h b/src/shape.h index 8e9ce037d..560d2b0b8 100644 --- a/src/shape.h +++ b/src/shape.h @@ -80,7 +80,5 @@ b2DistanceProxy b2MakeShapeDistanceProxy( const b2Shape* shape ); b2CastOutput b2RayCastShape( const b2RayCastInput* input, const b2Shape* shape, b2Transform transform ); b2CastOutput b2ShapeCastShape( const b2ShapeCastInput* input, const b2Shape* shape, b2Transform transform ); -b2Transform b2GetOwnerTransform( b2World* world, b2Shape* shape ); - B2_ARRAY_INLINE( b2ChainShape, b2ChainShape ); B2_ARRAY_INLINE( b2Shape, b2Shape ); diff --git a/src/solver.c b/src/solver.c index b7f112cfd..7e2735800 100644 --- a/src/solver.c +++ b/src/solver.c @@ -340,7 +340,6 @@ static void b2FinalizeBodiesTask( int startIndex, int endIndex, uint32_t threadI { // The AABB is updated after continuous collision. // Add to moved shapes regardless of AABB changes. - // todo_erin this blocks predicted AABB extension from helping shape->isFast = true; // Bit-set to keep the move array sorted @@ -359,39 +358,11 @@ static void b2FinalizeBodiesTask( int startIndex, int endIndex, uint32_t threadI if ( b2AABB_Contains( shape->fatAABB, aabb ) == false ) { -#if 0 - float deltax = b2_aabbVelocityScale * v.x * timeStep; - float deltay = b2_aabbVelocityScale * v.y * timeStep; - - b2AABB fatAABB = aabb; - if (deltax > 0.0f) - { - fatAABB.lowerBound.x -= aabbMargin; - fatAABB.upperBound.x += deltax + aabbMargin; - } - else - { - fatAABB.lowerBound.x += deltax - aabbMargin; - fatAABB.upperBound.x += aabbMargin; - } - - if (deltay > 0.0f) - { - fatAABB.lowerBound.y -= aabbMargin; - fatAABB.upperBound.y += deltay + aabbMargin; - } - else - { - fatAABB.lowerBound.y += deltay - aabbMargin; - fatAABB.upperBound.y += aabbMargin; - } -#else b2AABB fatAABB; fatAABB.lowerBound.x = aabb.lowerBound.x - aabbMargin; fatAABB.lowerBound.y = aabb.lowerBound.y - aabbMargin; fatAABB.upperBound.x = aabb.upperBound.x + aabbMargin; fatAABB.upperBound.y = aabb.upperBound.y + aabbMargin; -#endif shape->fatAABB = fatAABB; shape->enlargedAABB = true; @@ -974,9 +945,6 @@ static void b2SolveContinuous( b2World* world, int bodySimIndex ) b2BodySim* fastBodySim = b2BodySimArray_Get( &awakeSet->bodySims, bodySimIndex ); B2_ASSERT( fastBodySim->isFast ); - // todo_erin testing predicted aabb expansion - //b2BodyState* bodyState = b2BodyStateArray_Get( &awakeSet->bodyStates, bodySimIndex ); - b2Sweep sweep = b2MakeSweep( fastBodySim ); b2Transform xf1; @@ -1040,12 +1008,6 @@ static void b2SolveContinuous( b2World* world, int bodySimIndex ) const float speculativeDistance = b2_speculativeDistance; const float aabbMargin = b2_aabbMargin; -#if 0 - b2Vec2 v = bodyState->linearVelocity; - float deltax = b2_aabbVelocityScale * v.x * 1.0f / 60.0f; - float deltay = b2_aabbVelocityScale * v.y * 1.0f / 60.0f; -#endif - if ( context.fraction < 1.0f ) { // Handle time of impact event diff --git a/src/types.c b/src/types.c index 67b437d12..4a3bbe074 100644 --- a/src/types.c +++ b/src/types.c @@ -60,6 +60,7 @@ b2ShapeDef b2DefaultShapeDef( void ) def.density = 1.0f; def.filter = b2DefaultFilter(); def.enableSensorEvents = true; + // todo_erin this might be too costly to have on my default def.enableContactEvents = true; def.updateBodyMass = true; def.internalValue = B2_SECRET_COOKIE; diff --git a/test/test_determinism.c b/test/test_determinism.c index 662e0398a..933b7deb7 100644 --- a/test/test_determinism.c +++ b/test/test_determinism.c @@ -330,8 +330,8 @@ static int CrossPlatformTest(void) } ENSURE( stepCount < maxSteps ); - ENSURE( sleepStep == 313 ); - ENSURE( hash == 0x1fc667fa ); + ENSURE( sleepStep == 281 ); + ENSURE( hash == 0x7efc22e7 ); free( bodies ); diff --git a/test/test_math.c b/test/test_math.c index 1878e7323..a00e406e9 100644 --- a/test/test_math.c +++ b/test/test_math.c @@ -124,5 +124,25 @@ int MathTest( void ) ENSURE_SMALL( v.x - two.x, 8.0f * FLT_EPSILON ); ENSURE_SMALL( v.y - two.y, 8.0f * FLT_EPSILON ); + v = b2Normalize(( b2Vec2 ){ 0.2f, -0.5f }); + for ( float y = -1.0f; y <= 1.0f; y += 0.01f ) + { + for ( float x = -1.0f; x <= 1.0f; x += 0.01f ) + { + if (x == 0.0f && y == 0.0f) + { + continue; + } + + u = b2Normalize( ( b2Vec2 ){ x, y } ); + + b2Rot r = b2ComputeRotationBetweenUnitVectors( v, u ); + + b2Vec2 w = b2RotateVector( r, v ); + ENSURE_SMALL( w.x - u.x, 4.0f * FLT_EPSILON ); + ENSURE_SMALL( w.y - u.y, 4.0f * FLT_EPSILON ); + } + } + return 0; }