Skip to content

Commit 22f508a

Browse files
brettleomichel
andauthored
Improve speed, accuracy, and readability of dCollideBoxPlane. (#6688)
* Improve speed, accuracy, and readability of dCollideBoxPlane. Improve accuracy by always returning the maxContacts deepest contacts. The old code would produce at most 4 contacts and the 4th contact wouldn't necessarily be the 4th deepest. This could result in oscillations if the entire box went under the plane. Fixing this means that the change isn't perfectly backward compatible. In particular, the values in one test are adjusted because they relied on a thin box resting entirely below the ground for which the new code generates 8 contact points/joints instead of 4. The new code is significantly faster at colliding random boxes and planes when maxContacts <= 4, slightly faster when colliding boxes that are "settling" on the plane (ie. the most common case), and very close to the same speed in other cases. Improve readabilitly by avoiding goto statements, using less macros with clearer names, and commenting heavily. * Add changelog entry. --------- Co-authored-by: Olivier Michel <[email protected]>
1 parent aeb490d commit 22f508a

File tree

4 files changed

+245
-139
lines changed

4 files changed

+245
-139
lines changed

docs/reference/changelog-r2024.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Released on December **th, 2023.
1818
- Enhancements
1919
- Improved the image range of the rotating [Lidar](lidar.md) ([#6324](https://github.com/cyberbotics/webots/pull/6324)).
2020
- Show box-plane contact point normals when showing contact points ([#6678](https://github.com/cyberbotics/webots/pull/6678)).
21+
- Improved the speed and accuracy of box-plane collisions ([#6688](https://github.com/cyberbotics/webots/pull/6688)).
2122
- Cleanup
2223
- Removed deprecated `windowPosition`, `pixelSize` fields of [Display](display.md) node ([#6327](https://github.com/cyberbotics/webots/pull/6327)).
2324
- Bug Fixes

src/ode/ode/src/box.cpp

Lines changed: 234 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#ifdef _MSC_VER
4444
#pragma warning(disable:4291) // for VC++, no complaints about "no matching operator delete found"
4545
#endif
46+
#include <valarray>
4647

4748
//****************************************************************************
4849
// box public API
@@ -740,137 +741,238 @@ int dCollideBoxBox (dxGeom *o1, dxGeom *o2, int flags,
740741
return num;
741742
}
742743

743-
int dCollideBoxPlane (dxGeom *o1, dxGeom *o2,
744-
int flags, dContactGeom *contact, int skip)
745-
{
746-
dIASSERT (skip >= (int)sizeof(dContactGeom));
747-
dIASSERT (o1->type == dBoxClass);
748-
dIASSERT (o2->type == dPlaneClass);
749-
dIASSERT ((flags & NUMC_MASK) >= 1);
750-
751-
dxBox *box = (dxBox*) o1;
752-
dxPlane *plane = (dxPlane*) o2;
753-
754-
contact->g1 = o1;
755-
contact->g2 = o2;
756-
contact->side1 = -1;
757-
contact->side2 = -1;
758-
759-
int ret = 0;
760-
761-
//@@@ problem: using 4-vector (plane->p) as 3-vector (normal).
762-
const dReal *R = o1->final_posr->R; // rotation of box
763-
const dReal *n = plane->p; // normal vector
764-
765-
// project sides lengths along normal vector, get absolute values
766-
dReal Q1 = dCalcVectorDot3_14(n,R+0);
767-
dReal Q2 = dCalcVectorDot3_14(n,R+1);
768-
dReal Q3 = dCalcVectorDot3_14(n,R+2);
769-
dReal A1 = box->side[0] * Q1;
770-
dReal A2 = box->side[1] * Q2;
771-
dReal A3 = box->side[2] * Q3;
772-
dReal B1 = dFabs(A1);
773-
dReal B2 = dFabs(A2);
774-
dReal B3 = dFabs(A3);
775-
776-
// early exit test
777-
dReal depth = plane->p[3] + REAL(0.5)*(B1+B2+B3) - dCalcVectorDot3(n,o1->final_posr->pos);
778-
if (depth < 0) return 0;
779-
780-
// find number of contacts requested
781-
int maxc = flags & NUMC_MASK;
782-
// if (maxc < 1) maxc = 1; // an assertion is made on entry
783-
if (maxc > 4) maxc = 4; // not more than 4 contacts per box allowed
784-
785-
// find deepest point
786-
dVector3 p;
787-
p[0] = o1->final_posr->pos[0];
788-
p[1] = o1->final_posr->pos[1];
789-
p[2] = o1->final_posr->pos[2];
790-
#define FOO(i,op) \
791-
p[0] op REAL(0.5)*box->side[i] * R[0+i]; \
792-
p[1] op REAL(0.5)*box->side[i] * R[4+i]; \
793-
p[2] op REAL(0.5)*box->side[i] * R[8+i];
794-
#define BAR(i,iinc) if (A ## iinc > 0) { FOO(i,-=) } else { FOO(i,+=) }
795-
BAR(0,1);
796-
BAR(1,2);
797-
BAR(2,3);
798-
#undef FOO
799-
#undef BAR
800-
801-
// the deepest point is the first contact point
802-
contact->pos[0] = p[0];
803-
contact->pos[1] = p[1];
804-
contact->pos[2] = p[2];
805-
contact->depth = depth;
806-
ret = 1; // ret is number of contact points found so far
807-
if (maxc == 1) goto done;
808-
809-
// get the second and third contact points by starting from `p' and going
810-
// along the two sides with the smallest projected length.
811-
812-
#define FOO(i,j,op) \
813-
CONTACT(contact,i*skip)->pos[0] = p[0] op box->side[j] * R[0+j]; \
814-
CONTACT(contact,i*skip)->pos[1] = p[1] op box->side[j] * R[4+j]; \
815-
CONTACT(contact,i*skip)->pos[2] = p[2] op box->side[j] * R[8+j];
816-
#define BAR(ctact,side,sideinc) \
817-
if (depth - B ## sideinc < 0) goto done; \
818-
if (A ## sideinc > 0) { FOO(ctact,side,+); } else { FOO(ctact,side,-); } \
819-
CONTACT(contact,ctact*skip)->depth = depth - B ## sideinc; \
820-
ret++;
821-
822-
if (B1 < B2) {
823-
if (B3 < B1) goto use_side_3; else {
824-
BAR(1,0,1); // use side 1
825-
if (maxc == 2) goto done;
826-
if (B2 < B3) goto contact2_2; else goto contact2_3;
827-
}
828-
}
829-
else {
830-
if (B3 < B2) {
831-
use_side_3: // use side 3
832-
BAR(1,2,3);
833-
if (maxc == 2) goto done;
834-
if (B1 < B2) goto contact2_1; else goto contact2_2;
835-
}
836-
else {
837-
BAR(1,1,2); // use side 2
838-
if (maxc == 2) goto done;
839-
if (B1 < B3) goto contact2_1; else goto contact2_3;
840-
}
841-
}
842-
843-
contact2_1: BAR(2,0,1); goto done;
844-
contact2_2: BAR(2,1,2); goto done;
845-
contact2_3: BAR(2,2,3); goto done;
846-
#undef FOO
847-
#undef BAR
848-
849-
done:
850-
851-
if (maxc == 4 && ret == 3) { // If user requested 4 contacts, and the first 3 were created...
852-
// Combine contacts 2 and 3 (vectorial sum) and get the fourth one
853-
// Result: if a box face is completely inside a plane, contacts are created for all the 4 vertices
854-
dReal d4 = CONTACT(contact,1*skip)->depth + CONTACT(contact,2*skip)->depth - depth; // depth is the depth for first contact
855-
if (d4 > 0) {
856-
CONTACT(contact,3*skip)->pos[0] = CONTACT(contact,1*skip)->pos[0] + CONTACT(contact,2*skip)->pos[0] - p[0]; // p is the position of first contact
857-
CONTACT(contact,3*skip)->pos[1] = CONTACT(contact,1*skip)->pos[1] + CONTACT(contact,2*skip)->pos[1] - p[1];
858-
CONTACT(contact,3*skip)->pos[2] = CONTACT(contact,1*skip)->pos[2] + CONTACT(contact,2*skip)->pos[2] - p[2];
859-
CONTACT(contact,3*skip)->depth = d4;
860-
ret++;
861-
}
862-
}
863-
864-
for (int i=0; i<ret; i++) {
865-
dContactGeom *currContact = CONTACT(contact,i*skip);
866-
currContact->g1 = o1;
867-
currContact->g2 = o2;
868-
currContact->side1 = -1;
869-
currContact->side2 = -1;
744+
// Fill in the the non-varying contact info for the first n contacts.
745+
static inline void finishContacts(int n, dContactGeom *&contact, int skip, dxGeom *o1, dxGeom *o2, const dReal *normal) {
746+
for (int i = 0; i < n; i++) {
747+
dContactGeom *currContact = CONTACT(contact, i * skip);
748+
currContact->g1 = o1;
749+
currContact->g2 = o2;
750+
currContact->side1 = -1;
751+
currContact->side2 = -1;
752+
753+
currContact->normal[0] = normal[0];
754+
currContact->normal[1] = normal[1];
755+
currContact->normal[2] = normal[2];
756+
}
757+
}
870758

871-
currContact->normal[0] = n[0];
872-
currContact->normal[1] = n[1];
873-
currContact->normal[2] = n[2];
874-
}
875-
return ret;
759+
int dCollideBoxPlane(dxGeom *o1, dxGeom *o2, int flags, dContactGeom *contact, int skip) {
760+
dIASSERT(skip >= (int)sizeof(dContactGeom));
761+
dIASSERT(o1->type == dBoxClass);
762+
dIASSERT(o2->type == dPlaneClass);
763+
dIASSERT((flags & NUMC_MASK) >= 1);
764+
765+
dxBox *box = (dxBox *)o1;
766+
dxPlane *plane = (dxPlane *)o2;
767+
768+
//@@@ problem: using 4-vector (plane->p) as 3-vector (normal).
769+
const dReal *R = o1->final_posr->R; // rotation of box
770+
const dReal *n = plane->p; // normal vector
771+
772+
// project sides lengths along normal vector, get absolute values
773+
// A[i] will be the reduction in depth resulting from moving in direction i by an amount of side[i].
774+
const dReal Q0 = dCalcVectorDot3_14(n, R + 0);
775+
const dReal Q1 = dCalcVectorDot3_14(n, R + 1);
776+
const dReal Q2 = dCalcVectorDot3_14(n, R + 2);
777+
const dReal *const side = box->side;
778+
const dVector3 A = {side[0] * Q0, side[1] * Q1, side[2] * Q2};
779+
780+
// Find centerDepth
781+
const dReal centerDepth = plane->p[3] - dCalcVectorDot3(n, o1->final_posr->pos);
782+
783+
// The depths (positive = deeper) of the 8 vertices will be:
784+
//
785+
// centerDepth - 0.5*(+/- A[0] +/- A[1] +/- A[2])
786+
//
787+
// for the 8 combinations of +/-.
788+
789+
// What we'd *really* like is to get the vertices in depth order (deepest first), so that we can
790+
// easily return the maxc deepest vertices and also return early once we get to a vertex with a
791+
// negative depth. To achieve that efficiently, it will turn out to be useful to use |A[s]|
792+
// instead of A[s] when computing the depths. We store that in B.
793+
dVector3 B = {dFabs(A[0]), dFabs(A[1]), dFabs(A[2])};
794+
795+
// Note that A[s]=sgn(A[s])*B[s]. That means that the depths are
796+
// given by:
797+
//
798+
// centerDepth - 0.5*(+/- sgn(A[0])*B[0] +/- sgn(A[1])*B[1] +/- sgn(A[2])*B[2])
799+
//
800+
// The depth of the deepest vertex is achieved when the choice of + or - is opposite the
801+
// corresponding sgn() function. That will lead to the depth of the deepest vertex being:
802+
dReal depth = centerDepth + REAL(0.5) * (B[0] + B[1] + B[2]);
803+
804+
// The number of contacts found.
805+
int nContactsFound = 0;
806+
807+
do {
808+
// If that isn't below the plane, there is no collision and we can return immediately.
809+
if (depth < 0)
810+
break;
811+
812+
// Otherwise, we need to figure out the position of the vertex, and potentially other vertices.
813+
814+
// Let p be the center point of the box.
815+
const dVector3 p = {o1->final_posr->pos[0], o1->final_posr->pos[1], o1->final_posr->pos[2]};
816+
817+
// To compute the positions of each of the 8 vertices, note that side[s]*R[4*c+s] is the change
818+
// in coordinate c associated with moving along the full length of side s. As a result, the cth
819+
// coord of the vertices are given by:
820+
//
821+
// p[c] +/- 0.5*side[0]*R[4*c+0] +/- 0.5*side[1]*R[4*c+1] +/- 0.5*side[2]*R[4*c+2]
822+
//
823+
// for c in 0, 1, 2, and all 8 combinations of +/- where the choices for +/- for a vertex match
824+
// the choices when computing the depth of the vertex:
825+
//
826+
// centerDepth - 0.5*(+/- sgn(A[0])*B[0] +/- sgn(A[1])*B[1] +/- sgn(A[2])*B[2])
827+
//
828+
// Since the sgn() function will just flip or not flip the +/- to a -/+, and we just need to
829+
// keep the choices consistent between the computations of the vertices' depths and the
830+
// positions, we can move the sgn() to the computation of the coords to get:
831+
//
832+
// centerDepth - 0.5*(+/- B[0] +/- B[1] +/- B[2])
833+
//
834+
// and:
835+
//
836+
// p[c] +/- 0.5*sgn(A[0])*side[0]*R[4*c+0] +/- 0.5*sgn(A[1])*side[1]*R[4*c+1] +/-
837+
// 0.5*sgn(A[2])*side[2]*R[4*c+2]
838+
//
839+
// We can precompute signedHalfSides[s]=0.5*sgn(A[s])*side[s] and
840+
// signedHalfSideVectors[s][c]=signedHalfSides[s]*R[4*s+c]
841+
const dVector3 signedHalfSides = {
842+
(std::signbit(A[0]) ? REAL(-0.5) : REAL(0.5)) * side[0],
843+
(std::signbit(A[1]) ? REAL(-0.5) : REAL(0.5)) * side[1],
844+
(std::signbit(A[2]) ? REAL(-0.5) : REAL(0.5)) * side[2],
845+
};
846+
847+
const dVector3 signedHalfSideVectors[3] = {
848+
{signedHalfSides[0] * R[0 + 0], signedHalfSides[0] * R[4 + 0], signedHalfSides[0] * R[8 + 0]},
849+
{signedHalfSides[1] * R[0 + 1], signedHalfSides[1] * R[4 + 1], signedHalfSides[1] * R[8 + 1]},
850+
{signedHalfSides[2] * R[0 + 2], signedHalfSides[2] * R[4 + 2], signedHalfSides[2] * R[8 + 2]},
851+
};
852+
853+
// Now the vertices are given by:
854+
//
855+
// p[c] +/- signedHalfSides[0]*R[4*c+0] +/- signedHalfSides[1]*R[4*c+1] +/-
856+
// signedHalfSides[2]*R[4*c+2]
857+
//
858+
// The deepest vertex corresponds to always choosing "-". So we can fill it in right away.
859+
dContactGeom *currContact;
860+
currContact = contact;
861+
currContact->pos[0] = p[0] - signedHalfSideVectors[2][0] - signedHalfSideVectors[1][0] - signedHalfSideVectors[0][0];
862+
currContact->pos[1] = p[1] - signedHalfSideVectors[2][1] - signedHalfSideVectors[1][1] - signedHalfSideVectors[0][1];
863+
currContact->pos[2] = p[2] - signedHalfSideVectors[2][2] - signedHalfSideVectors[1][2] - signedHalfSideVectors[0][2];
864+
currContact->depth = depth;
865+
866+
nContactsFound++;
867+
868+
// The number of contacts requested. We can't return more than this many.
869+
const int maxc = flags & NUMC_MASK;
870+
871+
// If only 1 contact was requested, finish filling in the non-varying contact info and return.
872+
if (maxc == 1)
873+
break;
874+
875+
// The second deepest will correspond to choosing the - option for all but the smallest B[s].
876+
// That corresponds to moving along the edge that causes the least change in depth. The third
877+
// deepest will correspond to choosing the - option for all but the second smallest B[s]. The
878+
// least, second least, and third *least* deep can be found similarly. So the ranks of 6 of the
879+
// 8 vertices can be determined in this way. The only remaining ambiguity is which of the 2
880+
// other vertices is deeper, which we will handle later.
881+
//
882+
// To compute the vertices and depths efficiently in the desired order, we will presort the
883+
// sides and their corresponding Bs in ascending order of the Bs and we will precompute each of
884+
// the signedHalfSide[s]*R[4*c+s] terms as follows:
885+
//
886+
// Let minSide, midSide, maxSide be the indices of the sides such that
887+
// B[minSide]<=B[midSide]<=B[maxSide]
888+
//
889+
// Let orderedB[0]=B[minSide], orderedB[1]=B[midSide], and orderedB[2]=B[maxSide]
890+
//
891+
// and:
892+
//
893+
// orderedSignedHalfSideVectors[0][c] = signedHalfSideVectors[minSide]
894+
// orderedSignedHalfSideVectors[1][c] = signedHalfSideVectors[midSide]
895+
// orderedSignedHalfSideVectors[2][c] = signedHalfSideVectors[maxSide]
896+
//
897+
// The positions of the vertices will then be:
898+
//
899+
// p[c] +/- orderedHalfSideVector[0][c] +/- orderedHalfSideVector[1][c] +/-
900+
// orderedHalfSideVector[2][c]
901+
//
902+
// and the corresponding depths:
903+
//
904+
// centerDepth - 0.5*(+/- orderedB[0] +/- orderedB[1] +/- orderedB[2])
905+
//
906+
// We will call the following macro with the various +/- combos to compute the depth of a
907+
// vertex, add it to the contacts if it is inside and stop early if it isn't or we've found
908+
// the requested number of contacts.
909+
#define ADD_VERTEX_IF_INSIDE(op1, op2, op3) \
910+
depth = (centerDepth - REAL(0.5) * (op3 orderedB[2] op2 orderedB[1] op1 orderedB[0])); \
911+
if (depth >= 0.0) { \
912+
currContact = CONTACT(contact, nContactsFound * skip); \
913+
currContact->pos[0] = p[0] op3 orderedSignedHalfSideVectors[2][0] op2 orderedSignedHalfSideVectors[1][0] op1 \
914+
orderedSignedHalfSideVectors[0][0]; \
915+
currContact->pos[1] = p[1] op3 orderedSignedHalfSideVectors[2][1] op2 orderedSignedHalfSideVectors[1][1] op1 \
916+
orderedSignedHalfSideVectors[0][1]; \
917+
currContact->pos[2] = p[2] op3 orderedSignedHalfSideVectors[2][2] op2 orderedSignedHalfSideVectors[1][2] op1 \
918+
orderedSignedHalfSideVectors[0][2]; \
919+
currContact->depth = depth; \
920+
nContactsFound++; \
921+
} else \
922+
break; \
923+
if (nContactsFound >= maxc) \
924+
break;
925+
// END OF MACRO DEFINITION
926+
927+
// But first we need to sort the sides and Bs. We'll start with them in their original order...
928+
const dReal *orderedSignedHalfSideVectors[3] = {
929+
&(signedHalfSideVectors[0][0]),
930+
&(signedHalfSideVectors[1][0]),
931+
&(signedHalfSideVectors[2][0]),
932+
};
933+
const dReal *orderedB = B;
934+
935+
// Swap the one that corresponds to the lowest B into the 0th position.
936+
if (B[1] < B[0]) {
937+
std::swap(B[1], B[0]);
938+
std::swap(orderedSignedHalfSideVectors[1], orderedSignedHalfSideVectors[0]);
939+
}
940+
if (B[2] < B[0]) {
941+
std::swap(B[2], B[0]);
942+
std::swap(orderedSignedHalfSideVectors[2], orderedSignedHalfSideVectors[0]);
943+
}
944+
945+
// For the second deepest vertex, we don't actually care about the order of other 2 sides, so we
946+
// can go ahead and process that one. Maybe we'll get lucky and exit early.
947+
ADD_VERTEX_IF_INSIDE(+, -, -);
948+
949+
// If we didn't get lucky, we need to sort the remaining 2 sides.
950+
if (B[2] < B[1]) {
951+
std::swap(B[2], B[1]);
952+
std::swap(orderedSignedHalfSideVectors[2], orderedSignedHalfSideVectors[1]);
953+
}
954+
955+
// Now we can process the third deepest vertex.
956+
ADD_VERTEX_IF_INSIDE(-, +, -);
957+
958+
// Determine which of the 2 middle vertices is deeper and process them in the correct order.
959+
if (B[0] + B[1] < B[2]) {
960+
ADD_VERTEX_IF_INSIDE(+, +, -);
961+
ADD_VERTEX_IF_INSIDE(-, -, +);
962+
} else {
963+
ADD_VERTEX_IF_INSIDE(-, -, +);
964+
ADD_VERTEX_IF_INSIDE(+, +, -);
965+
}
966+
967+
// Process the 3 remaining vertices in order.
968+
ADD_VERTEX_IF_INSIDE(+, -, +);
969+
ADD_VERTEX_IF_INSIDE(-, +, +);
970+
ADD_VERTEX_IF_INSIDE(+, +, +);
971+
} while (false);
972+
973+
if (nContactsFound)
974+
finishContacts(nContactsFound, contact, skip, o1, o2, n);
975+
976+
int retVal = nContactsFound;
977+
return retVal;
876978
}

0 commit comments

Comments
 (0)