// Copyright 2016 Svetoslav Kolev // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include "engine/engine_collision_primitive.h" #include "engine/engine_inline.h" #include "engine/engine_util_blas.h" #include "engine/engine_util_misc.h" // hard-clamp vector to range [-limit(i), +limit(i)] static void mju_clampVec(mjtNum* vec, const mjtNum* limit, int n) { for (int i = 0; i < n; i++) { // loop over active limits if (limit[i] > 0) { vec[i] = mju_clip(vec[i], -limit[i], limit[i]); } } } // raw sphere : box int mjraw_SphereBox(mjPreContact* con, mjtNum margin, const mjtNum* pos1, const mjtNum* mat1, const mjtNum* size1, const mjtNum* pos2, const mjtNum* mat2, const mjtNum* size2) { int i, k; mjtNum tmp[3], center[3], clamped[3], deepest[3]; mjtNum pos[3]; mjtNum dist, closest; mji_sub3(tmp, pos1, pos2); mji_mulMatTVec3(center, mat2, tmp); mji_copy3(clamped, center); mju_clampVec(clamped, size2, 3); mji_copy3(deepest, center); mji_sub3(tmp, clamped, center); dist = mju_normalize3(tmp); if (dist - size1[0] > margin) return 0; // sphere center inside box if (dist <= mjMINVAL) { closest = (size2[0] + size2[1] + size2[2]) * 2; for (i = 0; i < 6; i++) { if (closest > mju_abs((i % 2 ? 1 : -1)*size2[i / 2] - center[i / 2])) { closest = mju_abs((i % 2 ? 1 : -1) * size2[i / 2] - center[i / 2]); k = i; } } mjtNum nearest[3] = {0}; nearest[k / 2] = (k % 2 ? -1 : 1); mji_copy3(pos, center); mji_addToScl3(pos, nearest, (size1[0] - closest) / 2); mji_mulMatVec3(con[0].normal, mat2, nearest); dist = -closest; } else { mji_addToScl3(deepest, tmp, size1[0]); mju_zero3(pos); mji_addToScl3(pos, clamped, 0.5); mji_addToScl3(pos, deepest, 0.5); mji_mulMatVec3(con[0].normal, mat2, tmp); } mji_mulMatVec3(tmp, mat2, pos); mji_add3(con[0].pos, tmp, pos2); con[0].dist = dist - size1[0]; mji_zero3(con[0].tangent); return 1; } // sphere : box int mjc_SphereBox(const mjModel* m, mjData* d, mjPreContact* con, int g1, int g2, mjtNum margin) { const mjtNum* pos1 = d->geom_xpos + 3*g1; const mjtNum* mat1 = d->geom_xmat + 9*g1; const mjtNum* size1 = m->geom_size + 3*g1; const mjtNum* pos2 = d->geom_xpos + 3*g2; const mjtNum* mat2 = d->geom_xmat + 9*g2; const mjtNum* size2 = m->geom_size + 3*g2; return mjraw_SphereBox(con, margin, pos1, mat1, size1, pos2, mat2, size2); } /* GENERAL THEORY OF OPERATION the following code is mostly for finding (line segment)/(box) collision after which box-sphere is called First the closest point to the box is found. Then a "sensible" second point is found if the angle between the segment and the box is low enough < 45 In the comments that follow, capsule just means the capsule's line segment It might be hard to understand all comments but you would need a picture to see what is happening at each line of the code */ // raw capsule : box int mjraw_CapsuleBox(mjPreContact* con, mjtNum margin, const mjtNum* pos1, const mjtNum* mat1, const mjtNum* size1, const mjtNum* pos2, const mjtNum* mat2, const mjtNum* size2) { mjtNum tmp1[3], tmp2[3], tmp3[3], halfaxis[3], axis[3], dif[3]; mjtNum pos[3]; // position of capsule in box-local frame mjtNum halflength; // half of capsule's length mjtNum bestdist; // closest contact point distance mjtNum bestdistmax; // init value for bestdist mjtNum bestsegmentpos; // between -1 and 1 : which point on the segment is closest to the box mjtNum secondpos; // distance of 2nd contact position on capsule segment from the first mjtNum dist; mjtNum bestboxpos; // closest contact point, position on the box's edge mjtNum mul, e1, e2, dp, de; // mjtNum penetration; mjtNum ma, mb, mc, u, v, det, x1, x2, idet; // linelinedist temps int s1, s2; // hold linelinedist info int i, j, c1, c2; // temporary variables int cltype = -4; // closest type int clface; // closest face int clcorner = 0; // closest corner (0..7 in binary) int cledge; // closest edge axis int axisdir; // direction of capsule axis in relation to the box int n; // number of contacts int ax1, ax2, ax; // axis temporaries halflength = size1[1]; secondpos = -4; // initialize to no 2nd contact (valid values are between -1 and 1) mji_sub3(tmp1, pos1, pos2); // bring capsule to box-local frame (center's box is at (0,0,0)) mji_mulMatTVec3(pos, mat2, tmp1); // and axis parallel to world tmp1[0] = mat1[2]; // capsule's axis tmp1[1] = mat1[5]; tmp1[2] = mat1[8]; mji_mulMatTVec3(axis, mat2, tmp1); // do the same for the capsule axis mji_scl3(halfaxis, axis, halflength); // scale to get actual capsule half-axis axisdir = 0; if (halfaxis[0] > 0) axisdir += 1; if (halfaxis[1] > 0) axisdir += 2; if (halfaxis[2] > 0) axisdir += 4; // under this notion "axisdir" and "7-axisdir" point in opposite directions, // essentially the same for a capsule bestdistmax = margin + 2 * (size1[0] + halflength + size2[0] + size2[1] + size2[2]); // initialize bestdist bestdist = bestdistmax; bestsegmentpos = 0; mju_zero3(tmp2); // test to see if maybe the a face of the box is closest to the capsule for (i = -1; i <= 1; i += 2) { mji_copy3(tmp1, pos); mji_addToScl3(tmp1, halfaxis, i); mji_copy3(tmp2, tmp1); for (c1 = 0, j = 0, c2 = -1; j < 3; j++) { if (tmp1[j] < -size2[j]) { c1++; c2 = j; tmp1[j] = -size2[j]; } else if (tmp1[j] > size2[j]) { c1++; c2 = j; tmp1[j] = size2[j]; } } if (c1 > 1) continue; mji_subFrom3(tmp1, tmp2); dist = mju_dot3(tmp1, tmp1); if (dist < bestdist) { bestdist = dist; bestsegmentpos = i; cltype = -2 + i; clface = c2; } } mju_zero3(tmp2); for (j = 0; j < 3; j++) { for (i = 0; i < 8; i++) { if ((i & (1 << j)) == 0) { // trick to get a corner tmp3[0] = ((i & 1) ? 1 : -1) * size2[0]; tmp3[1] = ((i & 2) ? 1 : -1) * size2[1]; tmp3[2] = ((i & 4) ? 1 : -1) * size2[2]; tmp3[j] = 0; // tmp3 is the starting point on the box // tmp2 is the direction along the "j"-th axis // pos is the capsule's center // halfaxis is the capsule direction // find closest point between capsule and the edge mji_sub3(dif, tmp3, pos); ma = size2[j] * size2[j]; mb = -size2[j] * halfaxis[j]; mc = size1[1] * size1[1]; u = -size2[j] * dif[j]; v = mju_dot3(halfaxis, dif); det = ma * mc - mb * mb; if (mju_abs(det) < mjMINVAL) continue; idet = 1 / det; // sX : X=1 means middle of segment. X=0 or 2 one or the other end x1 = (mc * u - mb * v) * idet; x2 = (ma * v - mb * u) * idet; s1 = s2 = 1; if (x1 > 1) { x1 = 1; s1 = 2; x2 = (v - mb) * (1 / mc); } else if (x1 < -1) { x1 = -1; s1 = 0; x2 = (v + mb) * (1 / mc); } if (x2 > 1) { x2 = 1; s2 = 2; x1 = (u - mb) * (1 / ma); if (x1 > 1) x1 = 1, s1 = 2; else if (x1 < -1) x1 = -1, s1 = 0; } else if (x2 < -1) { x2 = -1; s2 = 0; x1 = (u + mb) * (1 / ma); if (x1 > 1) x1 = 1, s1 = 2; else if (x1 < -1) x1 = -1, s1 = 0; } mji_sub3(dif, tmp3, pos); mji_addToScl3(dif, halfaxis, -x2); dif[j] += size2[j] * x1; tmp1[2] = mju_dot3(dif, dif); c1 = s1 * 3 + s2; // the -MINVAL might not be necessary. Fixes numerical problem when axis is numerically // parallel to the box if (tmp1[2] < bestdist - mjMINVAL) { bestdist = tmp1[2]; bestsegmentpos = x2; bestboxpos = x1; // c1<6 means that closest point on the box is at the lower end // or in the middle of the edge c2 = c1 / 6; clcorner = i + (1 << j) * c2; // which corner is the closest cledge = j; // which axis cltype = c1; // save clamped info } } } } // penetration = -bestdist; for (j = 0; j < 3; j++) { if (j == 2) { typedef union { struct { mjtNum x, y; }; mjtNum c[2]; } d2; d2 p, s, dd /*, c, tmp1*/; mjtNum uu, vv, w, ee1, best /* ,e2 */, l /* , e3, e4 */; bestdist = bestdistmax; p.x = pos[0]; p.y = pos[1]; dd.x = halfaxis[0]; dd.y = halfaxis[1]; s.x = size2[0]; s.y = size2[1]; l = sqrt(dd.x * dd.x + dd.y * dd.y); uu = dd.x * s.y; vv = dd.y * s.x; w = dd.x * p.y - dd.y * p.x; best = -1; ee1 = +uu - vv; if ((ee1 < 0) == (w < 0)) { if (best < mju_abs(ee1)) { best = mju_abs(ee1); c1 = 0; } } ee1 = -uu - vv; if ((ee1 < 0) == (w < 0)) { if (best < mju_abs(ee1)) { best = mju_abs(ee1); c1 = 1; } } ee1 = +uu + vv; if ((ee1 < 0) == (w < 0)) { if (best < mju_abs(ee1)) { best = mju_abs(ee1); c1 = 2; } } ee1 = -uu + vv; if ((ee1 < 0) == (w < 0)) { if (best < mju_abs(ee1)) { best = mju_abs(ee1); c1 = 3; } } // c.x = s.x * ((c1 / 2) ? -1 : 1); // c.y = s.y * ((c1 % 2) ? -1 : 1); ee1 = mju_abs(w) / l; // e2 = best / l; // printf("%g %g %g %g %g %g\n",c.x,c.y,d.x,d.y,e1,e2); // tmp1.x = c.x - p.x; // tmp1.y = c.y - p.y; ee1 = dd.x * dd.x + dd.y * dd.y; // e2 = tmp1.x * d.x + tmp1.y * d.y; // e3 = e2 / e1; // printf("%g %g %g %g %g %g %g \n",c.x,c.y,d.x,d.y,e1,e2,e3); ee1 = p.x + (+s.y - p.y) / dd.y * dd.x; // e2 = p.x + (-s.y - p.y) / d.y * d.x; // e3 = p.y + (+s.x - p.x) / d.x * d.y; // e4 = p.y + (-s.x - p.x) / d.x * d.y; // printf("%g %g %g %g\n",e1,e2,e3,e4); } } // goto skip; // allow only the closest contact // cltype: -3 -1 : face is closest to the capsule // cltype: 0..8 : edge is closest to the capsule // cltype/3==0 means the lower corner is closest to the capsule (note that edges include corners) // cltype/3==2 means the upper corner is closest to the capsule (note that edges include corners) // cltype/3==1 means the middle of the edge is closest to the capsule // cltype%3==0 means the lower corner is closest to the box (note that edges include corners) // cltype%3==2 means the upper corner is closest to the box (note that edges include corners) // cltype%3==1 means the middle of the capsule is closest to the box // invalid type if (cltype == -4) return 0; if (cltype >= 0 && cltype / 3 != 1) { // closest to a corner of the box c1 = axisdir ^ clcorner; // hack to find the relative orientation of capsule and corner // there are 2 cases: // 1: pointing to or away from the corner // 2: oriented along a face or an edge if (c1 == 0 || c1 == 7) goto skip; // case 1: no chance of additional contact if (c1 == 1 || c1 == 2 || c1 == 4) { mul = 1; de = 1 - bestsegmentpos; dp = 1 + bestsegmentpos; } if (c1 == 3 || c1 == 5 || c1 == 6) { mul = -1; c1 = 7 - c1; dp = 1 - bestsegmentpos; de = 1 + bestsegmentpos; } // "de" and "dp" distance from first closest point on the capsule to both ends of it // mul is a direction along the capsule's axis if (c1 == 1) ax = 0, ax1 = 1, ax2 = 2; if (c1 == 2) ax = 1, ax1 = 2, ax2 = 0; if (c1 == 4) ax = 2, ax1 = 0, ax2 = 1; if (axis[ax]*axis[ax] > 0.5) { // second point along the edge of the box secondpos = de; // initial position from the e1 = 2 * size2[ax] / mju_abs(halfaxis[ax]); if (e1 < secondpos) { secondpos = e1; // we overshoot, move back to the other corner of the edge } secondpos *= mul; } else { // second point along a face of the box secondpos = dp; // check for overshoot again e1 = 2 * size2[ax1] / mju_abs(halfaxis[ax1]); if (e1 < secondpos) secondpos = e1; e1 = 2 * size2[ax2] / mju_abs(halfaxis[ax2]); if (e1 < secondpos) secondpos = e1; secondpos *= -mul; } } else if (cltype >= 0 && cltype / 3 == 1) { // we are on box's edge // hacks to find the relative orientation of capsule and edge // there are 2 cases: // c1= 2^n: edge and capsule are oriented in a T configuration (no more contacts // c1!=2^n: oriented in a cross X configuration c1 = axisdir ^ clcorner; // same trick c1 &= 7 - (1 << cledge); // even more hacks // printf("%d %d %d %d %lf %lf %lf\n", // axisdir,clcorner,c1,cledge,halfaxis[0],halfaxis[1],halfaxis[2]); if (c1 != 1 && c1 != 2 && c1 != 4) goto skip; if (cledge == 0) ax1 = 1, ax2 = 2; if (cledge == 1) ax1 = 2, ax2 = 0; if (cledge == 2) ax1 = 0, ax2 = 1; ax = cledge; // Then it finds with which face the capsule has a lower angle and switches the axis names if (mju_abs(axis[ax1]) > mju_abs(axis[ax2])) ax1 = ax2; ax2 = 3 - ax - ax1; // keep track of the axis orientation (mul will tell us which direction along the capsule to // find the second point) you can notice all other references to the axis "halfaxis" are with // absolute value if (c1 & (1 << ax2)) { mul = 1; secondpos = 1 - bestsegmentpos; } else { mul = -1; secondpos = 1 + bestsegmentpos; } // now we have to find out whether we point towards the opposite side or towards one of the // sides and also find the farthest point along the capsule that is above the box e1 = 2 * size2[ax2] / mju_abs(halfaxis[ax2]); if (e1 < secondpos) secondpos = e1; if (((axisdir & (1 << ax)) != 0) == ((c1 & (1 << ax2)) != 0)) // that is insane e2 = 1 - bestboxpos; else e2 = 1 + bestboxpos; e1 = size2[ax] * e2 / mju_abs(halfaxis[ax]); if (e1 < secondpos) secondpos = e1; secondpos *= mul; } else if (cltype < 0) { // similarly we handle the case when one capsule's end is closest to a face of the box // and find where is the other end pointing to and clamping to the farthest point // of the capsule that's above the box if (clface == -1) goto skip; // here the closest point is inside the box, no need for a second point if (cltype == -3) mul = 1; else mul = -1; secondpos = 2; mji_copy3(tmp1, pos); mji_addToScl3(tmp1, halfaxis, -mul); for (i = 0; i < 3; i++) { if (i != clface) { e1 = (size2[i] - tmp1[i]) / halfaxis[i] * mul; if (e1 > 0) if (e1 < secondpos) secondpos = e1; e1 = (-size2[i] - tmp1[i]) / halfaxis[i] * mul; if (e1 > 0) if (e1 < secondpos) secondpos = e1; } } secondpos *= mul; } skip: // create sphere in original orientation at first contact point mju_copy3(tmp1, pos); mji_addToScl3(tmp1, halfaxis, bestsegmentpos); mju_mulMatVec3(tmp2, mat2, tmp1); mju_addTo3(tmp2, pos2); // collide with n = mjraw_SphereBox(con, margin, tmp2, mat1, size1, pos2, mat2, size2); if (secondpos > -3) { // secondpos was modified mju_copy3(tmp1, pos); mji_addToScl3(tmp1, halfaxis, secondpos + bestsegmentpos); // note the summation mju_mulMatVec3(tmp2, mat2, tmp1); mju_addTo3(tmp2, pos2); n += mjraw_SphereBox(con + n, margin, tmp2, mat1, size1, pos2, mat2, size2); } return n; } // capsule : box int mjc_CapsuleBox(const mjModel* m, mjData* d, mjPreContact* con, int g1, int g2, mjtNum margin) { const mjtNum* pos1 = d->geom_xpos + 3*g1; const mjtNum* pos2 = d->geom_xpos + 3*g2; const mjtNum* mat1 = d->geom_xmat + 9*g1; const mjtNum* mat2 = d->geom_xmat + 9*g2; const mjtNum* size1 = m->geom_size + 3*g1; const mjtNum* size2 = m->geom_size + 3*g2; return mjraw_CapsuleBox(con, margin, pos1, mat1, size1, pos2, mat2, size2); } // A box-box contact manifold is computed in two stages. // // Stage 1, separating-axis test: find the axis of maximum separation among the 15 candidate // directions (3 face normals per box, 9 cross products of edge directions). If the boxes are // separated by more than margin along any candidate axis there is no contact. Face axes are // preferred over edge axes on near-ties: a face axis yields a multi-point manifold, which the // solver strongly prefers over a single edge contact of nearly identical depth. // // Stage 2, manifold generation, depends on the kind of winning axis: // - face axis: the owner of the face is the reference box. The face of the other (incident) // box least aligned with the reference normal is clipped against the four side planes of // the reference face (Sutherland-Hodgman). Every clipped vertex within the margin band // becomes a contact. Depth is the distance between the surfaces along the reference // normal; contact position is midway between the surfaces along the normal, so it lies // inside the intersection of the margin-inflated boxes. // - edge axis: the contact is at the midpoint of the closest-point pair between the two // supporting edge segments, with depth measured along the separating axis. // // Every surviving clipped vertex becomes a contact, so a face manifold carries at most // mjBOXBOX_MAXVERT points. Reducing the patch below the clipped polygon is not worth it: // on stacks of plates, whose contact patch is wide relative to their thickness, dropping // the polygon to a four-point subset costs two to three orders of magnitude in residual // motion at rest, because the support polygon shrinks and its vertex subset changes from // step to step as the plates shift. // Rounding scales, in units of mjtNum epsilon. Supports are sums of products of box // extents with rotation entries, so their absolute error is proportional to the extents: // mjBOXBOX_SEPEPS multiplies the summed half-sizes. The rest are dimensionless. #ifdef mjUSESINGLE #define mjBOXBOX_SEPEPS 1e-6f // slack on the separation tests, times the box scale #define mjBOXBOX_PAREPS 1e-7f // sin^2 below which an edge-cross axis is noise #define mjBOXBOX_SGNEPS 1e-5f // axis component below which a support corner is ambiguous #define mjBOXBOX_DUPEPS 1e-10f // squared relative radius for clip-vertex deduplication #else #define mjBOXBOX_SEPEPS 1e-13 #define mjBOXBOX_PAREPS 1e-16 #define mjBOXBOX_SGNEPS 1e-9 #define mjBOXBOX_DUPEPS 1e-14 #endif // relative penalty applied to edge-axis separation on near-ties with the best face axis #define mjBOXBOX_EDGEBIAS 1e-6 // vertex capacity for face clipping: a 4-gon clipped by 4 half-planes has at most 8 // vertices, each of which may become a contact (mjMAXCONPAIR is far above that) #define mjBOXBOX_MAXVERT 12 // clip polygon *cur (nin vertices) against the half-plane sign*v[coord] <= limit; when // every vertex is already inside, *cur is left untouched (no copies, the common resting // case); otherwise the result is written to spare and the buffers are swapped; vertices // are (x, y, z) with z interpolated as an attribute; returns the vertex count static int clipHalfPlane(int nin, mjtNum (**cur)[3], mjtNum (**spare)[3], int coord, mjtNum sign, mjtNum limit) { mjtNum (*in)[3] = *cur; mjtNum d[mjBOXBOX_MAXVERT]; int all_inside = 1; for (int k = 0; k < nin; k++) { d[k] = sign*in[k][coord] - limit; all_inside &= d[k] <= 0; } if (all_inside) { return nin; } mjtNum (*out)[3] = *spare; int nout = 0; for (int k = 0; k < nin; k++) { const mjtNum* p = in[k]; int k1 = k + 1 == nin ? 0 : k + 1; mjtNum dp = d[k], dq = d[k1]; // emit p if inside if (dp <= 0 && nout < mjBOXBOX_MAXVERT) { mji_copy3(out[nout++], p); } // emit intersection if the edge strictly crosses the plane if (((dp < 0 && dq > 0) || (dp > 0 && dq < 0)) && nout < mjBOXBOX_MAXVERT) { const mjtNum* q = in[k1]; mjtNum t = dp / (dp - dq); out[nout][0] = p[0] + t*(q[0] - p[0]); out[nout][1] = p[1] + t*(q[1] - p[1]); out[nout][2] = p[2] + t*(q[2] - p[2]); nout++; } } *cur = out; *spare = in; return nout; } // box : box int mjc_BoxBox(const mjModel* m, mjData* d, mjPreContact* con, int g1, int g2, mjtNum margin) { const mjtNum* pos1 = d->geom_xpos + 3*g1; const mjtNum* pos2 = d->geom_xpos + 3*g2; const mjtNum* mat1 = d->geom_xmat + 9*g1; const mjtNum* mat2 = d->geom_xmat + 9*g2; const mjtNum* size1 = m->geom_size + 3*g1; const mjtNum* size2 = m->geom_size + 3*g2; // rot: box2 axes in box1 frame (columns); pos21: box2 center in box1 frame; // pos12: box1 center in box2 frame mjtNum rot[9], rotabs[9], pos21[3], pos12[3], tmp[3]; mji_sub3(tmp, pos2, pos1); mji_mulMatTVec3(pos21, mat1, tmp); mji_sub3(tmp, pos1, pos2); mji_mulMatTVec3(pos12, mat2, tmp); mju_mulMatTMat3(rot, mat1, mat2); for (int i = 0; i < 9; i++) { rotabs[i] = mju_abs(rot[i]); } //------------------------------ stage 1: separating-axis test // the separation tests decide contact against no contact, so they carry rounding slack: // without it a box pair that genuinely overlaps by less than the rounding error of its // own support evaluation is reported as separated, and the boxes pass through each other mjtNum septol = margin + mjBOXBOX_SEPEPS*(size1[0] + size1[1] + size1[2] + size2[0] + size2[1] + size2[2]); // best separation so far (most positive; negative = penetration), and the winning axis: // code 0..2 face of box1, 3..5 face of box2, >= 6 edge pair (i, j) as 6 + 3*i + j mjtNum sep_best = -mjMAXVAL; mjtNum sep_face = -mjMAXVAL; int code = -1; // face axes of box1: candidate normal is axis i of box1 for (int i = 0; i < 3; i++) { mjtNum radius2 = rotabs[3*i+0]*size2[0] + rotabs[3*i+1]*size2[1] + rotabs[3*i+2]*size2[2]; mjtNum sep = mju_abs(pos21[i]) - size1[i] - radius2; if (sep > septol) { return 0; } if (sep > sep_best) { sep_best = sep; code = i; } } // face axes of box2: candidate normal is axis j of box2 for (int j = 0; j < 3; j++) { mjtNum radius1 = rotabs[0+j]*size1[0] + rotabs[3+j]*size1[1] + rotabs[6+j]*size1[2]; mjtNum sep = mju_abs(pos12[j]) - size2[j] - radius1; if (sep > septol) { return 0; } if (sep > sep_best) { sep_best = sep; code = 3 + j; } } sep_face = sep_best; int code_face = code; // edge-cross axes: candidate direction is axis i of box1 crossed with axis j of box2 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { // cross product of e_i with column j of rot, in box1 frame; component i is zero int i1 = (i + 1) % 3, i2 = (i + 2) % 3; mjtNum ax1 = -rot[3*i2+j]; mjtNum ax2 = rot[3*i1+j]; // the cross product of two unit vectors has norm sin(angle); for nearly parallel // edges the components above are pure cancellation noise and the direction is // meaningless, so require sin(angle) well above rounding; the skipped axes are // covered by the face normals, which the cross product converges to as the angle // vanishes mjtNum norm2 = ax1*ax1 + ax2*ax2; if (norm2 < mjBOXBOX_PAREPS) { continue; } mjtNum inv = 1/mju_sqrt(norm2); ax1 *= inv; ax2 *= inv; // support radius of box1: component i of axis is zero by construction mjtNum radius1 = size1[i1]*mju_abs(ax1) + size1[i2]*mju_abs(ax2); // support radius of box2: transform axis to box2 frame; component j is zero there, // and only components i1, i2 of the axis are nonzero here int j1 = (j + 1) % 3, j2 = (j + 2) % 3; mjtNum a2_1 = ax1*rot[3*i1+j1] + ax2*rot[3*i2+j1]; mjtNum a2_2 = ax1*rot[3*i1+j2] + ax2*rot[3*i2+j2]; mjtNum radius2 = size2[j1]*mju_abs(a2_1) + size2[j2]*mju_abs(a2_2); mjtNum sep = mju_abs(ax1*pos21[i1] + ax2*pos21[i2]) - radius1 - radius2; if (sep > septol) { return 0; } // an edge axis must beat the best face axis by a bias-scaled amount: on exact ties // the face manifold (multiple points) is strictly better for the solver if (sep - mjBOXBOX_EDGEBIAS*mju_abs(sep) > sep_best && sep > sep_face) { sep_best = sep; code = 6 + 3*i + j; } } } if (code < 0) { return 0; // cannot happen: some face axis always sets code } // a winning edge axis nearly parallel to the best face axis (within ~8 degrees) // duplicates it: the face manifold covers the same contact with multiple points, and // resting stacks flip between the two codes by rounding noise if the near-tie is // allowed to alternate. The face is substituted unless the edge is better by five // percent of the face depth (ODE's classic fudge): resting-stack energy degrades // continuously as this margin shrinks, while the depth cost of the substitution is // bounded by the same five percent. Substituting after the search, rather than // filtering during it, prevents a worse non-aliasing edge from stealing the contact // that the substitution meant to give to the face. if (code >= 6) { int i = (code - 6) / 3; int j = (code - 6) % 3; int i1 = (i + 1) % 3, i2 = (i + 2) % 3; mjtNum axis[3]; axis[i] = 0; axis[i1] = -rot[3*i2+j]; axis[i2] = rot[3*i1+j]; mju_normalize3(axis); mjtNum face_dot; if (code_face < 3) { face_dot = mju_abs(axis[code_face]); } else { int f = code_face - 3; face_dot = mju_abs(axis[0]*rot[0+f] + axis[1]*rot[3+f] + axis[2]*rot[6+f]); } if (face_dot > 0.99 && sep_best < sep_face + 0.05*mju_abs(sep_face) + mjMINVAL) { code = code_face; sep_best = sep_face; } } //------------------------------ stage 2a: edge-edge contact if (code >= 6) { int i = (code - 6) / 3; int j = (code - 6) % 3; int i1 = (i + 1) % 3, i2 = (i + 2) % 3; int j1 = (j + 1) % 3, j2 = (j + 2) % 3; // unit separating axis in box1 frame, oriented from box1 toward box2 mjtNum axis[3]; axis[i] = 0; axis[i1] = -rot[3*i2+j]; axis[i2] = rot[3*i1+j]; mju_normalize3(axis); if (mju_dot3(axis, pos21) < 0) { axis[0] = -axis[0]; axis[1] = -axis[1]; axis[2] = -axis[2]; } // supporting edges: the box1 edge runs along e_i at a corner selected by the axis // signs in (i1, i2); the box2 edge runs along column j at a corner selected by the // signs of the axis in box2 coordinates. A near-zero component makes the sign choice // meaningless -- both edges support the axis -- and rounding can pick the wrong one, // producing witness points on the wrong side of the box. Enumerate both signs for any // ambiguous component (at most one per box) and keep the closest witness pair. mjtNum a2[3] = { axis[0]*rot[0+0] + axis[1]*rot[3+0] + axis[2]*rot[6+0], axis[0]*rot[0+1] + axis[1]*rot[3+1] + axis[2]*rot[6+1], axis[0]*rot[0+2] + axis[1]*rot[3+2] + axis[2]*rot[6+2], }; const mjtNum ambig = mjBOXBOX_SGNEPS; int amb1 = -1, amb2 = -1; if (mju_abs(axis[i1]) < ambig) amb1 = i1; else if (mju_abs(axis[i2]) < ambig) amb1 = i2; if (mju_abs(a2[j1]) < ambig) amb2 = j1; else if (mju_abs(a2[j2]) < ambig) amb2 = j2; mjtNum d2[3] = {rot[0+j], rot[3+j], rot[6+j]}; mjtNum b = d2[i]; // d1 . d2, with d1 = e_i mjtNum denom = 1 - b*b; mjtNum w1[3], w2[3]; mjtNum best_d2 = mjMAXVAL; for (int v1 = 0; v1 < (amb1 >= 0 ? 2 : 1); v1++) { for (int v2 = 0; v2 < (amb2 >= 0 ? 2 : 1); v2++) { // corner of the box1 edge: support along +axis, ambiguous component flipped by v1 mjtNum c1[3]; c1[i] = 0; c1[i1] = axis[i1] >= 0 ? size1[i1] : -size1[i1]; c1[i2] = axis[i2] >= 0 ? size1[i2] : -size1[i2]; if (amb1 >= 0 && v1) c1[amb1] = -c1[amb1]; // corner of the box2 edge: support along -axis in box2 coordinates mjtNum cc[3]; cc[j] = 0; cc[j1] = a2[j1] >= 0 ? -size2[j1] : size2[j1]; cc[j2] = a2[j2] >= 0 ? -size2[j2] : size2[j2]; if (amb2 >= 0 && v2) cc[amb2] = -cc[amb2]; mjtNum c2[3]; mji_mulMatVec3(c2, rot, cc); mji_addTo3(c2, pos21); // closest points between the two edge segments (directions are unit vectors) mjtNum e[3]; mji_sub3(e, c2, c1); mjtNum d1e = e[i]; // d1 . e mjtNum d2e = mju_dot3(d2, e); mjtNum s = denom < mjMINVAL ? 0 : (d1e - b*d2e) / denom; // clamp into the segments, letting each clamp re-solve the other parameter s = mju_clip(s, -size1[i], size1[i]); mjtNum t = mju_clip(b*s - d2e, -size2[j], size2[j]); s = mju_clip(d1e + b*t, -size1[i], size1[i]); mjtNum p1[3], p2[3], gap[3]; mji_copy3(p1, c1); p1[i] += s; mji_copy3(p2, c2); mji_addToScl3(p2, d2, t); mji_sub3(gap, p2, p1); mjtNum gap2 = mju_dot3(gap, gap); if (gap2 < best_d2) { best_d2 = gap2; mji_copy3(w1, p1); mji_copy3(w2, p2); } } } // signed distance along the axis mjtNum gap[3]; mji_sub3(gap, w2, w1); mjtNum dist = mju_dot3(gap, axis); if (dist > septol) { return 0; } // contact at the midpoint of the witness pair: for penetrating edges this is inside both // boxes; in the margin band it is midway between the two surfaces mjtNum mid[3] = {0.5*(w1[0] + w2[0]), 0.5*(w1[1] + w2[1]), 0.5*(w1[2] + w2[2])}; con[0].dist = dist; mji_mulMatVec3(tmp, mat1, mid); mji_add3(con[0].pos, tmp, pos1); mji_mulMatVec3(con[0].normal, mat1, axis); mji_zero3(con[0].tangent); return 1; } //------------------------------ stage 2b: face contact // reference box: owner of the winning face; incident box: the other one int ref1 = code < 3; // is box1 the reference? int a = ref1 ? code : code - 3; // face axis of the reference box const mjtNum* sizeref = ref1 ? size1 : size2; const mjtNum* sizeinc = ref1 ? size2 : size1; const mjtNum* posref = ref1 ? pos1 : pos2; const mjtNum* matref = ref1 ? mat1 : mat2; const mjtNum* posoi = ref1 ? pos21 : pos12; // incident center in reference frame // incident box axes in reference frame: rot maps box2 to box1, transpose maps box1 to box2; // rinc(r, c) = component r of incident axis c, in reference frame mjtNum rinc[9]; if (ref1) { mju_copy(rinc, rot, 9); } else { mju_transpose(rinc, rot, 3, 3); } // face direction: +1 if the incident box lies along +a, else -1 mjtNum sgn = posoi[a] >= 0 ? 1 : -1; // incident face: the face of the incident box most opposed to the reference face normal int binc = 0; for (int k = 1; k < 3; k++) { if (mju_abs(rinc[3*a+k]) > mju_abs(rinc[3*a+binc])) { binc = k; } } mjtNum tinc = sgn*rinc[3*a+binc] > 0 ? -1 : 1; // sign making the incident normal oppose // corners of the incident face in reference frame, cyclic winding; the in-plane // coordinates are (x, y) = the two non-a reference axes, z is the signed distance // above the reference face plane (negative = inside the reference box) int ax = (a + 1) % 3, ay = (a + 2) % 3; int bu = (binc + 1) % 3, bv = (binc + 2) % 3; mjtNum poly[2][mjBOXBOX_MAXVERT][3]; // face center and in-face half-edge offsets, in the projected (x, y, z) coordinates mjtNum cx[3], du[3], dv[3]; for (int r = 0; r < 3; r++) { int c = r == 0 ? ax : (r == 1 ? ay : a); cx[r] = posoi[c] + tinc*sizeinc[binc]*rinc[3*c+binc]; du[r] = sizeinc[bu]*rinc[3*c+bu]; dv[r] = sizeinc[bv]*rinc[3*c+bv]; } cx[2] = sgn*cx[2] - sizeref[a]; du[2] *= sgn; dv[2] *= sgn; static const mjtNum corner_sign[4][2] = {{1, 1}, {-1, 1}, {-1, -1}, {1, -1}}; for (int k = 0; k < 4; k++) { mjtNum su = corner_sign[k][0], sv = corner_sign[k][1]; poly[0][k][0] = cx[0] + su*du[0] + sv*dv[0]; poly[0][k][1] = cx[1] + su*du[1] + sv*dv[1]; poly[0][k][2] = cx[2] + su*du[2] + sv*dv[2]; } // clip against the four side planes of the reference face; the buffers swap only on // passes that actually clip int nvert = 4; mjtNum (*cur)[3] = poly[0]; mjtNum (*spare)[3] = poly[1]; nvert = clipHalfPlane(nvert, &cur, &spare, 0, 1, sizeref[ax]); nvert = clipHalfPlane(nvert, &cur, &spare, 0, -1, sizeref[ax]); nvert = clipHalfPlane(nvert, &cur, &spare, 1, 1, sizeref[ay]); nvert = clipHalfPlane(nvert, &cur, &spare, 1, -1, sizeref[ay]); // accept vertices within the margin band, dropping near-duplicates produced by clipping // at polygon corners; duplicate radius is relative to the reference face scale mjtNum accepted[mjBOXBOX_MAXVERT][3]; int naccept = 0; mjtNum dupe2 = mjBOXBOX_DUPEPS*(sizeref[ax]*sizeref[ax] + sizeref[ay]*sizeref[ay]); for (int k = 0; k < nvert; k++) { if (cur[k][2] > margin) { continue; } int dupe = 0; for (int q = 0; q < naccept; q++) { mjtNum dx = accepted[q][0] - cur[k][0]; mjtNum dy = accepted[q][1] - cur[k][1]; if (dx*dx + dy*dy < dupe2) { dupe = 1; break; } } if (!dupe) { mji_copy3(accepted[naccept++], cur[k]); } } if (naccept == 0) { return 0; } // world normal points from geom1 to geom2: along +sgn*a of the reference frame when box1 // is the reference, opposite when box2 is mjtNum normal[3]; mjtNum nsign = ref1 ? sgn : -sgn; normal[0] = nsign*matref[3*0+a]; normal[1] = nsign*matref[3*1+a]; normal[2] = nsign*matref[3*2+a]; for (int k = 0; k < naccept; k++) { const mjtNum* v = accepted[k]; // contact position: on the clipped incident polygon in (x, y), midway between the // reference face plane and the incident surface along the face axis mjtNum posc[3]; posc[ax] = v[0]; posc[ay] = v[1]; posc[a] = sgn*(sizeref[a] + 0.5*v[2]); con[k].dist = v[2]; mji_mulMatVec3(tmp, matref, posc); mji_add3(con[k].pos, tmp, posref); mji_copy3(con[k].normal, normal); mji_zero3(con[k].tangent); } return naccept; }