babylon-box3d 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,10 +92,15 @@ typedef struct bxBody
92
92
  b3BodyId id;
93
93
  int world;
94
94
  int alive;
95
+ // begin/end touch events and hit events on this body's shapes (Babylon's COLLISION_STARTED/FINISHED and CONTINUED)
95
96
  int contactEvents;
97
+ int hitEvents;
96
98
  int shapeDesc;
97
99
  b3ShapeId* shapes;
100
+ // the description each shape came from, and the description owning its geometry: the same, except for a mesh in a
101
+ // container, where a transformed copy of the mesh is baked for this body (see bxInstantiate)
98
102
  int* shapeDescs;
103
+ int* geomDescs;
99
104
  int shapeCount;
100
105
  int shapeCapacity;
101
106
  bxHelper* helpers;
@@ -103,6 +108,8 @@ typedef struct bxBody
103
108
  int helperCapacity;
104
109
  } bxBody;
105
110
 
111
+ static void bxReleaseBodyShapes( bxBody* body, int destroyShapes );
112
+
106
113
  static bxBody* s_bodies;
107
114
  static int s_bodyCount;
108
115
  static int s_bodyCapacity;
@@ -162,6 +169,8 @@ typedef struct bxJoint
162
169
  {
163
170
  b3JointId id;
164
171
  int alive;
172
+ // spherical motor target as a relative angular velocity in joint frame A, converted to world space every step
173
+ b3Vec3 motorVelocity;
165
174
  } bxJoint;
166
175
 
167
176
  static bxJoint* s_joints;
@@ -210,10 +219,23 @@ static int bxAllocJoint( void )
210
219
  return bxAllocSlot( (void**)&s_joints, sizeof( bxJoint ), &s_jointCount, &s_jointCapacity, &s_freeJoints, &s_freeJointCount );
211
220
  }
212
221
 
222
+ /// Slot lookups that failed on a non zero slot. Every entry point returns quietly when it does not recognize a slot,
223
+ /// which is the right thing during teardown but hides the two ways this can go wrong for real: a body used after it
224
+ /// was destroyed, and a box3d.js loader paired with a box3d.wasm from a different build, where the exports line up
225
+ /// with the wrong functions. The plugin reads this and says so once.
226
+ static int s_rejectedSlots;
227
+
228
+ /// Rejected slot lookups since the module was created. Slot 0 means "none" and is not counted.
229
+ BX_EXPORT int bx_GetRejectedSlotCount( void )
230
+ {
231
+ return s_rejectedSlots;
232
+ }
233
+
213
234
  static bxBody* bxGetBody( int slot )
