Merge pull request #10553 from howard-stearns/do-not-get-stuck-in-floor

Do not get stuck in floor
This commit is contained in:
Howard Stearns 2017-06-07 20:11:38 -07:00 committed by GitHub
commit 5352e85418
14 changed files with 197 additions and 25 deletions

View file

@ -177,7 +177,7 @@ ScrollingWindow {
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic"
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});

View file

@ -179,7 +179,7 @@ Rectangle {
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic"
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});

View file

@ -118,7 +118,7 @@ Rectangle {
id: text2
width: 160
color: "#ffffff"
text: qsTr("Models with automatic collisions set to 'Exact' cannot be dynamic")
text: qsTr("Models with automatic collisions set to 'Exact' cannot be dynamic, and should not be used as floors")
wrapMode: Text.WordWrap
font.pixelSize: 12
}

View file

@ -294,6 +294,7 @@ void MyAvatar::simulateAttachments(float deltaTime) {
QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) {
CameraMode mode = qApp->getCamera().getMode();
_globalPosition = getPosition();
// This might not be right! Isn't the capsule local offset in avatar space, and don't we need to add the radius to the y as well? -HRS 5/26/17
_globalBoundingBoxDimensions.x = _characterController.getCapsuleRadius();
_globalBoundingBoxDimensions.y = _characterController.getCapsuleHalfHeight();
_globalBoundingBoxDimensions.z = _characterController.getCapsuleRadius();
@ -436,6 +437,13 @@ void MyAvatar::update(float deltaTime) {
// so we update now. It's ok if it updates again in the normal way.
updateSensorToWorldMatrix();
emit positionGoneTo();
// Run safety tests as soon as we can after goToLocation, or clear if we're not colliding.
_physicsSafetyPending = getCollisionsEnabled();
}
if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) {
// When needed and ready, arrange to check and fix.
_physicsSafetyPending = false;
safeLanding(_goToPosition); // no-op if already safe
}
Head* head = getHead();
@ -450,6 +458,7 @@ void MyAvatar::update(float deltaTime) {
setAudioAverageLoudness(audio->getAudioAverageInputLoudness());
glm::vec3 halfBoundingBoxDimensions(_characterController.getCapsuleRadius(), _characterController.getCapsuleHalfHeight(), _characterController.getCapsuleRadius());
// This might not be right! Isn't the capsule local offset in avatar space? -HRS 5/26/17
halfBoundingBoxDimensions += _characterController.getCapsuleLocalOffset();
QMetaObject::invokeMethod(audio.data(), "setAvatarBoundingBoxParameters",
Q_ARG(glm::vec3, (getPosition() - halfBoundingBoxDimensions)),
@ -1552,6 +1561,10 @@ void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) {
if (_characterController.isEnabledAndReady()) {
setVelocity(_characterController.getLinearVelocity() + _characterController.getFollowVelocity());
if (_characterController.isStuck()) {
_physicsSafetyPending = true;
_goToPosition = getPosition();
}
} else {
setVelocity(getVelocity() + _characterController.getFollowVelocity());
}
@ -2224,6 +2237,144 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition,
emit transformChanged();
}
void MyAvatar::goToLocationAndEnableCollisions(const glm::vec3& position) { // See use case in safeLanding.
goToLocation(position);
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Qt::QueuedConnection, Q_ARG(bool, true));
}
bool MyAvatar::safeLanding(const glm::vec3& position) {
// Considers all collision hull or non-collisionless primitive intersections on a vertical line through the point.
// There needs to be a "landing" if:
// a) the closest above and the closest below are less than the avatar capsule height apart, or
// b) the above point is the top surface of an entity, indicating that we are inside it.
// If no landing is required, we go to that point directly and return false;
// When a landing is required by a, we find the highest intersection on that closest-agbove entity
// (which may be that same "nearest above intersection"). That highest intersection is the candidate landing point.
// For b, use that top surface point.
// We then place our feet there, recurse with new capsule center point, and return true.
if (QThread::currentThread() != thread()) {
bool result;
QMetaObject::invokeMethod(this, "safeLanding", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const glm::vec3&, position));
return result;
}
glm::vec3 better;
if (!requiresSafeLanding(position, better)) {
return false;
}
if (!getCollisionsEnabled()) {
goToLocation(better); // recurses on next update
} else { // If you try to go while stuck, physics will keep you stuck.
setCollisionsEnabled(false);
// Don't goToLocation just yet. Yield so that physics can act on the above.
QMetaObject::invokeMethod(this, "goToLocationAndEnableCollisions", Qt::QueuedConnection, // The equivalent of javascript nextTick
Q_ARG(glm::vec3, better));
}
return true;
}
// If position is not reliably safe from being stuck by physics, answer true and place a candidate better position in betterPositionOut.
bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& betterPositionOut) {
// We begin with utilities and tests. The Algorithm in four parts is below.
auto halfHeight = _characterController.getCapsuleHalfHeight() + _characterController.getCapsuleRadius();
if (halfHeight == 0) {
return false; // zero height avatar
}
auto entityTree = DependencyManager::get<EntityTreeRenderer>()->getTree();
if (!entityTree) {
return false; // no entity tree
}
// More utilities.
const auto offset = getOrientation() *_characterController.getCapsuleLocalOffset();
const auto capsuleCenter = positionIn + offset;
const auto up = _worldUpDirection, down = -up;
glm::vec3 upperIntersection, upperNormal, lowerIntersection, lowerNormal;
EntityItemID upperId, lowerId;
QVector<EntityItemID> include{}, ignore{};
auto mustMove = [&] { // Place bottom of capsule at the upperIntersection, and check again based on the capsule center.
betterPositionOut = upperIntersection + (up * halfHeight) - offset;
return true;
};
auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) {
OctreeElementPointer element;
EntityItemPointer intersectedEntity = NULL;
float distance;
BoxFace face;
const bool visibleOnly = false;
// This isn't quite what we really want here. findRayIntersection always works on mesh, skipping entirely based on collidable.
// What we really want is to use the collision hull!
// See https://highfidelity.fogbugz.com/f/cases/5003/findRayIntersection-has-option-to-use-collidableOnly-but-doesn-t-actually-use-colliders
const bool collidableOnly = true;
const bool precisionPicking = true;
const auto lockType = Octree::Lock; // Should we refactor to take a lock just once?
bool* accurateResult = NULL;
bool intersects = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking,
element, distance, face, normalOut, (void**)&intersectedEntity, lockType, accurateResult);
if (!intersects || !intersectedEntity) {
return false;
}
intersectionOut = startPointIn + (directionIn * distance);
entityIdOut = intersectedEntity->getEntityItemID();
return true;
};
// The Algorithm, in four parts:
if (!findIntersection(capsuleCenter, up, upperIntersection, upperId, upperNormal)) {
// We currently believe that physics will reliably push us out if our feet are embedded,
// as long as our capsule center is out and there's room above us. Here we have those
// conditions, so no need to check our feet below.
return false; // nothing above
}
if (!findIntersection(capsuleCenter, down, lowerIntersection, lowerId, lowerNormal)) {
// Our head may be embedded, but our center is out and there's room below. See corresponding comment above.
return false; // nothing below
}
// See if we have room between entities above and below, but that we are not contained.
// First check if the surface above us is the bottom of something, and the surface below us it the top of something.
// I.e., we are in a clearing between two objects.
if (isDown(upperNormal) && isUp(lowerNormal)) {
auto spaceBetween = glm::distance(upperIntersection, lowerIntersection);
const float halfHeightFactor = 2.5f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise.
if (spaceBetween > (halfHeightFactor * halfHeight)) {
// There is room for us to fit in that clearing. If there wasn't, physics would oscilate us between the objects above and below.
// We're now going to iterate upwards through successive upperIntersections, testing to see if we're contained within the top surface of some entity.
// There will be one of two outcomes:
// a) We're not contained, so we have enough room and our position is good.
// b) We are contained, so we'll bail out of this but try again at a position above the containing entity.
const int iterationLimit = 1000;
for (int counter = 0; counter < iterationLimit; counter++) {
ignore.push_back(upperId);
if (!findIntersection(upperIntersection, up, upperIntersection, upperId, upperNormal)) {
// We're not inside an entity, and from the nested tests, we have room between what is above and below. So position is good!
return false; // enough room
}
if (isUp(upperNormal)) {
// This new intersection is the top surface of an entity that we have not yet seen, which means we're contained within it.
// We could break here and recurse from the top of the original ceiling, but since we've already done the work to find the top
// of the enclosing entity, let's put our feet at upperIntersection and start over.
return mustMove();
}
// We found a new bottom surface, which we're not interested in.
// But there could still be a top surface above us for an entity we haven't seen, so keep looking upward.
}
qCDebug(interfaceapp) << "Loop in requiresSafeLanding. Floor/ceiling do not make sense.";
}
}
include.push_back(upperId); // We're now looking for the intersection from above onto this entity.
const float big = (float)TREE_SCALE;
const auto skyHigh = up * big;
auto fromAbove = capsuleCenter + skyHigh;
if (!findIntersection(fromAbove, down, upperIntersection, upperId, upperNormal)) {
return false; // Unable to find a landing
}
// Our arbitrary rule is to always go up. There's no need to look down or sideways for a "closer" safe candidate.
return mustMove();
}
void MyAvatar::updateMotionBehaviorFromMenu() {
if (QThread::currentThread() != thread()) {

View file

@ -510,6 +510,10 @@ public:
// results are in HMD frame
glm::mat4 deriveBodyFromHMDSensor() const;
Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up.
Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; };
public slots:
void increaseSize();
void decreaseSize();
@ -519,6 +523,8 @@ public slots:
bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(),
bool shouldFaceLocation = false);
void goToLocation(const QVariant& properties);
void goToLocationAndEnableCollisions(const glm::vec3& newPosition);
bool safeLanding(const glm::vec3& position);
void restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject);
void clearScaleRestriction();
@ -564,6 +570,8 @@ signals:
private:
bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut);
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override;
void simulate(float deltaTime);
@ -713,7 +721,8 @@ private:
};
FollowHelper _follow;
bool _goToPending;
bool _goToPending { false };
bool _physicsSafetyPending { false };
glm::vec3 _goToPosition;
glm::quat _goToOrientation;

