Merge pull request #6244 from sethalves/grab

kinematic grab fixes
This commit is contained in:
Andrew Meadows 2015-10-31 04:36:13 -07:00
commit 9809fcfa54
10 changed files with 165 additions and 60 deletions

View file

@ -15,13 +15,11 @@
Script.include("../libraries/utils.js");
////////////////////////////////////////////////////////////
//
// add lines where the hand ray picking is happening
//
var WANT_DEBUG = false;
/////////////////////////////////////////////////////////////////
//
// these tune time-averaging and "on" value for analog trigger
//
@ -30,7 +28,6 @@ var TRIGGER_SMOOTH_RATIO = 0.1; // 0.0 disables smoothing of trigger value
var TRIGGER_ON_VALUE = 0.4;
var TRIGGER_OFF_VALUE = 0.15;
/////////////////////////////////////////////////////////////////
//
// distant manipulation
//
@ -44,7 +41,6 @@ var LINE_ENTITY_DIMENSIONS = { x: 1000, y: 1000,z: 1000};
var LINE_LENGTH = 500;
var PICK_MAX_DISTANCE = 500; // max length of pick-ray
/////////////////////////////////////////////////////////////////
//
// near grabbing
//
@ -55,8 +51,8 @@ var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held
var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected
var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things
var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
/////////////////////////////////////////////////////////////////
//
// other constants
//
@ -79,6 +75,18 @@ var ACTION_TTL_REFRESH = 5;
var PICKS_PER_SECOND_PER_HAND = 5;
var MSECS_PER_SEC = 1000.0;
var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js
var GRAB_USER_DATA_KEY = "grabKey"; // shared with grab.js
var DEFAULT_GRABBABLE_DATA = {
grabbable: true,
invertSolidWhileHeld: false
};
var disabledHand ='none';
// states for the state machine
var STATE_OFF = 0;
var STATE_SEARCHING = 1;
@ -92,15 +100,35 @@ var STATE_FAR_GRABBING_NON_COLLIDING = 8;
var STATE_CONTINUE_FAR_GRABBING_NON_COLLIDING = 9;
var STATE_RELEASE = 10;
var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js
var GRAB_USER_DATA_KEY = "grabKey"; // shared with grab.js
var DEFAULT_GRABBABLE_DATA = {
grabbable: true,
invertSolidWhileHeld: false
};
function stateToName(state) {
switch (state) {
case STATE_OFF:
return "off";
case STATE_SEARCHING:
return "searching";
case STATE_DISTANCE_HOLDING:
return "distance_holding";
case STATE_CONTINUE_DISTANCE_HOLDING:
return "continue_distance_holding";
case STATE_NEAR_GRABBING:
return "near_grabbing";
case STATE_CONTINUE_NEAR_GRABBING:
return "continue_near_grabbing";
case STATE_NEAR_GRABBING_NON_COLLIDING:
return "near_grabbing_non_colliding";
case STATE_CONTINUE_NEAR_GRABBING_NON_COLLIDING:
return "continue_near_grabbing_non_colliding";
case STATE_FAR_GRABBING_NON_COLLIDING:
return "far_grabbing_non_colliding";
case STATE_CONTINUE_FAR_GRABBING_NON_COLLIDING:
return "continue_far_grabbing_non_colliding";
case STATE_RELEASE:
return "release";
}
var disabledHand ='none';
return "unknown";
}
function getTag() {
return "grab-" + MyAvatar.sessionUUID;
@ -196,7 +224,7 @@ function MyController(hand, triggerAction) {
this.setState = function(newState) {
if (WANT_DEBUG) {
print("STATE: " + this.state + " --> " + newState);
print("STATE: " + stateToName(this.state) + " --> " + stateToName(newState) + ", hand: " + this.hand);
}
this.state = newState;
}
@ -353,10 +381,11 @@ function MyController(hand, triggerAction) {
}
if (intersectionDistance <= NEAR_PICK_MAX_DISTANCE) {
// the hand is very close to the intersected object. go into close-grabbing mode.
if (intersection.properties.collisionsWillMove === 1) {
this.setState(STATE_NEAR_GRABBING);
} else {
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
if (grabbableData.wantsTrigger) {
this.setState(STATE_NEAR_GRABBING_NON_COLLIDING);
} else {
this.setState(STATE_NEAR_GRABBING);
}
} else {
// don't allow two people to distance grab the same object
@ -397,22 +426,22 @@ function MyController(hand, triggerAction) {
}
if (this.grabbedEntity === null) {
return;
} else if (props.locked === 0 && props.collisionsWillMove === 1) {
this.setState(STATE_NEAR_GRABBING);
} else if (props.collisionsWillMove === 0 && grabbableData.wantsTrigger) {
// We have grabbed a non-physical object, so we want to trigger a non-colliding event as opposed to a grab event
}
if (grabbableData.wantsTrigger) {
this.setState(STATE_NEAR_GRABBING_NON_COLLIDING);
} else if (props.locked === 0) {
this.setState(STATE_NEAR_GRABBING);
}
}
};
this.distanceHolding = function() {
var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition;
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation);
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation",
"gravity", "ignoreForCollisions"]);
"gravity", "ignoreForCollisions",
var now = Date.now();
// add the action and initialize some variables
@ -557,8 +586,14 @@ function MyController(hand, triggerAction) {
this.lineOff();
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity,
["position", "rotation", "gravity", "ignoreForCollisions"]);
["position", "rotation", "gravity",
"ignoreForCollisions", "collisionsWillMove"]);
this.activateEntity(this.grabbedEntity, grabbedProperties);
if (grabbedProperties.collisionsWillMove && NEAR_GRABBING_KINEMATIC) {
Entities.editEntity(this.grabbedEntity, {
collisionsWillMove: false
});
}
var handRotation = this.getHandRotation();
var handPosition = this.getHandPosition();
@ -587,7 +622,9 @@ function MyController(hand, triggerAction) {
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
relativePosition: this.offsetPosition,
relativeRotation: this.offsetRotation,
ttl: ACTION_TTL
ttl: ACTION_TTL,
kinematic: NEAR_GRABBING_KINEMATIC,
kinematicSetVelocity: true
});
if (this.actionID === NULL_ACTION_ID) {
this.actionID = null;
@ -639,7 +676,9 @@ function MyController(hand, triggerAction) {
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
relativePosition: this.offsetPosition,
relativeRotation: this.offsetRotation,
ttl: ACTION_TTL
ttl: ACTION_TTL,
kinematic: NEAR_GRABBING_KINEMATIC,
kinematicSetVelocity: true
});
this.actionTimeout = now + (ACTION_TTL * MSEC_PER_SEC);
}
@ -808,12 +847,13 @@ function MyController(hand, triggerAction) {
Entities.callEntityMethod(this.grabbedEntity, "releaseGrab");
}
this.deactivateEntity(this.grabbedEntity);
// the action will tend to quickly bring an object's velocity to zero. now that
// the action is gone, set the objects velocity to something the holder might expect.
Entities.editEntity(this.grabbedEntity, {
velocity: this.grabbedVelocity
});
this.deactivateEntity(this.grabbedEntity);
this.grabbedVelocity = ZERO_VEC;
this.grabbedEntity = null;
@ -836,6 +876,7 @@ function MyController(hand, triggerAction) {
if (data["refCount"] == 1) {
data["gravity"] = grabbedProperties.gravity;
data["ignoreForCollisions"] = grabbedProperties.ignoreForCollisions;
data["collisionsWillMove"] = grabbedProperties.collisionsWillMove;
var whileHeldProperties = {gravity: {x:0, y:0, z:0}};
if (invertSolidWhileHeld) {
whileHeldProperties["ignoreForCollisions"] = ! grabbedProperties.ignoreForCollisions;
@ -853,7 +894,8 @@ function MyController(hand, triggerAction) {
if (data["refCount"] < 1) {
Entities.editEntity(entityID, {
gravity: data["gravity"],
ignoreForCollisions: data["ignoreForCollisions"]
ignoreForCollisions: data["ignoreForCollisions"],
collisionsWillMove: data["collisionsWillMove"]
});
data = null;
}

View file

@ -119,6 +119,8 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) {
worldTrans.setRotation(glmToBullet(_rotationalTarget));
rigidBody->setWorldTransform(worldTrans);
motionState->dirtyInternalKinematicChanges();
_previousPositionalTarget = _positionalTarget;
_previousRotationalTarget = _rotationalTarget;
_previousSet = true;
@ -224,6 +226,8 @@ QVariantMap AvatarActionHold::getArguments() {
arguments["relativeRotation"] = glmToQMap(_relativeRotation);
arguments["timeScale"] = _linearTimeScale;
arguments["hand"] = _hand;
arguments["kinematic"] = _kinematic;
arguments["kinematicSetVelocity"] = _kinematicSetVelocity;
});
return arguments;
}

View file

@ -146,7 +146,7 @@ MotionType EntityMotionState::computeObjectMotionType() const {
if (_entity->getCollisionsWillMove()) {
return MOTION_TYPE_DYNAMIC;
}
return _entity->isMoving() ? MOTION_TYPE_KINEMATIC : MOTION_TYPE_STATIC;
return (_entity->isMoving() || _entity->hasActions()) ? MOTION_TYPE_KINEMATIC : MOTION_TYPE_STATIC;
}
bool EntityMotionState::isMoving() const {
@ -184,6 +184,7 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) {
if (!_entity) {
return;
}
assert(entityTreeIsLocked());
measureBodyAcceleration();
_entity->setPosition(bulletToGLM(worldTrans.getOrigin()) + ObjectMotionState::getWorldOffset());

View file

@ -16,7 +16,6 @@
ObjectAction::ObjectAction(EntityActionType type, const QUuid& id, EntityItemPointer ownerEntity) :
btActionInterface(),
EntityActionInterface(type, id),
_active(false),
_ownerEntity(ownerEntity) {
}
@ -35,7 +34,9 @@ void ObjectAction::updateAction(btCollisionWorld* collisionWorld, btScalar delta
if (ownerEntityExpired) {
qDebug() << "warning -- action with no entity removing self from btCollisionWorld.";
btDynamicsWorld* dynamicsWorld = static_cast<btDynamicsWorld*>(collisionWorld);
dynamicsWorld->removeAction(this);
if (dynamicsWorld) {
dynamicsWorld->removeAction(this);
}
return;
}
@ -120,6 +121,17 @@ QVariantMap ObjectAction::getArguments() {
arguments["ttl"] = (float)(_expires - now) / (float)USECS_PER_SECOND;
}
arguments["tag"] = _tag;
EntityItemPointer entity = _ownerEntity.lock();
if (entity) {
ObjectMotionState* motionState = static_cast<ObjectMotionState*>(entity->getPhysicsInfo());
if (motionState) {
arguments["::active"] = motionState->isActive();
arguments["::motion-type"] = motionTypeToString(motionState->getMotionType());
} else {
arguments["::no-motion-state"] = true;
}
}
});
return arguments;
}

View file

@ -50,6 +50,8 @@ public:
virtual quint64 getExpires() { return _expires; }
protected:
quint64 localTimeToServerTime(quint64 timeValue) const;
quint64 serverTimeToLocalTime(quint64 timeValue) const;
virtual btRigidBody* getRigidBody();
virtual glm::vec3 getPosition();
@ -62,14 +64,10 @@ protected:
virtual void setAngularVelocity(glm::vec3 angularVelocity);
virtual void activateBody();
bool _active;
EntityItemWeakPointer _ownerEntity;
quint64 _expires; // in seconds since epoch
QString _tag;
quint64 localTimeToServerTime(quint64 timeValue) const;
quint64 serverTimeToLocalTime(quint64 timeValue) const;
quint64 _expires { 0 }; // in seconds since epoch
bool _active { false };
private:
int getEntityServerClockSkew() const;

View file

@ -20,17 +20,17 @@
// origin of physics simulation in world-frame
glm::vec3 _worldOffset(0.0f);
// static
// static
void ObjectMotionState::setWorldOffset(const glm::vec3& offset) {
_worldOffset = offset;
}
// static
// static
const glm::vec3& ObjectMotionState::getWorldOffset() {
return _worldOffset;
}
// static
// static
uint32_t worldSimulationStep = 0;
void ObjectMotionState::setWorldSimulationStep(uint32_t step) {
assert(step > worldSimulationStep);
@ -41,7 +41,7 @@ uint32_t ObjectMotionState::getWorldSimulationStep() {
return worldSimulationStep;
}
// static
// static
ShapeManager* shapeManager = nullptr;
void ObjectMotionState::setShapeManager(ShapeManager* manager) {
assert(manager);
@ -85,7 +85,7 @@ glm::vec3 ObjectMotionState::getBodyLinearVelocity() const {
glm::vec3 ObjectMotionState::getBodyLinearVelocityGTSigma() const {
// NOTE: the threshold to use here relates to the linear displacement threshold (dX) for sending updates
// to objects that are tracked server-side (e.g. entities which use dX = 2mm). Hence an object moving
// to objects that are tracked server-side (e.g. entities which use dX = 2mm). Hence an object moving
// just under this velocity threshold would trigger an update about V/dX times per second.
const float MIN_LINEAR_SPEED_SQUARED = 0.0036f; // 6 mm/sec

View file

@ -29,18 +29,27 @@ enum MotionType {
MOTION_TYPE_KINEMATIC // keyframed motion
};
inline QString motionTypeToString(MotionType motionType) {
switch(motionType) {
case MOTION_TYPE_STATIC: return QString("static");
case MOTION_TYPE_DYNAMIC: return QString("dynamic");
case MOTION_TYPE_KINEMATIC: return QString("kinematic");
}
return QString("unknown");
}
enum MotionStateType {
MOTIONSTATE_TYPE_INVALID,
MOTIONSTATE_TYPE_ENTITY,
MOTIONSTATE_TYPE_AVATAR
};
// The update flags trigger two varieties of updates: "hard" which require the body to be pulled
// The update flags trigger two varieties of updates: "hard" which require the body to be pulled
// and re-added to the physics engine and "easy" which just updates the body properties.
const uint32_t HARD_DIRTY_PHYSICS_FLAGS = (uint32_t)(Simulation::DIRTY_MOTION_TYPE | Simulation::DIRTY_SHAPE |
const uint32_t HARD_DIRTY_PHYSICS_FLAGS = (uint32_t)(Simulation::DIRTY_MOTION_TYPE | Simulation::DIRTY_SHAPE |
Simulation::DIRTY_COLLISION_GROUP);
const uint32_t EASY_DIRTY_PHYSICS_FLAGS = (uint32_t)(Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES |
Simulation::DIRTY_MASS | Simulation::DIRTY_MATERIAL |
Simulation::DIRTY_MASS | Simulation::DIRTY_MATERIAL |
Simulation::DIRTY_SIMULATOR_ID | Simulation::DIRTY_SIMULATOR_OWNERSHIP);
// These are the set of incoming flags that the PhysicsEngine needs to hear about:
@ -57,7 +66,7 @@ class PhysicsEngine;
class ObjectMotionState : public btMotionState {
public:
// These poroperties of the PhysicsEngine are "global" within the context of all ObjectMotionStates
// (assuming just one PhysicsEngine). They are cached as statics for fast calculations in the
// (assuming just one PhysicsEngine). They are cached as statics for fast calculations in the
// ObjectMotionState context.
static void setWorldOffset(const glm::vec3& offset);
static const glm::vec3& getWorldOffset();
@ -112,7 +121,7 @@ public:
virtual float getObjectFriction() const = 0;
virtual float getObjectLinearDamping() const = 0;
virtual float getObjectAngularDamping() const = 0;
virtual glm::vec3 getObjectPosition() const = 0;
virtual glm::quat getObjectRotation() const = 0;
virtual glm::vec3 getObjectLinearVelocity() const = 0;
@ -131,6 +140,11 @@ public:
bool isActive() const { return _body ? _body->isActive() : false; }
bool hasInternalKinematicChanges() const { return _hasInternalKinematicChanges; }
void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; }
void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; }
friend class PhysicsEngine;
protected:
@ -151,6 +165,7 @@ protected:
float _mass;
uint32_t _lastKinematicStep;
bool _hasInternalKinematicChanges { false };
};
typedef QSet<ObjectMotionState*> SetOfMotionStates;

View file

@ -82,12 +82,12 @@ void PhysicsEngine::addObject(ObjectMotionState* motionState) {
btCollisionShape* shape = motionState->getShape();
assert(shape);
body = new btRigidBody(mass, motionState, shape, inertia);
motionState->setRigidBody(body);
} else {
body->setMassProps(mass, inertia);
}
body->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT);
body->updateInertiaTensor();
motionState->setRigidBody(body);
motionState->updateBodyVelocities();
const float KINEMATIC_LINEAR_VELOCITY_THRESHOLD = 0.01f; // 1 cm/sec
const float KINEMATIC_ANGULAR_VELOCITY_THRESHOLD = 0.01f; // ~1 deg/sec
@ -101,12 +101,15 @@ void PhysicsEngine::addObject(ObjectMotionState* motionState) {
shape->calculateLocalInertia(mass, inertia);
if (!body) {
body = new btRigidBody(mass, motionState, shape, inertia);
motionState->setRigidBody(body);
} else {
body->setMassProps(mass, inertia);
}
body->setCollisionFlags(body->getCollisionFlags() & ~(btCollisionObject::CF_KINEMATIC_OBJECT |
btCollisionObject::CF_STATIC_OBJECT));
body->updateInertiaTensor();
motionState->setRigidBody(body);
motionState->updateBodyVelocities();
// NOTE: Bullet will deactivate any object whose velocity is below these thresholds for longer than 2 seconds.
// (the 2 seconds is determined by: static btRigidBody::gDeactivationTime
const float DYNAMIC_LINEAR_VELOCITY_THRESHOLD = 0.05f; // 5 cm/sec
@ -123,12 +126,12 @@ void PhysicsEngine::addObject(ObjectMotionState* motionState) {
if (!body) {
assert(motionState->getShape());
body = new btRigidBody(mass, motionState, motionState->getShape(), inertia);
motionState->setRigidBody(body);
} else {
body->setMassProps(mass, inertia);
}
body->setCollisionFlags(btCollisionObject::CF_STATIC_OBJECT);
body->updateInertiaTensor();
motionState->setRigidBody(body);
break;
}
}

View file

@ -4,8 +4,8 @@
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from the use of this software.
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it freely,
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it freely,
* subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
@ -75,7 +75,7 @@ int ThreadSafeDynamicsWorld::stepSimulationWithSubstepCallback(btScalar timeStep
}
}
// NOTE: We do NOT call synchronizeMotionState() after each substep (to avoid multiple locks on the
// NOTE: We do NOT call synchronizeMotionStates() after each substep (to avoid multiple locks on the
// object data outside of the physics engine). A consequence of this is that the transforms of the
// external objects only ever update at the end of the full step.
@ -87,6 +87,33 @@ int ThreadSafeDynamicsWorld::stepSimulationWithSubstepCallback(btScalar timeStep
return subSteps;
}
// call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState()
void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) {
btAssert(body);
if (body->getMotionState() && !body->isStaticObject()) {
//we need to call the update at least once, even for sleeping objects
//otherwise the 'graphics' transform never updates properly
///@todo: add 'dirty' flag
//if (body->getActivationState() != ISLAND_SLEEPING)
{
if (body->isKinematicObject()) {
ObjectMotionState* objectMotionState = static_cast<ObjectMotionState*>(body->getMotionState());
if (objectMotionState->hasInternalKinematicChanges()) {
objectMotionState->clearInternalKinematicChanges();
body->getMotionState()->setWorldTransform(body->getWorldTransform());
}
return;
}
btTransform interpolatedTransform;
btTransformUtil::integrateTransform(body->getInterpolationWorldTransform(),
body->getInterpolationLinearVelocity(),body->getInterpolationAngularVelocity(),
(m_latencyMotionStateInterpolation && m_fixedTimeStep) ? m_localTime - m_fixedTimeStep : m_localTime*body->getHitFraction(),
interpolatedTransform);
body->getMotionState()->setWorldTransform(interpolatedTransform);
}
}
}
void ThreadSafeDynamicsWorld::synchronizeMotionStates() {
_changedMotionStates.clear();
BT_PROFILE("synchronizeMotionStates");
@ -97,22 +124,22 @@ void ThreadSafeDynamicsWorld::synchronizeMotionStates() {
btRigidBody* body = btRigidBody::upcast(colObj);
if (body) {
if (body->getMotionState()) {
synchronizeSingleMotionState(body);
synchronizeMotionState(body);
_changedMotionStates.push_back(static_cast<ObjectMotionState*>(body->getMotionState()));
}
}
}
} else {
} else {
//iterate over all active rigid bodies
for (int i=0;i<m_nonStaticRigidBodies.size();i++) {
btRigidBody* body = m_nonStaticRigidBodies[i];
if (body->isActive()) {
if (body->getMotionState()) {
synchronizeSingleMotionState(body);
synchronizeMotionState(body);
_changedMotionStates.push_back(static_cast<ObjectMotionState*>(body->getMotionState()));
}
}
}
}
}
}
}

View file

@ -4,8 +4,8 @@
*
* This software is provided 'as-is', without any express or implied warranty.
* In no event will the authors be held liable for any damages arising from the use of this software.
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it freely,
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it freely,
* subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
@ -43,13 +43,16 @@ public:
void synchronizeMotionStates();
// btDiscreteDynamicsWorld::m_localTime is the portion of real-time that has not yet been simulated
// but is used for MotionState::setWorldTransform() extrapolation (a feature that Bullet uses to provide
// but is used for MotionState::setWorldTransform() extrapolation (a feature that Bullet uses to provide
// smoother rendering of objects when the physics simulation loop is ansynchronous to the render loop).
float getLocalTimeAccumulation() const { return m_localTime; }
VectorOfMotionStates& getChangedMotionStates() { return _changedMotionStates; }
private:
// call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState()
void synchronizeMotionState(btRigidBody* body);
VectorOfMotionStates _changedMotionStates;
};