214
235
  {
215
236
  if ( slot <= 0 || slot >= s_bodyCount || s_bodies[slot].alive == 0 )
216
237
  {
238
+ s_rejectedSlots += slot > 0 ? 1 : 0;
217
239
  return NULL;
218
240
  }
219
241
  return s_bodies + slot;
@@ -223,6 +245,7 @@ static bxShapeDesc* bxGetDesc( int slot )
223
245
  {
224
246
  if ( slot <= 0 || slot >= s_descCount || s_descs[slot].alive == 0 )
225
247
  {
248
+ s_rejectedSlots += slot > 0 ? 1 : 0;
226
249
  return NULL;
227
250
  }
228
251
  return s_descs + slot;
@@ -246,9 +269,12 @@ static b3WorldId bxGetWorld( int slot )
246
269
  {
247
270
  if ( slot <= 0 || slot > BX_MAX_WORLDS )
248
271
  {
272
+ s_rejectedSlots += slot > 0 ? 1 : 0;
249
273
  return b3_nullWorldId;
250
274
  }
251
- return s_worlds[slot - 1];
275
+ b3WorldId world = s_worlds[slot - 1];
276
+ s_rejectedSlots += B3_IS_NULL( world ) ? 1 : 0;
277
+ return world;
252
278
  }
253
279
 
254
280
  static int bxBodySlotFromId( b3BodyId bodyId )
@@ -374,8 +400,12 @@ BX_EXPORT void bx_DestroyWorld( int w )
374
400
  {
375
401
  if ( s_bodies[i].alive && s_bodies[i].world == w )
376
402
  {
403
+ // the world is gone, so the shapes are too, but their geometry still has to be released: mesh and height
404
+ // field descriptions are reference counted and a mesh baked for this body is owned by it
405
+ bxReleaseBodyShapes( s_bodies + i, 0 );
377
406
  free( s_bodies[i].shapes );
378
407
  free( s_bodies[i].shapeDescs );
408
+ free( s_bodies[i].geomDescs );
379
409
  free( s_bodies[i].helpers );
380
410
  memset( s_bodies + i, 0, sizeof( bxBody ) );
381
411
  s_freeBodies[s_freeBodyCount++] = i;
@@ -466,6 +496,14 @@ BX_EXPORT int bx_World_GetAwakeBodyCount( int w )
466
496
  return B3_IS_NON_NULL( worldId ) ? b3World_GetAwakeBodyCount( worldId ) : 0;
467
497
  }
468
498
 
499
+ /// Number of joint force/torque threshold events from the last step. The plugin does not consume them; with the
500
+ /// default thresholds (FLT_MAX) this stays 0.
501
+ BX_EXPORT int bx_World_GetJointEventCount( int w )
502
+ {
503
+ b3WorldId worldId = bxGetWorld( w );
504
+ return B3_IS_NON_NULL( worldId ) ? b3World_GetJointEvents( worldId ).count : 0;
505
+ }
506
+
469
507
  /// scratch: [bodyCount, shapeCount, contactCount, jointCount, islandCount, stepMs, collideMs, solveMs]
470
508
  BX_EXPORT void bx_World_GetStats( int w )
471
509
  {
@@ -551,7 +589,46 @@ BX_EXPORT int bx_World_GetMoveEvents( int w )
551
589
  return count;
552
590
  }
553
591
 
554
- /// Each record: [kind (0 begin, 1 end, 2 hit), shapeDescA, shapeDescB, bodyA, bodyB, px, py, pz, nx, ny, nz, approachSpeed]
592
+ /// Writes the contact point, normal (from shape A to shape B) and the total normal impulse of a begin touch event,
593
+ /// taken from the contact's first manifold. Havok reports these on collision started events too.
594
+ static void bxWriteBeginTouchData( float* out, b3ContactId contactId, b3ShapeId shapeIdA )
595
+ {
596
+ memset( out, 0, 7 * sizeof( float ) );
597
+ if ( b3Contact_IsValid( contactId ) == false )
598
+ {
599
+ return;
600
+ }
601
+ b3ContactData data = b3Contact_GetData( contactId );
602
+ if ( data.manifoldCount <= 0 || data.manifolds[0].pointCount <= 0 )
603
+ {
604
+ return;
605
+ }
606
+ const b3Manifold* manifold = data.manifolds + 0;
607
+ // manifold anchors are relative to the center of mass of the contact's body A, which may be the event's shape B
608
+ int flipped = B3_ID_EQUALS( data.shapeIdA, shapeIdA ) == false;
609
+ b3Pos center = b3Body_GetWorldCenter( b3Shape_GetBody( data.shapeIdA ) );
610
+ b3Vec3 anchor = b3Vec3_zero;
611
+ float impulse = 0.0f;
612
+ for ( int i = 0; i < manifold->pointCount; ++i )
613
+ {
614
+ anchor = b3Add( anchor, manifold->points[i].anchorA );
615
+ impulse += manifold->points[i].totalNormalImpulse;
616
+ }
617
+ anchor = b3MulSV( 1.0f / (float)manifold->pointCount, anchor );
618
+ b3Pos point = b3OffsetPos( center, anchor );
619
+ b3Vec3 normal = flipped ? b3Neg( manifold->normal ) : manifold->normal;
620
+ out[0] = point.x;
621
+ out[1] = point.y;
622
+ out[2] = point.z;
623
+ out[3] = normal.x;
624
+ out[4] = normal.y;
625
+ out[5] = normal.z;
626
+ out[6] = impulse;
627
+ }
628
+
629
+ /// Each record: [kind (0 begin, 1 end, 2 hit), shapeDescA, shapeDescB, bodyA, bodyB, px, py, pz, nx, ny, nz, value]
630
+ /// Begin events carry the contact point, normal and the total normal impulse of the step, hit events the point, normal
631
+ /// and approach speed, end events nothing.
555
632
  BX_EXPORT int bx_World_GetContactEvents( int w )
556
633
  {
557
634
  b3WorldId worldId = bxGetWorld( w );
@@ -580,7 +657,7 @@ BX_EXPORT int bx_World_GetContactEvents( int w )
580
657
  out[2] = (float)bxDescSlotFromShape( e->shapeIdB );
581
658
  out[3] = (float)bodyA;
582
659
  out[4] = (float)bodyB;
583
- memset( out + 5, 0, 7 * sizeof( float ) );
660
+ bxWriteBeginTouchData( out + 5, e->contactId, e->shapeIdA );
584
661
  out += BX_CONTACT_EVENT_STRIDE;
585
662
  count += 1;
586
663
  }
@@ -722,6 +799,9 @@ static void bxDestroyHelpers( bxBody* body )
722
799
  body->helperCount = 0;
723
800
  }
724
801
 
802
+ static void bxDescRelease( int slot );
803
+ BX_EXPORT void bx_ShapeDesc_Destroy( int slot );
804
+
725
805
  static void bxReleaseBodyShapes( bxBody* body, int destroyShapes )
726
806
  {
727
807
  if ( destroyShapes )
@@ -730,15 +810,17 @@ static void bxReleaseBodyShapes( bxBody* body, int destroyShapes )
730
810
  }
731
811
  for ( int i = 0; i < body->shapeCount; ++i )
732
812
  {
733
- bxShapeDesc* desc = bxGetDesc( body->shapeDescs[i] );
734
- if ( desc != NULL && ( desc->kind == bx_meshDesc || desc->kind == bx_heightFieldDesc ) )
735
- {
736
- desc->refCount -= 1;
737
- }
738
813
  if ( destroyShapes && b3Shape_IsValid( body->shapes[i] ) )
739
814
  {
740
815
  b3DestroyShape( body->shapes[i], false );
741
816
  }
817
+ int geomSlot = body->geomDescs[i];
818
+ if ( geomSlot != body->shapeDescs[i] )
819
+ {
820
+ // a mesh baked for this body alone, nothing else refers to it
821
+ bx_ShapeDesc_Destroy( geomSlot );
822
+ }
823
+ bxDescRelease( geomSlot );
742
824
  }
743
825
  body->shapeCount = 0;
744
826
  }
@@ -750,15 +832,16 @@ BX_EXPORT void bx_DestroyBody( int slot )
750
832
  {
751
833
  return;
752
834
  }
753
- // b3DestroyBody destroys attached shapes and joints
835
+ // b3DestroyBody destroys attached shapes and joints; free the geometry only once no shape can refer to it
754
836
  bxDestroyHelpers( body );
755
- bxReleaseBodyShapes( body, 0 );
756
837
  if ( b3Body_IsValid( body->id ) )
757
838
  {
758
839
  b3DestroyBody( body->id );
759
840
  }
841
+ bxReleaseBodyShapes( body, 0 );
760
842
  free( body->shapes );
761
843
  free( body->shapeDescs );
844
+ free( body->geomDescs );
762
845
  free( body->helpers );
763
846
  memset( body, 0, sizeof( bxBody ) );
764
847
  s_freeBodies[s_freeBodyCount++] = slot;
@@ -966,7 +1049,67 @@ BX_EXPORT void bx_Body_GetAABB( int slot )
966
1049
  bxWriteVec3( 3, aabb.upperBound );
967
1050
  }
968
1051
 