View file

@ -605,7 +605,7 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori
QString extraInfo;
return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance,
face, surfaceNormal, extraInfo, precisionPicking);
face, surfaceNormal, extraInfo, precisionPicking, precisionPicking);
}
void RenderableModelEntityItem::getCollisionGeometryResource() {

View file

@ -152,6 +152,7 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) {
btDispatcher* dispatcher = collisionWorld->getDispatcher();
int numManifolds = dispatcher->getNumManifolds();
bool hasFloor = false;
bool isStuck = false;
btTransform rotation = _rigidBody->getWorldTransform();
rotation.setOrigin(btVector3(0.0f, 0.0f, 0.0f)); // clear translation part
@ -169,10 +170,18 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) {
btVector3 pointOnCharacter = characterIsFirst ? contact.m_localPointA : contact.m_localPointB; // object-local-frame
btVector3 normal = characterIsFirst ? contact.m_normalWorldOnB : -contact.m_normalWorldOnB; // points toward character
btScalar hitHeight = _halfHeight + _radius + pointOnCharacter.dot(_currentUp);
// If there's non-trivial penetration with a big impulse for several steps, we're probably stuck.
// Note it here in the controller, and let MyAvatar figure out what to do about it.
const float STUCK_PENETRATION = -0.05f; // always negative into the object.
const float STUCK_IMPULSE = 500.0f;
const int STUCK_LIFETIME = 3;
if ((contact.getDistance() < STUCK_PENETRATION) && (contact.getAppliedImpulse() > STUCK_IMPULSE) && (contact.getLifeTime() > STUCK_LIFETIME)) {
isStuck = true; // latch on
}
if (hitHeight < _maxStepHeight && normal.dot(_currentUp) > _minFloorNormalDotUp) {
hasFloor = true;
if (!pushing) {
// we're not pushing against anything so we can early exit
if (!pushing && isStuck) {
// we're not pushing against anything and we're stuck so we can early exit
// (all we need to know is that there is a floor)
break;
}
@ -198,12 +207,13 @@ bool CharacterController::checkForSupport(btCollisionWorld* collisionWorld) {
_stepHeight = highestStep;
_stepPoint = rotation * pointOnCharacter; // rotate into world-frame
}
if (hasFloor && !(pushing && _stepUpEnabled)) {
if (hasFloor && isStuck && !(pushing && _stepUpEnabled)) {
// early exit since all we need to know is that we're on a floor
break;
}
}
}
_isStuck = isStuck;
return hasFloor;
}

View file

@ -110,6 +110,7 @@ public:
void setLocalBoundingBox(const glm::vec3& minCorner, const glm::vec3& scale);
bool isEnabledAndReady() const { return _dynamicsWorld; }
bool isStuck() const { return _isStuck; }
void setCollisionless(bool collisionless);
int16_t computeCollisionGroup() const;
@ -192,6 +193,7 @@ protected:
State _state;
bool _isPushingUp;
bool _isStuck { false };
btDynamicsWorld* _dynamicsWorld { nullptr };
btRigidBody* _rigidBody { nullptr };

View file

@ -332,7 +332,7 @@ void Model::initJointStates() {
bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance,
BoxFace& face, glm::vec3& surfaceNormal,
QString& extraInfo, bool pickAgainstTriangles) {
QString& extraInfo, bool pickAgainstTriangles, bool allowBackface) {
bool intersectedSomething = false;
@ -381,7 +381,7 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g
float triangleSetDistance = 0.0f;
BoxFace triangleSetFace;
glm::vec3 triangleSetNormal;
if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles)) {
if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles, allowBackface)) {
glm::vec3 meshIntersectionPoint = meshFrameOrigin + (meshFrameDirection * triangleSetDistance);
glm::vec3 worldIntersectionPoint = glm::vec3(meshToWorldMatrix * glm::vec4(meshIntersectionPoint, 1.0f));

View file

@ -156,7 +156,7 @@ public:
bool findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance,
BoxFace& face, glm::vec3& surfaceNormal,
QString& extraInfo, bool pickAgainstTriangles = false);
QString& extraInfo, bool pickAgainstTriangles = false, bool allowBackface = false);
void setOffset(const glm::vec3& offset);
const glm::vec3& getOffset() const { return _offset; }

