Program Listing for File contact_patch_solver.hxx
↰ Return to documentation for file (include/coal/contact_patch/contact_patch_solver.hxx
)
/*
* Software License Agreement (BSD License)
*
* Copyright (c) 2024, INRIA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of INRIA nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef COAL_CONTACT_PATCH_SOLVER_HXX
#define COAL_CONTACT_PATCH_SOLVER_HXX
#include "coal/data_types.h"
#include "coal/shape/geometric_shapes_traits.h"
namespace coal {
// ============================================================================
inline void ContactPatchSolver::set(const ContactPatchRequest& request) {
// Note: it's important for the number of pre-allocated Vec3s in
// `m_clipping_sets` to be larger than `request.max_size_patch`
// because we don't know in advance how many supports will be discarded to
// form the convex-hulls of the shapes supports which will serve as the
// input of the Sutherland-Hodgman algorithm.
size_t num_preallocated_supports = default_num_preallocated_supports;
if (num_preallocated_supports < 2 * request.getNumSamplesCurvedShapes()) {
num_preallocated_supports = 2 * request.getNumSamplesCurvedShapes();
}
// Used for support set computation of shape1 and for the first iterate of the
// Sutherland-Hodgman algo.
this->support_set_shape1.points().reserve(num_preallocated_supports);
this->support_set_shape1.direction = SupportSetDirection::DEFAULT;
// Used for computing the next iterate of the Sutherland-Hodgman algo.
this->support_set_buffer.points().reserve(num_preallocated_supports);
// Used for support set computation of shape2 and acts as the "clipper" set in
// the Sutherland-Hodgman algo.
this->support_set_shape2.points().reserve(num_preallocated_supports);
this->support_set_shape2.direction = SupportSetDirection::INVERTED;
this->num_samples_curved_shapes = request.getNumSamplesCurvedShapes();
this->patch_tolerance = request.getPatchTolerance();
}
// ============================================================================
template <typename ShapeType1, typename ShapeType2>
void ContactPatchSolver::computePatch(const ShapeType1& s1,
const Transform3s& tf1,
const ShapeType2& s2,
const Transform3s& tf2,
const Contact& contact,
ContactPatch& contact_patch) const {
// Note: `ContactPatch` is an alias for `SupportSet`.
// Step 1
constructContactPatchFrameFromContact(contact, contact_patch);
contact_patch.points().clear();
if ((bool)(shape_traits<ShapeType1>::IsStrictlyConvex) ||
(bool)(shape_traits<ShapeType2>::IsStrictlyConvex)) {
// If a shape is strictly convex, the support set in any direction is
// reduced to a single point. Thus, the contact point `contact.pos` is the
// only point belonging to the contact patch, and it has already been
// computed.
// TODO(louis): even for strictly convex shapes, we can sample the support
// function around the normal and return a pseudo support set. This would
// allow spheres and ellipsoids to have a contact surface, which does make
// sense in certain physics simulation cases.
// Do the same for strictly convex regions of non-strictly convex shapes
// like the ends of capsules.
contact_patch.addPoint(contact.pos);
return;
}
// Step 2 - Compute support set of each shape, in the direction of
// the contact's normal.
// The first shape's support set is called "current"; it will be the first
// iterate of the Sutherland-Hodgman algorithm. The second shape's support set
// is called "clipper"; it will be used to clip "current". The support set
// computation step computes a convex polygon; its vertices are ordered
// counter-clockwise. This is important as the Sutherland-Hodgman algorithm
// expects points to be ranked counter-clockwise.
this->reset(s1, tf1, s2, tf2, contact_patch);
assert(this->num_samples_curved_shapes > 3);
this->supportFuncShape1(&s1, this->support_set_shape1, this->support_guess[0],
this->supports_data[0],
this->num_samples_curved_shapes,
this->patch_tolerance);
this->supportFuncShape2(&s2, this->support_set_shape2, this->support_guess[1],
this->supports_data[1],
this->num_samples_curved_shapes,
this->patch_tolerance);
// We can immediatly return if one of the support set has only
// one point.
if (this->support_set_shape1.size() <= 1 ||
this->support_set_shape2.size() <= 1) {
contact_patch.addPoint(contact.pos);
return;
}
// `eps` is be used to check strict positivity of determinants.
const CoalScalar eps = Eigen::NumTraits<CoalScalar>::dummy_precision();
using Polygon = SupportSet::Polygon;
if ((this->support_set_shape1.size() == 2) &&
(this->support_set_shape2.size() == 2)) {
// Segment-Segment case
// We compute the determinant; if it is non-zero, the intersection
// has already been computed: it's `Contact::pos`.
const Polygon& pts1 = this->support_set_shape1.points();
const Vec2s& a = pts1[0];
const Vec2s& b = pts1[1];
const Polygon& pts2 = this->support_set_shape2.points();
const Vec2s& c = pts2[0];
const Vec2s& d = pts2[1];
const CoalScalar det =
(b(0) - a(0)) * (d(1) - c(1)) >= (b(1) - a(1)) * (d(0) - c(0));
if ((std::abs(det) > eps) || ((c - d).squaredNorm() < eps) ||
((b - a).squaredNorm() < eps)) {
contact_patch.addPoint(contact.pos);
return;
}
const Vec2s cd = (d - c);
const CoalScalar l = cd.squaredNorm();
Polygon& patch = contact_patch.points();
// Project a onto [c, d]
CoalScalar t1 = (a - c).dot(cd);
t1 = (t1 >= l) ? 1.0 : ((t1 <= 0) ? 0.0 : (t1 / l));
const Vec2s p1 = c + t1 * cd;
patch.emplace_back(p1);
// Project b onto [c, d]
CoalScalar t2 = (b - c).dot(cd);
t2 = (t2 >= l) ? 1.0 : ((t2 <= 0) ? 0.0 : (t2 / l));
const Vec2s p2 = c + t2 * cd;
if ((p1 - p2).squaredNorm() >= eps) {
patch.emplace_back(p2);
}
return;
}
//
// Step 3 - Main loop of the algorithm: use the "clipper" polygon to clip the
// "current" polygon. The resulting intersection is the contact patch of the
// contact between s1 and s2. "clipper" and "current" are the support sets of
// shape1 and shape2 (they can be swapped, i.e. clipper can be assigned to
// shape1 and current to shape2, depending on which case we are). Currently,
// to clip one polygon with the other, we use the Sutherland-Hodgman
// algorithm:
// https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm
// In the general case, Sutherland-Hodgman clips one polygon of size >=3 using
// another polygon of size >=3. However, it can be easily extended to handle
// the segment-polygon case.
//
// The maximum size of the output of the Sutherland-Hodgman algorithm is n1 +
// n2 where n1 and n2 are the sizes of the first and second polygon.
const size_t max_result_size =
this->support_set_shape1.size() + this->support_set_shape2.size();
if (this->added_to_patch.size() < max_result_size) {
this->added_to_patch.assign(max_result_size, false);
}
const Polygon* clipper_ptr = nullptr;
Polygon* current_ptr = nullptr;
Polygon* previous_ptr = &(this->support_set_buffer.points());
// Let the clipper set be the one with the most vertices, to make sure it is
// at least a triangle.
if (this->support_set_shape1.size() < this->support_set_shape2.size()) {
current_ptr = &(this->support_set_shape1.points());
clipper_ptr = &(this->support_set_shape2.points());
} else {
current_ptr = &(this->support_set_shape2.points());
clipper_ptr = &(this->support_set_shape1.points());
}
const Polygon& clipper = *(clipper_ptr);
const size_t clipper_size = clipper.size();
for (size_t i = 0; i < clipper_size; ++i) {
// Swap `current` and `previous`.
// `previous` tracks the last iteration of the algorithm; `current` is
// filled by clipping `current` using `clipper`.
Polygon* tmp_ptr = previous_ptr;
previous_ptr = current_ptr;
current_ptr = tmp_ptr;
const Polygon& previous = *(previous_ptr);
Polygon& current = *(current_ptr);
current.clear();
const Vec2s& a = clipper[i];
const Vec2s& b = clipper[(i + 1) % clipper_size];
const Vec2s ab = b - a;
if (previous.size() == 2) {
//
// Segment-Polygon case
//
const Vec2s& p1 = previous[0];
const Vec2s& p2 = previous[1];
const Vec2s ap1 = p1 - a;
const Vec2s ap2 = p2 - a;
const CoalScalar det1 = ab(0) * ap1(1) - ab(1) * ap1(0);
const CoalScalar det2 = ab(0) * ap2(1) - ab(1) * ap2(0);
if (det1 < 0 && det2 < 0) {
// Both p1 and p2 are outside the clipping polygon, i.e. there is no
// intersection. The algorithm can stop.
break;
}
if (det1 >= 0 && det2 >= 0) {
// Both p1 and p2 are inside the clipping polygon, there is nothing to
// do; move to the next iteration.
current = previous;
continue;
}
// Compute the intersection between the line (a, b) and the segment
// [p1, p2].
if (det1 >= 0) {
if (det1 > eps) {
const Vec2s p = computeLineSegmentIntersection(a, b, p1, p2);
current.emplace_back(p1);
current.emplace_back(p);
continue;
} else {
// p1 is the only point of current which is also a point of the
// clipper. We can exit.
current.emplace_back(p1);
break;
}
} else {
if (det2 > eps) {
const Vec2s p = computeLineSegmentIntersection(a, b, p1, p2);
current.emplace_back(p2);
current.emplace_back(p);
continue;
} else {
// p2 is the only point of current which is also a point of the
// clipper. We can exit.
current.emplace_back(p2);
break;
}
}
} else {
//
// Polygon-Polygon case.
//
std::fill(this->added_to_patch.begin(), //
this->added_to_patch.end(), //
false);
const size_t previous_size = previous.size();
for (size_t j = 0; j < previous_size; ++j) {
const Vec2s& p1 = previous[j];
const Vec2s& p2 = previous[(j + 1) % previous_size];
const Vec2s ap1 = p1 - a;
const Vec2s ap2 = p2 - a;
const CoalScalar det1 = ab(0) * ap1(1) - ab(1) * ap1(0);
const CoalScalar det2 = ab(0) * ap2(1) - ab(1) * ap2(0);
if (det1 < 0 && det2 < 0) {
// No intersection. Continue to next segment of previous.
continue;
}
if (det1 >= 0 && det2 >= 0) {
// Both p1 and p2 are inside the clipping polygon, add p1 to current
// (only if it has not already been added).
if (!this->added_to_patch[j]) {
current.emplace_back(p1);
this->added_to_patch[j] = true;
}
// Continue to next segment of previous.
continue;
}
if (det1 >= 0) {
if (det1 > eps) {
if (!this->added_to_patch[j]) {
current.emplace_back(p1);
this->added_to_patch[j] = true;
}
const Vec2s p = computeLineSegmentIntersection(a, b, p1, p2);
current.emplace_back(p);
} else {
// a, b and p1 are colinear; we add only p1.
if (!this->added_to_patch[j]) {
current.emplace_back(p1);
this->added_to_patch[j] = true;
}
}
} else {
if (det2 > eps) {
const Vec2s p = computeLineSegmentIntersection(a, b, p1, p2);
current.emplace_back(p);
} else {
if (!this->added_to_patch[(j + 1) % previous.size()]) {
current.emplace_back(p2);
this->added_to_patch[(j + 1) % previous.size()] = true;
}
}
}
}
}
//
// End of iteration i of Sutherland-Hodgman.
if (current.size() <= 1) {
// No intersection or one point found, the algo can early stop.
break;
}
}
// Transfer the result of the Sutherland-Hodgman algorithm to the contact
// patch.
this->getResult(contact, current_ptr, contact_patch);
}
// ============================================================================
inline void ContactPatchSolver::getResult(
const Contact& contact, const ContactPatch::Polygon* result_ptr,
ContactPatch& contact_patch) const {
if (result_ptr->size() <= 1) {
contact_patch.addPoint(contact.pos);
return;
}
const ContactPatch::Polygon& result = *(result_ptr);
ContactPatch::Polygon& patch = contact_patch.points();
patch = result;
}
// ============================================================================
template <typename ShapeType1, typename ShapeType2>
inline void ContactPatchSolver::reset(const ShapeType1& shape1,
const Transform3s& tf1,
const ShapeType2& shape2,
const Transform3s& tf2,
const ContactPatch& contact_patch) const {
// Reset internal quantities
this->support_set_shape1.clear();
this->support_set_shape2.clear();
this->support_set_buffer.clear();
// Get the support function of each shape
const Transform3s& tfc = contact_patch.tf;
this->support_set_shape1.direction = SupportSetDirection::DEFAULT;
// Set the reference frame of the support set of the first shape to be the
// local frame of shape 1.
Transform3s& tf1c = this->support_set_shape1.tf;
tf1c.rotation().noalias() = tf1.rotation().transpose() * tfc.rotation();
tf1c.translation().noalias() =
tf1.rotation().transpose() * (tfc.translation() - tf1.translation());
this->supportFuncShape1 =
this->makeSupportSetFunction(&shape1, this->supports_data[0]);
this->support_set_shape2.direction = SupportSetDirection::INVERTED;
// Set the reference frame of the support set of the second shape to be the
// local frame of shape 2.
Transform3s& tf2c = this->support_set_shape2.tf;
tf2c.rotation().noalias() = tf2.rotation().transpose() * tfc.rotation();
tf2c.translation().noalias() =
tf2.rotation().transpose() * (tfc.translation() - tf2.translation());
this->supportFuncShape2 =
this->makeSupportSetFunction(&shape2, this->supports_data[1]);
}
// ==========================================================================
inline Vec2s ContactPatchSolver::computeLineSegmentIntersection(
const Vec2s& a, const Vec2s& b, const Vec2s& c, const Vec2s& d) {
const Vec2s ab = b - a;
const Vec2s n(-ab(1), ab(0));
const CoalScalar denominator = n.dot(c - d);
if (std::abs(denominator) < std::numeric_limits<double>::epsilon()) {
return d;
}
const CoalScalar nominator = n.dot(a - d);
CoalScalar alpha = nominator / denominator;
alpha = std::min<double>(1.0, std::max<CoalScalar>(0.0, alpha));
return alpha * c + (1 - alpha) * d;
}
} // namespace coal
#endif // COAL_CONTACT_PATCH_SOLVER_HXX