969
- /// scratch: [mass, cx, cy, cz, ixx, iyy, izz]
1052
+ /// Mass data the body's shapes would give it, whatever its type. Box3D keeps mass 0 on static and kinematic bodies,
1053
+ /// while Havok reports the shape mass for every body, so the plugin uses this for its mass property getters.
1054
+ /// scratch: [mass, cx, cy, cz, ixx, iyy, izz, ixy, ixz, iyz]
1055
+ BX_EXPORT void bx_Body_ComputeShapeMassData( int slot )
1056
+ {
1057
+ memset( s_scratch, 0, 10 * sizeof( float ) );
1058
+ bxBody* body = bxGetBody( slot );
1059
+ if ( body == NULL || b3Body_IsValid( body->id ) == false )
1060
+ {
1061
+ return;
1062
+ }
1063
+ float mass = 0.0f;
1064
+ b3Vec3 center = b3Vec3_zero;
1065
+ for ( int i = 0; i < body->shapeCount; ++i )
1066
+ {
1067
+ if ( b3Shape_IsValid( body->shapes[i] ) == false )
1068
+ {
1069
+ continue;
1070
+ }
1071
+ b3MassData md = b3Shape_ComputeMassData( body->shapes[i] );
1072
+ mass += md.mass;
1073
+ center = b3MulAdd( center, md.mass, md.center );
1074
+ }
1075
+ if ( mass <= 0.0f )
1076
+ {
1077
+ return;
1078
+ }
1079
+ center = b3MulSV( 1.0f / mass, center );
1080
+ // parallel axis theorem: shift each shape's inertia to the combined center of mass
1081
+ b3Matrix3 inertia = b3Mat3_zero;
1082
+ for ( int i = 0; i < body->shapeCount; ++i )
1083
+ {
1084
+ if ( b3Shape_IsValid( body->shapes[i] ) == false )
1085
+ {
1086
+ continue;
1087
+ }
1088
+ b3MassData md = b3Shape_ComputeMassData( body->shapes[i] );
1089
+ b3Vec3 d = b3Sub( md.center, center );
1090
+ float dd = b3Dot( d, d );
1091
+ inertia = b3AddMM( inertia, md.inertia );
1092
+ inertia.cx.x += md.mass * ( dd - d.x * d.x );
1093
+ inertia.cy.y += md.mass * ( dd - d.y * d.y );
1094
+ inertia.cz.z += md.mass * ( dd - d.z * d.z );
1095
+ inertia.cy.x -= md.mass * d.x * d.y;
1096
+ inertia.cx.y -= md.mass * d.x * d.y;
1097
+ inertia.cz.x -= md.mass * d.x * d.z;
1098
+ inertia.cx.z -= md.mass * d.x * d.z;
1099
+ inertia.cz.y -= md.mass * d.y * d.z;
1100
+ inertia.cy.z -= md.mass * d.y * d.z;
1101
+ }
1102
+ s_scratch[0] = mass;
1103
+ bxWriteVec3( 1, center );
1104
+ s_scratch[4] = inertia.cx.x;
1105
+ s_scratch[5] = inertia.cy.y;
1106
+ s_scratch[6] = inertia.cz.z;
1107
+ s_scratch[7] = inertia.cy.x;
1108
+ s_scratch[8] = inertia.cz.x;
1109
+ s_scratch[9] = inertia.cz.y;
1110
+ }
1111
+
1112
+ /// scratch: [mass, cx, cy, cz, ixx, iyy, izz, ixy, ixz, iyz], the inertia tensor about the center of mass
970
1113
  BX_EXPORT void bx_Body_GetMassData( int slot )
971
1114
  {
972
1115
  BX_BODY( slot );
@@ -976,21 +1119,43 @@ BX_EXPORT void bx_Body_GetMassData( int slot )
976
1119
  s_scratch[4] = md.inertia.cx.x;
977
1120
  s_scratch[5] = md.inertia.cy.y;
978
1121
  s_scratch[6] = md.inertia.cz.z;
1122
+ s_scratch[7] = md.inertia.cy.x;
1123
+ s_scratch[8] = md.inertia.cz.x;
1124
+ s_scratch[9] = md.inertia.cz.y;
979
1125
  }
980
1126
 
981
- BX_EXPORT void bx_Body_SetMassData( int slot, float mass, float cx, float cy, float cz, float ixx, float iyy, float izz )
1127
+ /// Full symmetric inertia tensor, needed for Babylon mass properties with an inertia orientation.
1128
+ BX_EXPORT void bx_Body_SetMassDataFull( int slot, float mass, float cx, float cy, float cz, float ixx, float iyy, float izz,
1129
+ float ixy, float ixz, float iyz )
982
1130
  {
983
1131
  BX_BODY( slot );
984
1132
  b3MassData md;
985
1133
  md.mass = mass;
986
1134
  md.center = bxVec3( cx, cy, cz );
987
- md.inertia = b3Mat3_zero;
988
- md.inertia.cx.x = ixx;
989
- md.inertia.cy.y = iyy;
990
- md.inertia.cz.z = izz;
1135
+ md.inertia.cx = bxVec3( ixx, ixy, ixz );
1136
+ md.inertia.cy = bxVec3( ixy, iyy, iyz );
1137
+ md.inertia.cz = bxVec3( ixz, iyz, izz );
991
1138
  b3Body_SetMassData( body->id, md );
992
1139
  }
993
1140
 
1141
+ BX_EXPORT void bx_Body_SetMassData( int slot, float mass, float cx, float cy, float cz, float ixx, float iyy, float izz )
1142
+ {
1143
+ bx_Body_SetMassDataFull( slot, mass, cx, cy, cz, ixx, iyy, izz, 0.0f, 0.0f, 0.0f );
1144
+ }
1145
+
1146
+ /// scratch: [lx, ly, lz, ax, ay, az], 1 where the motion is locked
1147
+ BX_EXPORT void bx_Body_GetMotionLocks( int slot )
1148
+ {
1149
+ BX_BODY( slot );
1150
+ b3MotionLocks locks = b3Body_GetMotionLocks( body->id );
1151
+ s_scratch[0] = locks.linearX ? 1.0f : 0.0f;
1152
+ s_scratch[1] = locks.linearY ? 1.0f : 0.0f;
1153
+ s_scratch[2] = locks.linearZ ? 1.0f : 0.0f;
1154
+ s_scratch[3] = locks.angularX ? 1.0f : 0.0f;
1155
+ s_scratch[4] = locks.angularY ? 1.0f : 0.0f;
1156
+ s_scratch[5] = locks.angularZ ? 1.0f : 0.0f;
1157
+ }
1158
+
994
1159
  BX_EXPORT void bx_Body_ApplyMassFromShapes( int slot )
995
1160
  {
996
1161
  BX_BODY( slot );
@@ -1003,20 +1168,34 @@ BX_EXPORT int bx_Body_GetShapeCount( int slot )
1003
1168
  return body->shapeCount;
1004
1169
  }
1005
1170
 