View file

@ -290,12 +290,12 @@ glm::vec3 Triangle::getNormal() const {
}
bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction,
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance) {
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance, bool allowBackface) {
glm::vec3 firstSide = v0 - v1;
glm::vec3 secondSide = v2 - v1;
glm::vec3 normal = glm::cross(secondSide, firstSide);
float dividend = glm::dot(normal, v1) - glm::dot(origin, normal);
if (dividend > 0.0f) {
if (!allowBackface && dividend > 0.0f) {
return false; // origin below plane
}
float divisor = glm::dot(normal, direction);

View file

@ -83,7 +83,7 @@ bool findRayRectangleIntersection(const glm::vec3& origin, const glm::vec3& dire
const glm::vec3& position, const glm::vec2& dimensions, float& distance);
bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction,
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance);
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance, bool allowBackface = false);
/// \brief decomposes rotation into its components such that: rotation = swing * twist
/// \param rotation[in] rotation to decompose
@ -104,8 +104,8 @@ public:
};
inline bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction,
const Triangle& triangle, float& distance) {
return findRayTriangleIntersection(origin, direction, triangle.v0, triangle.v1, triangle.v2, distance);
const Triangle& triangle, float& distance, bool allowBackface = false) {
return findRayTriangleIntersection(origin, direction, triangle.v0, triangle.v1, triangle.v2, distance, allowBackface);
}

