Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add friction direction unit vector and frame parameters #1427

Merged
merged 12 commits into from
Dec 7, 2019
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Added type property to constrain classes: [#1415](https://github.com/dartsim/dart/pull/1415)
* Allowed to set joint rest position out of joint limits: [#1418](https://github.com/dartsim/dart/pull/1418)
* Added secondary friction coefficient parameter: [#1424](https://github.com/dartsim/dart/pull/1424)
* Allowed to set friction direction per ShapeFrame: [#1427](https://github.com/dartsim/dart/pull/1427)

* GUI

Expand Down
118 changes: 91 additions & 27 deletions dart/constraint/ContactConstraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ double ContactConstraint::mConstraintForceMixing = DART_CFM;

constexpr double DART_DEFAULT_FRICTION_COEFF = 1.0;
constexpr double DART_DEFAULT_RESTITUTION_COEFF = 0.0;
const Eigen::Vector3d DART_DEFAULT_FRICTION_DIR = Eigen::Vector3d::UnitZ();

//==============================================================================
ContactConstraint::ContactConstraint(
Expand All @@ -82,7 +83,7 @@ ContactConstraint::ContactConstraint(
->getBodyNodePtr()
.get()),
mContact(contact),
mFirstFrictionalDirection(Eigen::Vector3d::UnitZ()),
mFirstFrictionalDirection(DART_DEFAULT_FRICTION_DIR),
mIsFrictionOn(true),
mAppliedImpulseIndex(dynamics::INVALID_INDEX),
mIsBounceOn(false),
Expand Down Expand Up @@ -115,26 +116,62 @@ ContactConstraint::ContactConstraint(
// TODO(JS): Assume the frictional coefficient can be changed during
// simulation steps.
// Update mFrictionCoeff
const double primaryFrictionCoeffA =
computePrimaryFrictionCoefficient(shapeNodeA);
const double primaryFrictionCoeffB =
computePrimaryFrictionCoefficient(shapeNodeB);
const double secondaryFrictionCoeffA =
computeSecondaryFrictionCoefficient(shapeNodeA);
const double secondaryFrictionCoeffB =
computeSecondaryFrictionCoefficient(shapeNodeB);
const double primaryFrictionCoeffA
= computePrimaryFrictionCoefficient(shapeNodeA);
const double primaryFrictionCoeffB
= computePrimaryFrictionCoefficient(shapeNodeB);
const double secondaryFrictionCoeffA
= computeSecondaryFrictionCoefficient(shapeNodeA);
const double secondaryFrictionCoeffB
= computeSecondaryFrictionCoefficient(shapeNodeB);

// TODO(JS): Consider providing various ways of the combined friction or
// allowing to override this method by a custom method
mPrimaryFrictionCoeff =
std::min(primaryFrictionCoeffA, primaryFrictionCoeffB);
mSecondaryFrictionCoeff =
std::min(secondaryFrictionCoeffA, secondaryFrictionCoeffB);
if (mPrimaryFrictionCoeff > DART_FRICTION_COEFF_THRESHOLD ||
mSecondaryFrictionCoeff > DART_FRICTION_COEFF_THRESHOLD)
mPrimaryFrictionCoeff
= std::min(primaryFrictionCoeffA, primaryFrictionCoeffB);
mSecondaryFrictionCoeff
= std::min(secondaryFrictionCoeffA, secondaryFrictionCoeffB);
if (mPrimaryFrictionCoeff > DART_FRICTION_COEFF_THRESHOLD
|| mSecondaryFrictionCoeff > DART_FRICTION_COEFF_THRESHOLD)
{
mIsFrictionOn = true;

// Check shapeNodes for valid friction direction unit vectors
auto frictionDirA = computeWorldFirstFrictionDir(shapeNodeA);
auto frictionDirB = computeWorldFirstFrictionDir(shapeNodeB);

// check if the friction direction unit vectors have been set
bool nonzeroDirA
= frictionDirA.squaredNorm() >= DART_CONTACT_CONSTRAINT_EPSILON_SQUARED;
bool nonzeroDirB
= frictionDirB.squaredNorm() >= DART_CONTACT_CONSTRAINT_EPSILON_SQUARED;

// only consider custom friction direction if one has nonzero length
if (nonzeroDirA || nonzeroDirB)
{
// if A and B are both set, choose one with smaller friction coefficient
// since it's friction properties will dominate
if (nonzeroDirA && nonzeroDirB)
{
if (primaryFrictionCoeffA <= primaryFrictionCoeffB)
{
mFirstFrictionalDirection = frictionDirA.normalized();
}
else
{
mFirstFrictionalDirection = frictionDirB.normalized();
}
}
else if (nonzeroDirA)
{
mFirstFrictionalDirection = frictionDirA.normalized();
}
else
{
mFirstFrictionalDirection = frictionDirB.normalized();
}
}

// Update frictional direction
updateFirstFrictionalDirection();
}
Expand All @@ -147,11 +184,6 @@ ContactConstraint::ContactConstraint(
assert(mBodyNodeB->getSkeleton());
mIsSelfCollision = (mBodyNodeA->getSkeleton() == mBodyNodeB->getSkeleton());

const math::Jacobian jacA
= mBodyNodeA->getSkeleton()->getJacobian(mBodyNodeA);
const math::Jacobian jacB
= mBodyNodeB->getSkeleton()->getJacobian(mBodyNodeB);

// Compute local contact Jacobians expressed in body frame
if (mIsFrictionOn)
{
Expand Down Expand Up @@ -547,7 +579,7 @@ void ContactConstraint::applyUnitImpulse(std::size_t index)
// Both bodies are not reactive
else
{
// This case should not be happed
// This case should not be happened
assert(0);
}
}
Expand Down Expand Up @@ -754,6 +786,36 @@ double ContactConstraint::computeSecondaryFrictionCoefficient(
return dynamicAspect->getSecondaryFrictionCoeff();
}

//==============================================================================
Eigen::Vector3d ContactConstraint::computeWorldFirstFrictionDir(
const dynamics::ShapeNode* shapeNode)
{
assert(shapeNode);

auto dynamicAspect = shapeNode->getDynamicsAspect();

if (dynamicAspect == nullptr)
{
dtwarn << "[ContactConstraint] Attempt to extract friction direction "
<< "from a ShapeNode that doesn't have DynamicAspect. The default "
<< "value (" << DART_DEFAULT_FRICTION_DIR.transpose()
<< ") will be used instead.\n";
return DART_DEFAULT_FRICTION_DIR;
}

auto frame = dynamicAspect->getFirstFrictionDirectionFrame();
const Eigen::Vector3d& frictionDir
= dynamicAspect->getFirstFrictionDirection();

// rotate using custom frame if it is specified
if (frame)
{
return frame->getWorldTransform().linear() * frictionDir;
}
// otherwise rotate using shapeNode
return shapeNode->getWorldTransform().linear() * frictionDir;
}

//==============================================================================
double ContactConstraint::computeRestitutionCoefficient(
const dynamics::ShapeNode* shapeNode)
Expand Down Expand Up @@ -810,24 +872,24 @@ ContactConstraint::getTangentBasisMatrixODE(const Eigen::Vector3d& n)

// Pick an arbitrary vector to take the cross product of (in this case,
// Z-axis)
Eigen::Vector3d tangent = mFirstFrictionalDirection.cross(n);
Eigen::Vector3d tangent = n.cross(mFirstFrictionalDirection);

// TODO(JS): Modify following lines once _updateFirstFrictionalDirection() is
// implemented.
// If they're too close (or opposing directions, or one of the vectors 0),
// pick another tangent (use X-axis as arbitrary vector)
if (tangent.squaredNorm() < DART_CONTACT_CONSTRAINT_EPSILON_SQUARED)
{
tangent = Eigen::Vector3d::UnitX().cross(n);
tangent = n.cross(Eigen::Vector3d::UnitX());

// Make sure this is not zero length, otherwise normalization will lead to
// NaN values.
if (tangent.squaredNorm() < DART_CONTACT_CONSTRAINT_EPSILON_SQUARED)
{
tangent = Eigen::Vector3d::UnitY().cross(n);
tangent = n.cross(Eigen::Vector3d::UnitY());
if (tangent.squaredNorm() < DART_CONTACT_CONSTRAINT_EPSILON_SQUARED)
{
tangent = Eigen::Vector3d::UnitZ().cross(n);
tangent = n.cross(Eigen::Vector3d::UnitZ());

// Now tangent shouldn't be zero-length unless the normal is
// zero-length, which shouldn't the case because ConstraintSolver
Expand All @@ -850,8 +912,10 @@ ContactConstraint::getTangentBasisMatrixODE(const Eigen::Vector3d& n)
// Note: a possible speedup is in place for mNumDir % 2 = 0
// Each basis and its opposite belong in the matrix, so we iterate half as
// many times
T.col(0) = tangent;
T.col(1) = Eigen::Quaterniond(Eigen::AngleAxisd(0.5_pi, n)) * tangent;
// The first column is the same as mFirstFrictionalDirection unless
// mFirstFrictionalDirection is parallel to the normal
T.col(0) = Eigen::Quaterniond(Eigen::AngleAxisd(0.5_pi, n)) * tangent;
T.col(1) = tangent;
Comment on lines +915 to +918
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the part of this change, but I would like to negate the direction of tangent so that T.col(0).cross(T.col(1)) == n (currently, it's T.col(1).cross(T.col(0)) == n) because I believe it'd be more intuitive. This change wouldn't affect the behavior (but just flipping the sign of impulse for the second tangential friction).

It can be done by changing the order of all the cross products in this function. For example, line 813 should be changed to:

Eigen::Vector3d tangent = n.cross(mFirstFrictionalDirection);

or simply negating the final tangent. I'd prefer changing the order of the cross products though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to that, but I'll bring @azeey into the conversation since he made this specific change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negating the direction of tangent sounds fine to me if it doesn't affect the behavior. I think the main change here is that T.col(0) has to be parallel to the first frictional direction.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reversed in 5fe40b7

return T;
}

Expand Down
2 changes: 2 additions & 0 deletions dart/constraint/ContactConstraint.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ class ContactConstraint : public ConstraintBase
const dynamics::ShapeNode* shapeNode);
static double computeSecondaryFrictionCoefficient(
const dynamics::ShapeNode* shapeNode);
static Eigen::Vector3d computeWorldFirstFrictionDir(
const dynamics::ShapeNode* shapenode);
static double computeRestitutionCoefficient(
const dynamics::ShapeNode* shapeNode);

Expand Down
29 changes: 23 additions & 6 deletions dart/dynamics/ShapeFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ DynamicsAspectProperties::DynamicsAspectProperties(
const double frictionCoeff, const double restitutionCoeff)
: mFrictionCoeff(frictionCoeff),
mRestitutionCoeff(restitutionCoeff),
mSecondaryFrictionCoeff(frictionCoeff)
mSecondaryFrictionCoeff(frictionCoeff),
mFirstFrictionDirection(Eigen::Vector3d::Zero()),
Copy link
Member

@jslee02 jslee02 Dec 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the first friction direction will be set to z-direction in world coordinates (unless it's parallel to the contact normal). Maybe we want to set the default direction to non-zero direction (e.g., Eigen::Vector3d::UnitZ()) so that the default behavior is more consistent in terms of the friction direction?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my intent was that an unset vector with zero length would use the global friction direction, and that only friction directions that are explicitly set to have a nonzero length would use their custom direction and frame. See the following logic in ContactConstraint.cpp

  • // only consider custom friction direction if one has nonzero length
    if (nonzeroDirA || nonzeroDirB)
    {
    // if A and B are both set, choose one with smaller friction coefficient
    // since it's friction properties will dominate
    if (nonzeroDirA && nonzeroDirB)
    {
    if (primaryFrictionCoeffA <= primaryFrictionCoeffB)
    {
    mFirstFrictionalDirection = frictionDirA.normalized();
    }
    else
    {
    mFirstFrictionalDirection = frictionDirB.normalized();
    }
    }
    else if (nonzeroDirA)
    {
    mFirstFrictionalDirection = frictionDirA.normalized();
    }
    else
    {
    mFirstFrictionalDirection = frictionDirB.normalized();
    }
    }

does that make sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I read the logic but was suggesting changing the default behavior to using local friction direction (by setting mFirstFrictionDirection to non-zero vector intentionally) because first/second friction coefficients would make more sense when they're aligned with local directions (if they have different values).

Downside of it is that it changes the default simulation behavior (from using global friction direction to local friction direction) though. I'm okay to leave as it is as long as the logic is documented.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a tricky aspect of the friction directions is choosing which to use when custom directions are set for both bodies. I've added logic in this PR to choose the direction from the body with the smaller primary friction coefficient, since that would be the dominant parameter, which makes sense to me, but I haven't tested it, and I'd be wary of making a big behavior change like that without testing.

Is the logic sufficiently documented? If not, I'll add the necessary documentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to not changing the default behavior.

It's well documented in the implementation, but it'd be great if it's also documented in the header as well (maybe on the constructor?).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does cddef7a look to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks awesome! Thank you for adding it.

mFirstFrictionDirectionFrame(nullptr)
{
// Do nothing
}
Expand All @@ -66,10 +68,14 @@ DynamicsAspectProperties::DynamicsAspectProperties(
DynamicsAspectProperties::DynamicsAspectProperties(
const double primaryFrictionCoeff,
const double secondaryFrictionCoeff,
const double restitutionCoeff)
const double restitutionCoeff,
const Eigen::Vector3d& firstFrictionDirection,
const Frame* firstFrictionDirectionFrame)
: mFrictionCoeff(primaryFrictionCoeff),
mRestitutionCoeff(restitutionCoeff),
mSecondaryFrictionCoeff(secondaryFrictionCoeff)
mSecondaryFrictionCoeff(secondaryFrictionCoeff),
mFirstFrictionDirection(firstFrictionDirection),
mFirstFrictionDirectionFrame(firstFrictionDirectionFrame)
{
// Do nothing
}
Expand Down Expand Up @@ -195,9 +201,8 @@ void DynamicsAspect::setFrictionCoeff(const double& value)

double DynamicsAspect::getFrictionCoeff() const
{
return 0.5 * (
mProperties.mFrictionCoeff +
mProperties.mSecondaryFrictionCoeff);
return 0.5
* (mProperties.mFrictionCoeff + mProperties.mSecondaryFrictionCoeff);
}

void DynamicsAspect::setPrimaryFrictionCoeff(const double& value)
Expand All @@ -210,6 +215,18 @@ const double& DynamicsAspect::getPrimaryFrictionCoeff() const
return mProperties.mFrictionCoeff;
}

//==============================================================================
void DynamicsAspect::setFirstFrictionDirectionFrame(const Frame* value)
{
mProperties.mFirstFrictionDirectionFrame = value;
}

//==============================================================================
const Frame* DynamicsAspect::getFirstFrictionDirectionFrame() const
{
return mProperties.mFirstFrictionDirectionFrame;
}

//==============================================================================
ShapeFrame::~ShapeFrame()
{
Expand Down
12 changes: 12 additions & 0 deletions dart/dynamics/ShapeFrame.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ class DynamicsAspect final : public common::AspectWithVersionedProperties<
DART_COMMON_SET_GET_ASPECT_PROPERTY(double, RestitutionCoeff)
// void setRestitutionCoeff(const double& value);
// const double& getRestitutionCoeff() const;

/// Set the frame for interpreting the first friction direction vector.
/// The frame pointer defaults to nullptr, which is interpreted as this
/// ShapeFrame.
void setFirstFrictionDirectionFrame(const Frame* value);

/// Get the frame for the first friction direction vector.
const Frame* getFirstFrictionDirectionFrame() const;

DART_COMMON_SET_GET_ASPECT_PROPERTY(Eigen::Vector3d, FirstFrictionDirection)
// void setFirstFrictionDirection(const Eigen::Vector3d& value);
// const Eigen::Vector3d& getFirstFrictionDirection() const;
};

//==============================================================================
Expand Down
28 changes: 26 additions & 2 deletions dart/dynamics/detail/ShapeFrameAspect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,39 @@ struct DynamicsAspectProperties
/// Secondary coefficient of friction
double mSecondaryFrictionCoeff;

/// First friction direction unit vector
Eigen::Vector3d mFirstFrictionDirection;

/// First friction direction frame
/// The first friction direction unit vector is expressed in this frame
const Frame* mFirstFrictionDirectionFrame;

/// Constructors
/// The frictionCoeff argument will be used for both primary and secondary friction
/// The frictionCoeff argument will be used for both primary and secondary
/// friction
DynamicsAspectProperties(
const double frictionCoeff = 1.0, const double restitutionCoeff = 0.0);

/// Set primary and secondary friction and restitution coefficients.
/// The first friction direction vector and frame may optionally be set.
/// The vector defaults to a zero-vector, which will cause it to be ignored
/// and the global friction directions used instead.
/// If the vector is set to a non-zero vector, the first friction direction
/// for this shape is computed from this vector expressed in the frame
/// given by the Frame pointer. THe Frame pointer defaults to nullptr,
/// which is interpreted as the body-fixed frame of this Shape.
/// Note that if two objects with custom friction directions come into
/// contact, only one of the directions can be chosen.
/// One approach is to use the first friction direction for the ShapeNode
/// with the smaller primary friction coefficient, since that has the
/// dominant effect. See the ContactConstraint implementation for
/// further details.
DynamicsAspectProperties(
const double primaryFrictionCoeff,
const double secondaryFrictionCoeff,
const double restitutionCoeff);
const double restitutionCoeff,
const Eigen::Vector3d& firstFrictionDirection = Eigen::Vector3d::Zero(),
const Frame* firstFrictionDirectionFrame = nullptr);

/// Destructor
virtual ~DynamicsAspectProperties() = default;
Expand Down
Loading