1006
- BX_EXPORT void bx_Body_EnableContactEvents( int slot, int flag )
1171
+ /// Begin/end touch events and hit events are enabled separately so a body only pays for the events it asked for.
1172
+ BX_EXPORT void bx_Body_SetEventFlags( int slot, int contactEvents, int hitEvents )
1007
1173
  {
1008
1174
  BX_BODY( slot );
1009
- body->contactEvents = flag != 0;
1175
+ body->contactEvents = contactEvents != 0;
1176
+ body->hitEvents = hitEvents != 0;
1010
1177
  for ( int i = 0; i < body->shapeCount; ++i )
1011
1178
  {
1012
1179
  if ( b3Shape_IsValid( body->shapes[i] ) )
1013
1180
  {
1014
- b3Shape_EnableContactEvents( body->shapes[i], flag != 0 );
1015
- b3Shape_EnableHitEvents( body->shapes[i], flag != 0 );
1181
+ b3Shape_EnableContactEvents( body->shapes[i], body->contactEvents != 0 );
1182
+ b3Shape_EnableHitEvents( body->shapes[i], body->hitEvents != 0 );
1016
1183
  }
1017
1184
  }
1018
1185
  }
1019
1186
 
1187
+ /// Bit 1: begin/end touch events, bit 2: hit events.
1188
+ BX_EXPORT int bx_Body_GetEventFlags( int slot )
1189
+ {
1190
+ BX_BODY_RET( slot, 0 );
1191
+ return ( body->contactEvents ? 1 : 0 ) | ( body->hitEvents ? 2 : 0 );
1192
+ }
1193
+
1194
+ BX_EXPORT void bx_Body_EnableContactEvents( int slot, int flag )
1195
+ {
1196
+ bx_Body_SetEventFlags( slot, flag, flag );
1197
+ }
1198
+
1020
1199
  // ---------------------------------------------------------------------------------------------
1021
1200
  // Shape descriptions
1022
1201
  // ---------------------------------------------------------------------------------------------
@@ -1133,8 +1312,10 @@ BX_EXPORT int bx_ShapeDesc_CreateMesh( const float* vertices, int vertexCount, c
1133
1312
  }
1134
1313
 
1135
1314
  /// heights: countX * countZ floats indexed [z * countX + x]
1136
- BX_EXPORT int bx_ShapeDesc_CreateHeightField( const float* heights, int countX, int countZ, float sx, float sy, float sz,
1137
- int clockwise, float ox, float oy, float oz )
1315
+ /// materials: (countX - 1) * (countZ - 1) cell material indices indexed [z * (countX - 1) + x], or NULL for every cell 0.
1316
+ /// A cell of B3_HEIGHT_FIELD_HOLE (255) is a hole: nothing collides with it and rays pass through. Box3D copies them.
1317
+ BX_EXPORT int bx_ShapeDesc_CreateHeightField( const float* heights, const unsigned char* materials, int countX, int countZ,
1318
+ float sx, float sy, float sz, int clockwise, float ox, float oy, float oz )
1138
1319
  {
1139
1320
  float minHeight = heights[0];
1140
1321
  float maxHeight = heights[0];
@@ -1155,6 +1336,7 @@ BX_EXPORT int bx_ShapeDesc_CreateHeightField( const float* heights, int countX,
1155
1336
  }
1156
1337
  b3HeightFieldDef def = { 0 };
1157
1338
  def.heights = (float*)heights;
1339
+ def.materialIndices = (uint8_t*)materials;
1158
1340
  def.countX = countX;
1159
1341
  def.countZ = countZ;
1160
1342
  def.scale = bxVec3( sx, sy, sz );
@@ -1473,28 +1655,106 @@ static void bxDescRelease( int slot )
1473
1655
  // Shape instantiation on bodies
1474
1656
  // ---------------------------------------------------------------------------------------------
1475
1657
 
1476
- static void bxBodyPushShape( bxBody* body, b3ShapeId shapeId, int descSlot )
1658
+ // Counts every box3d shape the shim has created. Rebuilding a shape description on its bodies is the expensive path,
1659
+ // so this makes it observable (bx_GetShapeBuildCount).
1660
+ static int s_shapeBuildCount;
1661
+
1662
+ BX_EXPORT int bx_GetShapeBuildCount( void )
1663
+ {
1664
+ return s_shapeBuildCount;
1665
+ }
1666
+
1667
+ /// Live shape descriptions, including the mesh copies baked for container children. Used to spot leaks.
1668
+ BX_EXPORT int bx_GetShapeDescCount( void )
1669
+ {
1670
+ int count = 0;
1671
+ for ( int i = 1; i < s_descCount; ++i )
1672
+ {
1673
+ if ( s_descs[i].alive != 0 || s_descs[i].refCount > 0 )
1674
+ {
1675
+ count += 1;
1676
+ }
1677
+ }
1678
+ return count;
1679
+ }
1680
+
1681
+ /// descSlot is the description the shape belongs to, geomSlot the one owning the geometry it was built from (the same,
1682
+ /// unless a transformed mesh copy was baked for this body).
1683
+ static void bxBodyPushShape( bxBody* body, b3ShapeId shapeId, int descSlot, int geomSlot )
1477
1684
  {
1478
1685
  if ( B3_IS_NULL( shapeId ) )
1479
1686
  {
1480
1687
  return;
1481
1688
  }
1689
+ s_shapeBuildCount += 1;
1482
1690
  if ( body->shapeCount >= body->shapeCapacity )
1483
1691
  {
1484
1692
  int capacity = body->shapeCapacity == 0 ? 4 : body->shapeCapacity * 2;
1485
1693
  body->shapes = (b3ShapeId*)realloc( body->shapes, (size_t)capacity * sizeof( b3ShapeId ) );
1486
1694
  body->shapeDescs = (int*)realloc( body->shapeDescs, (size_t)capacity * sizeof( int ) );
1695
+ body->geomDescs = (int*)realloc( body->geomDescs, (size_t)capacity * sizeof( int ) );
1487
1696
  body->shapeCapacity = capacity;
1488
1697
  }
1489
1698
  b3Shape_SetUserData( shapeId, (void*)(intptr_t)descSlot );
1490
1699
  body->shapes[body->shapeCount] = shapeId;
1491
1700
  body->shapeDescs[body->shapeCount] = descSlot;
1701
+ body->geomDescs[body->shapeCount] = geomSlot;
1492
1702
  body->shapeCount += 1;
1493
- bxShapeDesc* desc = bxGetDesc( descSlot );
1494
- if ( desc != NULL && ( desc->kind == bx_meshDesc || desc->kind == bx_heightFieldDesc ) )
1703
+ bxShapeDesc* geom = bxGetDesc( geomSlot );
1704
+ if ( geom != NULL && ( geom->kind == bx_meshDesc || geom->kind == bx_heightFieldDesc ) )
1705
+ {
1706
+ geom->refCount += 1;
1707
+ }
1708
+ }
1709
+
1710
+ /// Box3D mesh shapes have no local transform, so a mesh inside a container is baked into a private transformed copy of
1711
+ /// the mesh data. Returns a new description owning that copy, or 0.
1712
+ static int bxBakeTransformedMesh( const bxShapeDesc* source, b3Transform xf, b3Vec3 scale )
1713
+ {
1714
+ const b3Vec3* vertices = b3GetMeshVertices( source->mesh );
1715
+ const b3MeshTriangle* triangles = b3GetMeshTriangles( source->mesh );
1716
+ int vertexCount = source->mesh->vertexCount;
1717
+ int triangleCount = source->mesh->triangleCount;
1718
+ if ( vertices == NULL || triangles == NULL || vertexCount < 3 || triangleCount < 1 )
1719
+ {
1720
+ return 0;
1721
+ }
1722
+ b3Vec3* baked = (b3Vec3*)malloc( (size_t)vertexCount * sizeof( b3Vec3 ) );
1723
+ int32_t* indices = (int32_t*)malloc( (size_t)triangleCount * 3 * sizeof( int32_t ) );
1724
+ if ( baked == NULL || indices == NULL )
1725
+ {
1726
+ free( baked );
1727
+ free( indices );
1728
+ return 0;
1729
+ }
1730
+ for ( int i = 0; i < vertexCount; ++i )
1731
+ {
1732
+ baked[i] = b3Add( xf.p, b3RotateVector( xf.q, bxMulScale( vertices[i], scale ) ) );
1733
+ }
1734
+ // a mirroring scale flips the winding, so swap two indices to keep the triangles facing outwards
1735
+ int mirrored = scale.x * scale.y * scale.z < 0.0f;
1736
+ for ( int i = 0; i < triangleCount; ++i )
1495
1737
  {
1496
- desc->refCount += 1;
1738
+ indices[3 * i + 0] = triangles[i].index1;
1739
+ indices[3 * i + 1] = mirrored ? triangles[i].index3 : triangles[i].index2;
1740
+ indices[3 * i + 2] = mirrored ? triangles[i].index2 : triangles[i].index3;
1497
1741
  }
1742
+ b3MeshDef def = { 0 };
1743
+ def.vertices = baked;
1744
+ def.vertexCount = vertexCount;
1745
+ def.indices = indices;
1746
+ def.triangleCount = triangleCount;
1747
+ def.identifyEdges = true;
1748
+ b3MeshData* mesh = b3CreateMesh( &def, NULL, 0 );
1749
+ free( baked );
1750
+ free( indices );
1751
+ if ( mesh == NULL )
1752
+ {
1753
+ return 0;
1754
+ }
1755
+ int slot = bxNewDesc( bx_meshDesc );
1756
+ s_descs[slot].mesh = mesh;
1757
+ return slot;
1498
1758
  }
1499
1759
 
1500
1760
  static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 scale, int depth )