View file

@ -31,7 +31,7 @@ void TriangleSet::clear() {
}
bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) {
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface) {
// reset our distance to be the max possible, lower level tests will store best distance here
distance = std::numeric_limits<float>::max();
@ -41,7 +41,7 @@ bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3&
}
int trianglesTouched = 0;
auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched);
auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched, allowBackface);
#if WANT_DEBUGGING
if (precision) {
@ -95,7 +95,7 @@ void TriangleSet::balanceOctree() {
// Determine of the given ray (origin/direction) in model space intersects with any triangles
// in the set. If an intersection occurs, the distance and surface normal will be provided.
bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) {
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) {
bool intersectedSomething = false;
float boxDistance = distance;
@ -114,7 +114,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec
const auto& triangle = _allTriangles[triangleIndex];
float thisTriangleDistance;
trianglesTouched++;
if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) {
if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance, allowBackface)) {
if (thisTriangleDistance < bestDistance) {
bestDistance = thisTriangleDistance;
intersectedSomething = true;
@ -203,7 +203,7 @@ void TriangleSet::TriangleOctreeCell::insert(size_t triangleIndex) {
}
bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched) {
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) {
if (_population < 1) {
return false; // no triangles below here, so we can't intersect
@ -247,7 +247,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi
}
}
// also check our local triangle set
if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) {
if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched, allowBackface)) {
if (childDistance < bestLocalDistance) {
bestLocalDistance = childDistance;
bestLocalFace = childFace;

View file

@ -27,7 +27,7 @@ class TriangleSet {
void clear();
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched);
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false);
const AABox& getBounds() const { return _bounds; }
@ -38,7 +38,7 @@ class TriangleSet {
// checks our internal list of triangles
bool findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched);
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false);
std::vector<Triangle>& _allTriangles;
std::map<AABox::OctreeChild, TriangleOctreeCell> _children;
@ -60,7 +60,7 @@ public:
void insert(const Triangle& t);
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision);
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface = false);
void balanceOctree();