@@ -1517,8 +1777,9 @@ static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 sc
1517
1777
  // Visitors must opt in to be seen by sensors. Babylon triggers see everything.
1518
1778
  def.enableSensorEvents = true;
1519
1779
  def.enableContactEvents = body->contactEvents != 0;
1520
- def.enableHitEvents = body->contactEvents != 0;
1521
- def.updateBodyMass = true;
1780
+ def.enableHitEvents = body->hitEvents != 0;
1781
+ // the caller applies the mass once after instantiating every shape of the description
1782
+ def.updateBodyMass = false;
1522
1783
 
1523
1784
  switch ( desc->kind )
1524
1785
  {
@@ -1527,7 +1788,7 @@ static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 sc
1527
1788
  b3Sphere s = desc->sphere;
1528
1789
  s.center = b3Add( xf.p, b3RotateVector( xf.q, bxMulScale( s.center, scale ) ) );
1529
1790
  s.radius *= bxMaxAbsScale( scale );
1530
- bxBodyPushShape( body, b3CreateSphereShape( body->id, &def, &s ), descSlot );
1791
+ bxBodyPushShape( body, b3CreateSphereShape( body->id, &def, &s ), descSlot, descSlot );
1531
1792
  break;
1532
1793
  }
1533
1794
  case bx_capsuleDesc:
@@ -1536,26 +1797,47 @@ static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 sc
1536
1797
  c.center1 = b3Add( xf.p, b3RotateVector( xf.q, bxMulScale( c.center1, scale ) ) );
1537
1798
  c.center2 = b3Add( xf.p, b3RotateVector( xf.q, bxMulScale( c.center2, scale ) ) );
1538
1799
  c.radius *= bxMaxAbsScale( scale );
1539
- bxBodyPushShape( body, b3CreateCapsuleShape( body->id, &def, &c ), descSlot );
1800
+ bxBodyPushShape( body, b3CreateCapsuleShape( body->id, &def, &c ), descSlot, descSlot );
1540
1801
  break;
1541
1802
  }
1542
1803
  case bx_hullDesc:
1543
1804
  {
1544
1805
  if ( bxIsIdentity( xf, scale ) )
1545
1806
  {
1546
- bxBodyPushShape( body, b3CreateHullShape( body->id, &def, desc->hull ), descSlot );
1807
+ bxBodyPushShape( body, b3CreateHullShape( body->id, &def, desc->hull ), descSlot, descSlot );
1547
1808
  }
1548
1809
  else
1549
1810
  {
1550
- bxBodyPushShape( body, b3CreateTransformedHullShape( body->id, &def, desc->hull, xf, scale ), descSlot );
1811
+ bxBodyPushShape( body, b3CreateTransformedHullShape( body->id, &def, desc->hull, xf, scale ), descSlot, descSlot );
1551
1812
  }
1552
1813
  break;
1553
1814
  }
1554
1815
  case bx_meshDesc:
1555
1816
  {
1556
- // Mesh shapes have no local transform in box3d. The plugin bakes any container offset
1557
- // into the vertices at creation time, so only the scale is applied here.
1558
- bxBodyPushShape( body, b3CreateMeshShape( body->id, &def, desc->mesh, bxMulScale( desc->meshScale, scale ) ), descSlot );
1817
+ // Mesh shapes have no local transform in box3d, only a scale. A mesh that sits inside a container with an
1818
+ // offset or a rotation gets a transformed copy of its data baked for this body.
1819
+ b3Vec3 meshScale = bxMulScale( desc->meshScale, scale );
1820
+ if ( bxIsIdentity( xf, b3Vec3_one ) )
1821
+ {
1822
+ bxBodyPushShape( body, b3CreateMeshShape( body->id, &def, desc->mesh, meshScale ), descSlot, descSlot );
1823
+ }
1824
+ else
1825
+ {
1826
+ int baked = bxBakeTransformedMesh( desc, xf, meshScale );
1827
+ if ( baked != 0 )
1828
+ {
1829
+ b3ShapeId shapeId = b3CreateMeshShape( body->id, &def, s_descs[baked].mesh, b3Vec3_one );
1830
+ if ( B3_IS_NON_NULL( shapeId ) )
1831
+ {
1832
+ bxBodyPushShape( body, shapeId, descSlot, baked );
1833
+ }
1834
+ else
1835
+ {
1836
+ // nothing recorded the baked copy, so free it here instead of leaking the slot and its mesh
1837
+ bx_ShapeDesc_Destroy( baked );
1838
+ }
1839
+ }
1840
+ }
1559
1841
  break;
1560
1842
  }
1561
1843
  case bx_heightFieldDesc:
@@ -1564,7 +1846,7 @@ static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 sc
1564
1846
  b3Transform local = b3MulTransforms( xf, offset );
1565
1847
  if ( bxIsIdentity( local, b3Vec3_one ) )
1566
1848
  {
1567
- bxBodyPushShape( body, b3CreateHeightFieldShape( body->id, &def, desc->heightField ), descSlot );
1849
+ bxBodyPushShape( body, b3CreateHeightFieldShape( body->id, &def, desc->heightField ), descSlot, descSlot );
1568
1850
  }
1569
1851
  else
1570
1852
  {
@@ -1585,15 +1867,22 @@ static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 sc
1585
1867
  body->helpers[body->helperCount].id = helperId;
1586
1868
  body->helpers[body->helperCount].local = local;
1587
1869
  body->helperCount += 1;
1588
- bxBodyPushShape( body, b3CreateHeightFieldShape( helperId, &def, desc->heightField ), descSlot );
1870
+ bxBodyPushShape( body, b3CreateHeightFieldShape( helperId, &def, desc->heightField ), descSlot, descSlot );
1589
1871
  }
1590
1872
  break;
1591
1873
  }
1592
1874
  case bx_containerDesc:
1593
1875
  {
1594
- for ( int i = 0; i < desc->childCount; ++i )
1876
+ // Instantiating a child can bake a mesh, which allocates a description and may move the description table,
1877
+ // so `desc` must be re-fetched every iteration instead of being held across the recursion.
1878
+ for ( int i = 0;; ++i )
1595
1879
  {
1596
- bxChild child = desc->children[i];
1880
+ bxShapeDesc* container = bxGetDesc( descSlot );
1881
+ if ( container == NULL || i >= container->childCount )
1882
+ {
1883
+ break;
1884
+ }
1885
+ bxChild child = container->children[i];
1597
1886
  bxInstantiate( body, child.desc, b3MulTransforms( xf, child.transform ), bxMulScale( scale, child.scale ), depth + 1 );
1598
1887
  }
1599
1888
  break;
@@ -1607,22 +1896,68 @@ static void bxInstantiate( bxBody* body, int descSlot, b3Transform xf, b3Vec3 sc
1607
1896
  BX_EXPORT void bx_Body_SetShape( int slot, int descSlot )
1608
1897
  {
1609
1898
  BX_BODY( slot );
1610
- bxDestroyHelpers( body );
1899
+ bxReleaseBodyShapes( body, 1 );
1900
+ body->shapeDesc = descSlot;
1901
+ if ( descSlot != 0 )
1902
+ {
1903
+ bxInstantiate( body, descSlot, b3Transform_identity, b3Vec3_one, 0 );
1904
+ }
1905
+ b3Body_ApplyMassFromShapes( body->id );
1906
+ }
1907
+
1908
+ /// Properties that can be changed on live shapes, see bx_Body_SyncShapeDesc.
1909
+ typedef enum bxSyncFlags
1910
+ {
1911
+ bx_syncFilter = 1,
1912
+ bx_syncMaterial = 2,
1913
+ bx_syncDensity = 4,
1914
+ } bxSyncFlags;
1915
+
1916
+ /// Applies changed description properties to the shapes this body already has, instead of rebuilding them. Rebuilding
1917
+ /// loses contacts and re-applies the mass, and Babylon games change filters on live jointed bodies often.
1918
+ BX_EXPORT void bx_Body_SyncShapeDesc( int slot, int descSlot, int what )
1919
+ {
1920
+ BX_BODY( slot );
1921
+ bxShapeDesc* desc = bxGetDesc( descSlot );
1922
+ if ( desc == NULL )
1923
+ {
1924
+ return;
1925
+ }
1926
+ int massChanged = 0;
1611
1927
  for ( int i = 0; i < body->shapeCount; ++i )
1612
1928
  {
1613
- if ( b3Shape_IsValid( body->shapes[i] ) )
1929
+ if ( body->shapeDescs[i] != descSlot || b3Shape_IsValid( body->shapes[i] ) == false )
1614
1930
  {
1615
- b3DestroyShape( body->shapes[i], false );
1931
+ continue;
1932
+ }
1933
+ b3ShapeId shapeId = body->shapes[i];
1934
+ if ( what & bx_syncFilter )
1935
+ {
1936
+ b3Filter filter = b3Shape_GetFilter( shapeId );
1937
+ filter.categoryBits = desc->categoryBits;
1938
+ filter.maskBits = desc->maskBits;
1939
+ filter.groupIndex = desc->groupIndex;
1940
+ // true: contacts that the new filter forbids are dropped and new pairs are found on the next step
1941
+ b3Shape_SetFilter( shapeId, filter, true );
1942
+ }
1943
+ if ( what & bx_syncMaterial )
1944
+ {
1945
+ b3SurfaceMaterial material = b3Shape_GetSurfaceMaterial( shapeId );
1946
+ material.friction = desc->friction;
1947
+ material.restitution = desc->restitution;
1948
+ material.rollingResistance = desc->rollingResistance;
1949
+ b3Shape_SetSurfaceMaterial( shapeId, material );
1950
+ }
1951
+ if ( what & bx_syncDensity )
1952
+ {
1953
+ b3Shape_SetDensity( shapeId, desc->density, false );
1954
+ massChanged = 1;
1616
1955
  }
1617
- bxDescRelease( body->shapeDescs[i] );
1618
1956
  }
1619
- body->shapeCount = 0;
1620
- body->shapeDesc = descSlot;
1621
- if ( descSlot != 0 )
1957
+ if ( massChanged )
1622
1958
  {
1623
- bxInstantiate( body, descSlot, b3Transform_identity, b3Vec3_one, 0 );
1959
+ b3Body_ApplyMassFromShapes( body->id );
1624
1960
  }
1625
- b3Body_ApplyMassFromShapes( body->id );
1626
1961
  }
1627
1962
 
1628
1963
  BX_EXPORT int bx_Body_GetShapeDesc( int slot )
@@ -1777,6 +2112,19 @@ typedef enum bxJointType
1777
2112
  /// steeringHertz, steeringDamping, targetSteering, maxSteeringTorque, enableSteeringLimit, lowerSteering, upperSteering.
1778
2113
  /// Parallel joints read [hertz, dampingRatio, maxTorque] from scratch[14..].
1779
2114
  /// Returns the joint slot, 0 on failure.
2115
+ // Fills the shared part of a joint definition. The definition must come from its b3Default*JointDef function so every
2116
+ // field not set here keeps Box3D's default. In particular the force and torque thresholds default to FLT_MAX: a zeroed
2117
+ // definition sets them to 0, which makes Box3D compute reaction forces and emit a joint event for every awake joint
2118
+ // on every step.
2119
+ static void bxFillJointBase( b3JointDef* base, bxBody* bodyA, bxBody* bodyB, int collideConnected )
2120
+ {
2121
+ base->bodyIdA = bodyA->id;
2122
+ base->bodyIdB = bodyB->id;
2123
+ base->localFrameA = bxTransform( s_scratch[0], s_scratch[1], s_scratch[2], s_scratch[3], s_scratch[4], s_scratch[5], s_scratch[6] );
2124
+ base->localFrameB = bxTransform( s_scratch[7], s_scratch[8], s_scratch[9], s_scratch[10], s_scratch[11], s_scratch[12], s_scratch[13] );
2125
+ base->collideConnected = collideConnected != 0;
2126
+ }
2127
+
1780
2128
  BX_EXPORT int bx_CreateJoint( int w, int type, int bodyASlot, int bodyBSlot, int collideConnected, float param )
1781
2129
  {
1782
2130
  b3WorldId worldId = bxGetWorld( w );
@@ -1787,68 +2135,41 @@ BX_EXPORT int bx_CreateJoint( int w, int type, int bodyASlot, int bodyBSlot, int
1787
2135
  return 0;
1788
2136
  }
1789
2137
 
1790
- b3JointDef base = { 0 };
1791
- base.bodyIdA = bodyA->id;
1792
- base.bodyIdB = bodyB->id;
1793
- base.localFrameA = bxTransform( s_scratch[0], s_scratch[1], s_scratch[2], s_scratch[3], s_scratch[4], s_scratch[5], s_scratch[6] );
1794
- base.localFrameB = bxTransform( s_scratch[7], s_scratch[8], s_scratch[9], s_scratch[10], s_scratch[11], s_scratch[12], s_scratch[13] );
1795
- base.collideConnected = collideConnected != 0;
1796
-
1797
2138
  b3JointId jointId = b3_nullJointId;
1798
2139
  switch ( type )
1799
2140
  {
1800
2141
  case bx_weldJoint:
1801
2142
  {
1802
2143
  b3WeldJointDef def = b3DefaultWeldJointDef();
1803
- base.constraintHertz = def.base.constraintHertz;
1804
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1805
- base.drawScale = def.base.drawScale;
1806
- base.internalValue = def.base.internalValue;
1807
- def.base = base;
2144
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1808
2145
  jointId = b3CreateWeldJoint( worldId, &def );
1809
2146
  break;
1810
2147
  }
1811
2148
  case bx_sphericalJoint:
1812
2149
  {
1813
2150
  b3SphericalJointDef def = b3DefaultSphericalJointDef();
1814
- base.constraintHertz = def.base.constraintHertz;
1815
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1816
- base.drawScale = def.base.drawScale;
1817
- base.internalValue = def.base.internalValue;
1818
- def.base = base;
2151
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1819
2152
  jointId = b3CreateSphericalJoint( worldId, &def );
1820
2153
  break;
1821
2154
  }
1822
2155
  case bx_revoluteJoint:
1823
2156
  {
1824
2157
  b3RevoluteJointDef def = b3DefaultRevoluteJointDef();
1825
- base.constraintHertz = def.base.constraintHertz;
1826
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1827
- base.drawScale = def.base.drawScale;
1828
- base.internalValue = def.base.internalValue;
1829
- def.base = base;
2158
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1830
2159
  jointId = b3CreateRevoluteJoint( worldId, &def );
1831
2160
  break;
1832
2161
  }
1833
2162
  case bx_prismaticJoint:
1834
2163
  {
1835
2164
  b3PrismaticJointDef def = b3DefaultPrismaticJointDef();
1836
- base.constraintHertz = def.base.constraintHertz;
1837
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1838
- base.drawScale = def.base.drawScale;
1839
- base.internalValue = def.base.internalValue;
1840
- def.base = base;
2165
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1841
2166
  jointId = b3CreatePrismaticJoint( worldId, &def );
1842
2167
  break;
1843
2168
  }
1844
2169
  case bx_distanceJoint:
1845
2170
  {
1846
2171
  b3DistanceJointDef def = b3DefaultDistanceJointDef();
1847
- base.constraintHertz = def.base.constraintHertz;
1848
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1849
- base.drawScale = def.base.drawScale;
1850
- base.internalValue = def.base.internalValue;
1851
- def.base = base;
2172
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1852
2173
  def.length = param;
1853
2174
  def.enableLimit = true;
1854
2175
  def.minLength = param;
@@ -1859,11 +2180,7 @@ BX_EXPORT int bx_CreateJoint( int w, int type, int bodyASlot, int bodyBSlot, int
1859
2180
  case bx_wheelJoint:
1860
2181
  {
1861
2182
  b3WheelJointDef def = b3DefaultWheelJointDef();
1862
- base.constraintHertz = def.base.constraintHertz;
1863
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1864
- base.drawScale = def.base.drawScale;
1865
- base.internalValue = def.base.internalValue;
1866
- def.base = base;
2183
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1867
2184
  const float* p = s_scratch + 14;
1868
2185
  def.enableSuspensionSpring = p[0] != 0.0f;
1869
2186
  def.suspensionHertz = p[1];
@@ -1888,11 +2205,7 @@ BX_EXPORT int bx_CreateJoint( int w, int type, int bodyASlot, int bodyBSlot, int
1888
2205
  case bx_parallelJoint:
1889
2206
  {
1890
2207
  b3ParallelJointDef def = b3DefaultParallelJointDef();
1891
- base.constraintHertz = def.base.constraintHertz;
1892
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1893
- base.drawScale = def.base.drawScale;
1894
- base.internalValue = def.base.internalValue;
1895
- def.base = base;
2208
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1896
2209
  def.hertz = s_scratch[14];
1897
2210
  def.dampingRatio = s_scratch[15];
1898
2211
  def.maxTorque = s_scratch[16];
@@ -1902,11 +2215,7 @@ BX_EXPORT int bx_CreateJoint( int w, int type, int bodyASlot, int bodyBSlot, int
1902
2215
  case bx_filterJoint:
1903
2216
  {
1904
2217
  b3FilterJointDef def = b3DefaultFilterJointDef();
1905
- base.constraintHertz = def.base.constraintHertz;
1906
- base.constraintDampingRatio = def.base.constraintDampingRatio;
1907
- base.drawScale = def.base.drawScale;
1908
- base.internalValue = def.base.internalValue;
1909
- def.base = base;
2218
+ bxFillJointBase( &def.base, bodyA, bodyB, collideConnected );
1910
2219
  jointId = b3CreateFilterJoint( worldId, &def );
1911
2220
  break;
1912
2221
  }
@@ -2065,6 +2374,44 @@ BX_EXPORT void bx_Joint_EnableMotor( int slot, int flag )
2065
2374
  }
2066
2375
  }
2067
2376
 
2377
+ /// Spherical joints take a relative angular velocity (angularVelocityB - angularVelocityA) in WORLD space. The target
2378
+ /// is stored in joint frame A here and converted, so the motor keeps meaning the same thing as body A turns. Call
2379
+ /// bx_Joint_UpdateMotorFrame once per step for joints with a non-zero target.
2380
+ BX_EXPORT void bx_Joint_UpdateMotorFrame( int slot )
2381
+ {
2382
+ b3JointId id = bxGetJointId( slot );
2383
+ if ( B3_IS_NULL( id ) || b3Joint_GetType( id ) != b3_sphericalJoint )
2384
+ {
2385
+ return;
2386
+ }
2387
+ b3Quat frameA = b3MulQuat( b3Body_GetRotation( b3Joint_GetBodyA( id ) ), b3Joint_GetLocalFrameA( id ).q );
2388
+ b3SphericalJoint_SetMotorVelocity( id, b3RotateVector( frameA, s_joints[slot].motorVelocity ) );
2389
+ }
2390
+
2391
+ /// Enables the spherical motor with a target relative angular velocity in joint frame A (x, y, z) and a torque limit.
2392
+ BX_EXPORT void bx_Joint_SetSphericalMotor( int slot, int enable, float x, float y, float z, float maxTorque )
2393
+ {
2394
+ b3JointId id = bxGetJointId( slot );
2395
+ if ( B3_IS_NULL( id ) || b3Joint_GetType( id ) != b3_sphericalJoint )
2396
+ {
2397
+ return;
2398
+ }
2399
+ s_joints[slot].motorVelocity = bxVec3( x, y, z );
2400
+ b3SphericalJoint_EnableMotor( id, enable != 0 );
2401
+ b3SphericalJoint_SetMaxMotorTorque( id, maxTorque );
2402
+ bx_Joint_UpdateMotorFrame( slot );
2403
+ }
2404
+
2405
+ /// Target rotation of a spherical joint's spring: frame B relative to frame A.
2406
+ BX_EXPORT void bx_Joint_SetSphericalTarget( int slot, float qx, float qy, float qz, float qw )
2407
+ {
2408
+ b3JointId id = bxGetJointId( slot );
2409
+ if ( B3_IS_NON_NULL( id ) && b3Joint_GetType( id ) == b3_sphericalJoint )
2410
+ {
2411
+ b3SphericalJoint_SetTargetRotation( id, bxQuat( qx, qy, qz, qw ) );
2412
+ }
2413
+ }
2414
+
2068
2415
  BX_EXPORT void bx_Joint_SetMotorSpeed( int slot, float speed )
2069
2416
  {
2070
2417
  b3JointId id = bxGetJointId( slot );
@@ -2084,7 +2431,8 @@ BX_EXPORT void bx_Joint_SetMotorSpeed( int slot, float speed )
2084
2431
  b3DistanceJoint_SetMotorSpeed( id, speed );
2085
2432
  break;
2086
2433
  case b3_sphericalJoint:
2087
- b3SphericalJoint_SetMotorVelocity( id, bxVec3( 0.0f, 0.0f, speed ) );
2434
+ // twist about the joint's own axis (frame z), not a world axis
2435
+ bx_Joint_SetSphericalMotor( slot, 1, 0.0f, 0.0f, speed, b3SphericalJoint_GetMaxMotorTorque( id ) );
2088
2436
  break;
2089
2437
  default:
2090
2438
  break;