diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index b3231b906d..0f66f3bb41 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -96,10 +96,8 @@ const float CENTIMETERS_PER_METER = 100.0f;
const QString AVATAR_SETTINGS_GROUP_NAME { "Avatar" };
-static const QString USER_RECENTER_MODEL_FORCE_SIT = QStringLiteral("ForceSit");
-static const QString USER_RECENTER_MODEL_FORCE_STAND = QStringLiteral("ForceStand");
-static const QString USER_RECENTER_MODEL_AUTO = QStringLiteral("Auto");
-static const QString USER_RECENTER_MODEL_DISABLE_HMD_LEAN = QStringLiteral("DisableHMDLean");
+static const QString ALLOW_AVATAR_STANDING_ALWAYS = QStringLiteral("Always");
+static const QString ALLOW_AVATAR_STANDING_WHEN_USER_IS_STANDING = QStringLiteral("UserStanding");
const QString HEAD_BLEND_DIRECTIONAL_ALPHA_NAME = "lookAroundAlpha";
const QString HEAD_BLEND_LINEAR_ALPHA_NAME = "lookBlendAlpha";
@@ -111,30 +109,38 @@ const QString POINT_BLEND_LINEAR_ALPHA_NAME = "pointBlendAlpha";
const QString POINT_REF_JOINT_NAME = "RightShoulder";
const float POINT_ALPHA_BLENDING = 1.0f;
-MyAvatar::SitStandModelType stringToUserRecenterModel(const QString& str) {
- if (str == USER_RECENTER_MODEL_FORCE_SIT) {
- return MyAvatar::ForceSit;
- } else if (str == USER_RECENTER_MODEL_FORCE_STAND) {
- return MyAvatar::ForceStand;
- } else if (str == USER_RECENTER_MODEL_DISABLE_HMD_LEAN) {
- return MyAvatar::DisableHMDLean;
- } else {
- return MyAvatar::Auto;
+const std::array(MyAvatar::AllowAvatarStandingPreference::Count)>
+ MyAvatar::allowAvatarStandingPreferenceStrings = {
+ QStringLiteral("WhenUserIsStanding"),
+ QStringLiteral("Always")
+};
+
+const std::array(MyAvatar::AllowAvatarLeaningPreference::Count)>
+ MyAvatar::allowAvatarLeaningPreferenceStrings = {
+ QStringLiteral("WhenUserIsStanding"),
+ QStringLiteral("Always"),
+ QStringLiteral("Never"),
+ QStringLiteral("AlwaysNoRecenter")
+};
+
+MyAvatar::AllowAvatarStandingPreference stringToAllowAvatarStandingPreference(const QString& str) {
+ for (uint stringIndex = 0; stringIndex < static_cast(MyAvatar::AllowAvatarStandingPreference::Count); stringIndex++) {
+ if (MyAvatar::allowAvatarStandingPreferenceStrings[stringIndex] == str) {
+ return static_cast(stringIndex);
+ }
}
+
+ return MyAvatar::AllowAvatarStandingPreference::Default;
}
-QString userRecenterModelToString(MyAvatar::SitStandModelType model) {
- switch (model) {
- case MyAvatar::ForceSit:
- return USER_RECENTER_MODEL_FORCE_SIT;
- case MyAvatar::ForceStand:
- return USER_RECENTER_MODEL_FORCE_STAND;
- case MyAvatar::DisableHMDLean:
- return USER_RECENTER_MODEL_DISABLE_HMD_LEAN;
- case MyAvatar::Auto:
- default:
- return USER_RECENTER_MODEL_AUTO;
+MyAvatar::AllowAvatarLeaningPreference stringToAllowAvatarLeaningPreference(const QString& str) {
+ for (uint stringIndex = 0; stringIndex < static_cast(MyAvatar::AllowAvatarLeaningPreference::Count); stringIndex++) {
+ if (MyAvatar::allowAvatarLeaningPreferenceStrings[stringIndex] == str) {
+ return static_cast(stringIndex);
+ }
}
+
+ return MyAvatar::AllowAvatarLeaningPreference::Default;
}
static const QStringList TRIGGER_REACTION_NAMES = {
@@ -166,7 +172,7 @@ MyAvatar::MyAvatar(QThread* thread) :
_scriptedMotorFrame(SCRIPTED_MOTOR_CAMERA_FRAME),
_scriptedMotorMode(SCRIPTED_MOTOR_SIMPLE_MODE),
_motionBehaviors(AVATAR_MOTION_DEFAULTS),
- _characterController(std::shared_ptr(this)),
+ _characterController(std::shared_ptr(this), _follow._timeRemaining),
_eyeContactTarget(LEFT_EYE),
_realWorldFieldOfView("realWorldFieldOfView",
DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES),
@@ -214,8 +220,12 @@ MyAvatar::MyAvatar(QThread* thread) :
_analogWalkSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "analogWalkSpeed", _analogWalkSpeed.get()),
_analogPlusWalkSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "analogPlusWalkSpeed", _analogPlusWalkSpeed.get()),
_controlSchemeIndexSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "controlSchemeIndex", _controlSchemeIndex),
- _userRecenterModelSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "userRecenterModel", USER_RECENTER_MODEL_AUTO)
-{
+ _allowAvatarStandingPreferenceSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "allowAvatarStandingPreference",
+ allowAvatarStandingPreferenceStrings[static_cast(
+ AllowAvatarStandingPreference::Default)]),
+ _allowAvatarLeaningPreferenceSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "allowAvatarLeaningPreference",
+ allowAvatarLeaningPreferenceStrings[static_cast(
+ AllowAvatarLeaningPreference::Default)]) {
_clientTraitsHandler.reset(new ClientTraitsHandler(this));
// give the pointer to our head to inherited _headData variable from AvatarData
@@ -493,8 +503,15 @@ void MyAvatar::centerBody() {
return;
}
+ centerBodyInternal(false);
+}
+
+// forceFollowYPos (default false): true to force the body matrix to be affected by the HMD's
+// vertical position, even if crouch recentering is disabled.
+void MyAvatar::centerBodyInternal(const bool forceFollowYPos) {
// derive the desired body orientation from the current hmd orientation, before the sensor reset.
- auto newBodySensorMatrix = deriveBodyFromHMDSensor(); // Based on current cached HMD position/rotation..
+ auto newBodySensorMatrix =
+ deriveBodyFromHMDSensor(forceFollowYPos); // Based on current cached HMD position/rotation..
// transform this body into world space
auto worldBodyMatrix = _sensorToWorldMatrix * newBodySensorMatrix;
@@ -571,64 +588,63 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) {
}
}
+// Determine if the user is sitting or standing in the real world.
void MyAvatar::updateSitStandState(float newHeightReading, float dt) {
const float STANDING_HEIGHT_MULTIPLE = 1.2f;
const float SITTING_HEIGHT_MULTIPLE = 0.833f;
const float SITTING_TIMEOUT = 4.0f; // 4 seconds
const float STANDING_TIMEOUT = 0.3333f; // 1/3 second
const float SITTING_UPPER_BOUND = 1.52f;
- if (!getIsSitStandStateLocked()) {
- if (!getIsAway() && getControllerPoseInAvatarFrame(controller::Action::HEAD).isValid()) {
- if (getIsInSittingState()) {
- if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) {
- // if we recenter upwards then no longer in sitting state
- _sitStandStateTimer += dt;
- if (_sitStandStateTimer > STANDING_TIMEOUT) {
- _averageUserHeightSensorSpace = newHeightReading;
- _tippingPoint = newHeightReading;
- setIsInSittingState(false);
- }
- } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) {
- // if we are mis labelled as sitting but we are standing in the real world this will
- // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state
- _sitStandStateTimer += dt;
- if (_sitStandStateTimer > SITTING_TIMEOUT) {
- _averageUserHeightSensorSpace = newHeightReading;
- _tippingPoint = newHeightReading;
- // here we stay in sit state but reset the average height
- setIsInSittingState(true);
- }
- } else {
- // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please)
- if (_averageUserHeightSensorSpace > SITTING_UPPER_BOUND) {
- setIsInSittingState(false);
- } else {
- // tipping point is average height when sitting.
- _tippingPoint = _averageUserHeightSensorSpace;
- _sitStandStateTimer = 0.0f;
- }
+ if (!getIsAway() && _isBodyPartTracked._head) {
+ if (getIsInSittingState()) {
+ if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) {
+ // if we recenter upwards then no longer in sitting state
+ _sitStandStateTimer += dt;
+ if (_sitStandStateTimer > STANDING_TIMEOUT) {
+ _averageUserHeightSensorSpace = newHeightReading;
+ _tippingPoint = newHeightReading;
+ setIsInSittingState(false);
+ }
+ } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) {
+ // if we are mis labelled as sitting but we are standing in the real world this will
+ // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state
+ _sitStandStateTimer += dt;
+ if (_sitStandStateTimer > SITTING_TIMEOUT) {
+ _averageUserHeightSensorSpace = newHeightReading;
+ _tippingPoint = newHeightReading;
+ // here we stay in sit state but reset the average height
+ setIsInSittingState(true);
}
} else {
- // in the standing state
- if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) {
- _sitStandStateTimer += dt;
- if (_sitStandStateTimer > SITTING_TIMEOUT) {
- _averageUserHeightSensorSpace = newHeightReading;
- _tippingPoint = newHeightReading;
- setIsInSittingState(true);
- }
+ // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please)
+ if (_averageUserHeightSensorSpace > SITTING_UPPER_BOUND) {
+ setIsInSittingState(false);
} else {
- // use the mode height for the tipping point when we are standing.
- _tippingPoint = getCurrentStandingHeight();
+ // tipping point is average height when sitting.
+ _tippingPoint = _averageUserHeightSensorSpace;
_sitStandStateTimer = 0.0f;
}
}
} else {
- //if you are away then reset the average and set state to standing.
- _averageUserHeightSensorSpace = _userHeight.get();
- _tippingPoint = _userHeight.get();
- setIsInSittingState(false);
+ // in the standing state
+ if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) {
+ _sitStandStateTimer += dt;
+ if (_sitStandStateTimer > SITTING_TIMEOUT) {
+ _averageUserHeightSensorSpace = newHeightReading;
+ _tippingPoint = newHeightReading;
+ setIsInSittingState(true);
+ }
+ } else {
+ // use the mode height for the tipping point when we are standing.
+ _tippingPoint = getCurrentStandingHeight();
+ _sitStandStateTimer = 0.0f;
+ }
}
+ } else {
+ //if you are away then reset the average and set state to standing.
+ _averageUserHeightSensorSpace = _userHeight.get();
+ _tippingPoint = _userHeight.get();
+ setIsInSittingState(false);
}
}
@@ -636,17 +652,37 @@ void MyAvatar::update(float deltaTime) {
// update moving average of HMD facing in xz plane.
const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength();
const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders
- const float COSINE_THIRTY_DEGREES = 0.866f;
- const float SQUATTY_TIMEOUT = 30.0f; // 30 seconds
const float HEIGHT_FILTER_COEFFICIENT = 0.01f;
float tau = deltaTime / HMD_FACING_TIMESCALE;
setHipToHandController(computeHandAzimuth());
+ // Determine which body parts are under direct control (tracked).
+ {
+ _isBodyPartTracked._leftHand = getControllerPoseInSensorFrame(controller::Action::LEFT_HAND).isValid();
+ _isBodyPartTracked._rightHand = getControllerPoseInSensorFrame(controller::Action::RIGHT_HAND).isValid();
+ _isBodyPartTracked._head = getControllerPoseInSensorFrame(controller::Action::HEAD).isValid();
+
+ // Check for either foot so that if one foot loses tracking, we don't break out of foot-tracking behaviour
+ // (in terms of avatar recentering for example).
+ _isBodyPartTracked._feet = _isBodyPartTracked._head && // Feet can't be tracked unless head is tracked.
+ (getControllerPoseInSensorFrame(controller::Action::LEFT_FOOT).isValid() ||
+ getControllerPoseInSensorFrame(controller::Action::RIGHT_FOOT).isValid());
+
+ _isBodyPartTracked._hips = _isBodyPartTracked._feet && // Hips can't be tracked unless feet are tracked.
+ getControllerPoseInSensorFrame(controller::Action::HIPS).isValid();
+ }
+
+ // Recenter the body when foot tracking starts or ends.
+ if (_isBodyPartTracked._feet != _isBodyPartTracked._feetPreviousUpdate) {
+ centerBodyInternal(false);
+ _isBodyPartTracked._feetPreviousUpdate = _isBodyPartTracked._feet;
+ }
+
// put the average hand azimuth into sensor space.
// then mix it with head facing direction to determine rotation recenter
int spine2Index = _skeletonModel->getRig().indexOfJoint("Spine2");
- if (getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).isValid() && !(spine2Index < 0)) {
+ if (_isBodyPartTracked._leftHand && _isBodyPartTracked._rightHand && !(spine2Index < 0)) {
// use the spine for the azimuth origin.
glm::quat spine2Rot = getAbsoluteJointRotationInObjectFrame(spine2Index);
@@ -682,29 +718,6 @@ void MyAvatar::update(float deltaTime) {
setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD)));
}
- // if the spine is straight and the head is below the default position by 5 cm then increment squatty count.
- const float SQUAT_THRESHOLD = 0.05f;
- glm::vec3 headDefaultPositionAvatarSpace = getAbsoluteDefaultJointTranslationInObjectFrame(getJointIndex("Head"));
- glm::quat spine2OrientationAvatarSpace = getAbsoluteJointRotationInObjectFrame(getJointIndex("Spine2"));
- glm::vec3 upSpine2 = spine2OrientationAvatarSpace * glm::vec3(0.0f, 1.0f, 0.0f);
- if (glm::length(upSpine2) > 0.0f) {
- upSpine2 = glm::normalize(upSpine2);
- }
- float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f));
-
- if (getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) &&
- (angleSpine2 > COSINE_THIRTY_DEGREES) &&
- (getUserRecenterModel() != MyAvatar::SitStandModelType::ForceStand)) {
-
- _squatTimer += deltaTime;
- if (_squatTimer > SQUATTY_TIMEOUT) {
- _squatTimer = 0.0f;
- _follow._squatDetected = true;
- }
- } else {
- _squatTimer = 0.0f;
- }
-
// put update sit stand state counts here
updateSitStandState(newHeightReading.getTranslation().y, deltaTime);
@@ -832,7 +845,7 @@ void MyAvatar::recalculateChildCauterization() const {
_cauterizationNeedsUpdate = true;
}
-bool MyAvatar::isFollowActive(FollowHelper::FollowType followType) const {
+bool MyAvatar::isFollowActive(CharacterController::FollowType followType) const {
return _follow.isActive(followType);
}
@@ -1277,6 +1290,10 @@ void MyAvatar::resizeAvatarEntitySettingHandles(uint32_t maxIndex) {
void MyAvatar::saveData() {
_dominantHandSetting.set(getDominantHand());
+ _allowAvatarStandingPreferenceSetting.set(
+ allowAvatarStandingPreferenceStrings[static_cast(getAllowAvatarStandingPreference())]);
+ _allowAvatarLeaningPreferenceSetting.set(
+ allowAvatarLeaningPreferenceStrings[static_cast(getAllowAvatarLeaningPreference())]);
_strafeEnabledSetting.set(getStrafeEnabled());
_hmdAvatarAlignmentTypeSetting.set(getHmdAvatarAlignmentType());
_headPitchSetting.set(getHead()->getBasePitch());
@@ -1311,7 +1328,10 @@ void MyAvatar::saveData() {
_analogWalkSpeedSetting.set(getAnalogWalkSpeed());
_analogPlusWalkSpeedSetting.set(getAnalogPlusWalkSpeed());
_controlSchemeIndexSetting.set(getControlSchemeIndex());
- _userRecenterModelSetting.set(userRecenterModelToString(getUserRecenterModel()));
+ _allowAvatarStandingPreferenceSetting.set(
+ allowAvatarStandingPreferenceStrings[static_cast(getAllowAvatarStandingPreference())]);
+ _allowAvatarLeaningPreferenceSetting.set(
+ allowAvatarLeaningPreferenceStrings[static_cast(getAllowAvatarLeaningPreference())]);
auto hmdInterface = DependencyManager::get();
saveAvatarEntityDataToSettings();
@@ -2004,7 +2024,10 @@ void MyAvatar::loadData() {
setUserHeight(_userHeightSetting.get(DEFAULT_AVATAR_HEIGHT));
setTargetScale(_scaleSetting.get());
- setUserRecenterModel(stringToUserRecenterModel(_userRecenterModelSetting.get(USER_RECENTER_MODEL_AUTO)));
+ setAllowAvatarStandingPreference(stringToAllowAvatarStandingPreference(_allowAvatarStandingPreferenceSetting.get(
+ allowAvatarStandingPreferenceStrings[static_cast(AllowAvatarStandingPreference::Default)])));
+ setAllowAvatarLeaningPreference(stringToAllowAvatarLeaningPreference(_allowAvatarLeaningPreferenceSetting.get(
+ allowAvatarLeaningPreferenceStrings[static_cast(AllowAvatarLeaningPreference::Default)])));
setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible));
_follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing));
@@ -2666,15 +2689,8 @@ controller::Pose MyAvatar::getControllerPoseInAvatarFrame(controller::Action act
}
}
-glm::quat MyAvatar::getOffHandRotation() const {
- auto hand = (getDominantHand() == DOMINANT_RIGHT_HAND) ? controller::Action::LEFT_HAND : controller::Action::RIGHT_HAND;
- auto pose = getControllerPoseInAvatarFrame(hand);
- return pose.rotation;
-}
-
void MyAvatar::updateMotors() {
_characterController.clearMotors();
- glm::quat motorRotation;
const float FLYING_MOTOR_TIMESCALE = 0.05f;
const float WALKING_MOTOR_TIMESCALE = 0.2f;
@@ -2693,35 +2709,17 @@ void MyAvatar::updateMotors() {
}
if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
- if (_characterController.getState() == CharacterController::State::Hover ||
- _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) {
- CameraMode mode = qApp->getCamera().getMode();
- if (!qApp->isHMDMode() && (mode == CAMERA_MODE_FIRST_PERSON_LOOK_AT || mode == CAMERA_MODE_LOOK_AT || mode == CAMERA_MODE_SELFIE)) {
- motorRotation = getLookAtRotation();
- } else {
- motorRotation = getMyHead()->getHeadOrientation();
- }
- } else {
- // non-hovering = walking: follow camera twist about vertical but not lift
- // we decompose camera's rotation and store the twist part in motorRotation
- // however, we need to perform the decomposition in the avatar-frame
- // using the local UP axis and then transform back into world-frame
- glm::quat orientation = getWorldOrientation();
- glm::quat headOrientation = glm::inverse(orientation) * getMyHead()->getHeadOrientation(); // avatar-frame
- glm::quat liftRotation;
- swingTwistDecomposition(headOrientation, Vectors::UNIT_Y, liftRotation, motorRotation);
- motorRotation = orientation * motorRotation;
- }
-
if (_isPushing || _isBraking || !_isBeingPushed) {
- _characterController.addMotor(_actionMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale);
+ _characterController.addMotor(_actionMotorVelocity, Quaternions::IDENTITY, horizontalMotorTimescale,
+ verticalMotorTimescale);
} else {
// _isBeingPushed must be true --> disable action motor by giving it a long timescale,
// otherwise it's attempt to "stand in in place" could defeat scripted motor/thrusts
- _characterController.addMotor(_actionMotorVelocity, motorRotation, INVALID_MOTOR_TIMESCALE);
+ _characterController.addMotor(_actionMotorVelocity, Quaternions::IDENTITY, INVALID_MOTOR_TIMESCALE);
}
}
if (_motionBehaviors & AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED) {
+ glm::quat motorRotation;
if (_scriptedMotorFrame == SCRIPTED_MOTOR_CAMERA_FRAME) {
motorRotation = getMyHead()->getHeadOrientation() * glm::angleAxis(PI, Vectors::UNIT_Y);
} else if (_scriptedMotorFrame == SCRIPTED_MOTOR_AVATAR_FRAME) {
@@ -2759,8 +2757,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
_characterController.setScaleFactor(getSensorToWorldScale());
_characterController.setPositionAndOrientation(getWorldPosition(), getWorldOrientation());
- auto headPose = getControllerPoseInAvatarFrame(controller::Action::HEAD);
- if (headPose.isValid()) {
+ if (_isBodyPartTracked._head) {
_follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput());
} else {
_follow.deactivate();
@@ -3757,15 +3754,15 @@ glm::vec3 MyAvatar::scaleMotorSpeed(const glm::vec3 forward, const glm::vec3 rig
if (length > EPSILON) {
direction /= length;
}
- return getSensorToWorldScale() * direction * getSprintSpeed() * _walkSpeedScalar;
+ return direction * getSprintSpeed() * _walkSpeedScalar;
} else {
return Vectors::ZERO;
}
case LocomotionControlsMode::CONTROLS_ANALOG:
case LocomotionControlsMode::CONTROLS_ANALOG_PLUS:
if (zSpeed || xSpeed) {
- glm::vec3 scaledForward = getSensorToWorldScale() * calculateGearedSpeed(zSpeed) * _walkSpeedScalar * ((zSpeed >= stickFullOn) ? getSprintSpeed() : getWalkSpeed()) * forward;
- glm::vec3 scaledRight = getSensorToWorldScale() * calculateGearedSpeed(xSpeed) * _walkSpeedScalar * ((xSpeed > stickFullOn) ? getSprintSpeed() : getWalkSpeed()) * right;
+ glm::vec3 scaledForward = calculateGearedSpeed(zSpeed) * _walkSpeedScalar * ((zSpeed >= stickFullOn) ? getSprintSpeed() : getWalkSpeed()) * forward;
+ glm::vec3 scaledRight = calculateGearedSpeed(xSpeed) * _walkSpeedScalar * ((xSpeed > stickFullOn) ? getSprintSpeed() : getWalkSpeed()) * right;
direction = scaledForward + scaledRight;
return direction;
} else {
@@ -3793,54 +3790,114 @@ glm::vec3 MyAvatar::scaleMotorSpeed(const glm::vec3 forward, const glm::vec3 rig
}
}
-glm::vec3 MyAvatar::calculateScaledDirection(){
+// Calculate the world-space motor velocity for the avatar.
+glm::vec3 MyAvatar::calculateScaledDirection() {
CharacterController::State state = _characterController.getState();
// compute action input
// Determine if we're head or controller relative...
glm::vec3 forward, right;
- if (qApp->isHMDMode()) {
- auto handRotation = getOffHandRotation();
- glm::vec3 controllerForward(0.0f, 1.0f, 0.0f);
- glm::vec3 controllerRight(0.0f, 0.0f, (getDominantHand() == DOMINANT_RIGHT_HAND ? 1.0f : -1.0f));
- glm::vec3 transform;
- switch (getMovementReference()) {
- case LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE:
- forward = (handRotation * controllerForward);
- right = (handRotation * controllerRight);
- break;
- case LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE_LEVELED:
- forward = (handRotation * controllerForward);
- transform = forward - (glm::dot(forward, Vectors::UNIT_Y) * Vectors::UNIT_Y);
- if (glm::length(transform) > EPSILON) {
- forward = glm::normalize(transform);
- } else {
- forward = Vectors::ZERO;
- }
- right = (handRotation * controllerRight);
- transform = right - (glm::dot(right, Vectors::UNIT_Y) * Vectors::UNIT_Y);
- if (glm::length(transform) > EPSILON) {
- right = glm::normalize(transform);
- } else {
- right = Vectors::ZERO;
- }
- break;
- case LocomotionRelativeMovementMode::MOVEMENT_HMD_RELATIVE:
- default:
- forward = IDENTITY_FORWARD;
- right = IDENTITY_RIGHT;
+ int movementReference = getMovementReference();
+ CameraMode cameraMode = qApp->getCamera().getMode();
+
+ bool vectorsAreInAvatarFrame = true;
+ bool removeLocalYComponent = false;
+
+ bool HMDHandRelativeMovement =
+ qApp->isHMDMode() && (movementReference == LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE ||
+ movementReference == LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE_LEVELED);
+
+ bool desktopLookatOrSelfieMode =
+ !qApp->isHMDMode() && (cameraMode == CAMERA_MODE_FIRST_PERSON_LOOK_AT || cameraMode == CAMERA_MODE_LOOK_AT ||
+ cameraMode == CAMERA_MODE_SELFIE);
+
+ bool hoveringOrCollisionless = _characterController.getState() == CharacterController::State::Hover ||
+ _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS;
+
+ if (HMDHandRelativeMovement) {
+ controller::Action directionHand =
+ (getDominantHand() == DOMINANT_RIGHT_HAND) ? controller::Action::LEFT_HAND : controller::Action::RIGHT_HAND;
+ controller::Pose handPoseInAvatarFrame = getControllerPoseInAvatarFrame(directionHand);
+
+ if (handPoseInAvatarFrame.isValid()) {
+ glm::vec3 controllerForward(0.0f, 1.0f, 0.0f);
+ glm::vec3 controllerRight(0.0f, 0.0f, (directionHand == controller::Action::LEFT_HAND) ? 1.0f : -1.0f);
+
+ forward = (handPoseInAvatarFrame.rotation * controllerForward);
+ right = (handPoseInAvatarFrame.rotation * controllerRight);
+
+ removeLocalYComponent = (movementReference == LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE_LEVELED);
+ }
+ } else { // MOVEMENT_HMD_RELATIVE or desktop mode
+ if (qApp->isHMDMode()) {
+ forward = -IDENTITY_FORWARD;
+ right = -IDENTITY_RIGHT;
+ } else {
+ forward = IDENTITY_FORWARD;
+ right = IDENTITY_RIGHT;
+ }
+
+ glm::quat rotation = Quaternions::IDENTITY;
+
+ if (hoveringOrCollisionless && desktopLookatOrSelfieMode) {
+ rotation = getLookAtRotation();
+ removeLocalYComponent = false;
+ vectorsAreInAvatarFrame = false;
+ } else {
+ controller::Pose headPoseLocal = getControllerPoseInAvatarFrame(controller::Action::HEAD);
+ if (headPoseLocal.isValid()) {
+ rotation = headPoseLocal.rotation;
+ }
+ removeLocalYComponent = !hoveringOrCollisionless;
+ }
+
+ forward = rotation * forward;
+ right = rotation * right;
+ }
+
+ if (removeLocalYComponent) {
+ assert(vectorsAreInAvatarFrame);
+
+ auto removeYAndNormalize = [](glm::vec3& vector) {
+ vector.y = 0.f;
+ // Normalize if the remaining components are large enough to get a reliable direction.
+ float length = glm::length(vector);
+ const float MIN_LENGTH_FOR_NORMALIZE = 0.061f; // sin(3.5 degrees)
+ if (length > MIN_LENGTH_FOR_NORMALIZE) {
+ vector /= length;
+ } else {
+ vector = Vectors::ZERO;
+ }
+ };
+
+ removeYAndNormalize(forward);
+ removeYAndNormalize(right);
+ }
+
+ // In HMD, we combine the head pitch into the flying direction even when using hand-relative movement.
+ // Todo: Option to ignore head pitch in hand-relative flying (MOVEMENT_HAND_RELATIVE_LEVELED would then act like MOVEMENT_HAND_RELATIVE when flying).
+ if (HMDHandRelativeMovement && hoveringOrCollisionless) {
+ controller::Pose headPoseLocal = getControllerPoseInAvatarFrame(controller::Action::HEAD);
+
+ if (headPoseLocal.isValid()) {
+ glm::quat headLocalPitchRotation;
+ glm::quat headLocalYawRotation_unused;
+ swingTwistDecomposition(headPoseLocal.rotation, Vectors::UP, headLocalPitchRotation, headLocalYawRotation_unused);
+
+ forward = headLocalPitchRotation * forward;
+ right = headLocalPitchRotation * right;
}
- } else {
- forward = IDENTITY_FORWARD;
- right = IDENTITY_RIGHT;
}
glm::vec3 direction = scaleMotorSpeed(forward, right);
- if (state == CharacterController::State::Hover ||
- _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) {
- glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
+ if (vectorsAreInAvatarFrame) {
+ direction = getWorldOrientation() * direction;
+ }
+
+ if (hoveringOrCollisionless) {
+ glm::vec3 up = getDriveKey(TRANSLATE_Y) * IDENTITY_UP;
direction += up;
}
@@ -4562,7 +4619,7 @@ bool MyAvatar::getFlyingHMDPref() {
}
// Public interface for targetscale
-float MyAvatar::getAvatarScale() {
+float MyAvatar::getAvatarScale() const {
return getTargetScale();
}
@@ -4723,8 +4780,11 @@ void MyAvatar::triggerRotationRecenter() {
_follow.setForceActivateRotation(true);
}
+// Derive the sensor-space matrix for the body, based on the pose of the HMD and hips tracker.
// old school meat hook style
-glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const {
+// forceFollowYPos (default false): true to force the body matrix to be affected by the HMD's
+// vertical position, even if crouch recentering is disabled.
+glm::mat4 MyAvatar::deriveBodyFromHMDSensor(const bool forceFollowYPos) const {
glm::vec3 headPosition(0.0f, _userHeight.get(), 0.0f);
glm::quat headOrientation;
auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD);
@@ -4758,10 +4818,33 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const {
glm::vec3 headToNeck = headOrientation * Quaternions::Y_180 * (localNeck - localHead);
glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck;
- float invSensorToWorldScale = getUserEyeHeight() / getEyeHeight();
- glm::vec3 bodyPos = headPosition + invSensorToWorldScale * (headToNeck + neckToRoot);
+ float worldToSensorScale = getUserEyeHeight() / getEyeHeight();
+ glm::vec3 bodyPos = headPosition + worldToSensorScale * (headToNeck + neckToRoot);
+ glm::quat bodyQuat;
- return createMatFromQuatAndPos(headOrientationYawOnly, bodyPos);
+ controller::Pose hipsControllerPose = getControllerPoseInSensorFrame(controller::Action::HIPS);
+ if (hipsControllerPose.isValid()) {
+ glm::quat hipsOrientation = hipsControllerPose.rotation * Quaternions::Y_180;
+ glm::quat hipsOrientationYawOnly = cancelOutRollAndPitch(hipsOrientation);
+
+ glm::vec3 hipsPos = hipsControllerPose.getTranslation();
+ bodyPos.x = hipsPos.x;
+ bodyPos.z = hipsPos.z;
+
+ bodyQuat = hipsOrientationYawOnly;
+ } else {
+ bodyQuat = headOrientationYawOnly;
+ }
+
+ if (!forceFollowYPos && !getHMDCrouchRecenterEnabled()) {
+ // Set the body's vertical position as if it were standing in its T-pose.
+ float rigToUserScale = getUserEyeHeight() / getUnscaledEyeHeight();
+ bodyPos.y = rigToUserScale * rig.getUnscaledHipsHeight();
+ }
+
+ glm::mat4 bodyMat = createMatFromQuatAndPos(bodyQuat, bodyPos);
+
+ return bodyMat;
}
glm::mat4 MyAvatar::getSpine2RotationRigSpace() const {
@@ -4893,11 +4976,11 @@ glm::vec3 MyAvatar::computeCounterBalance() {
glm::vec3 currentCg = (1.0f / totalMass) * sumOfMoments;
currentCg.y = 0.0f;
// dampening the center of gravity, in effect, limits the value to the perimeter of the base of support
- float baseScale = 1.0f;
+ float baseAndAvatarScale = getAvatarScale();
if (getUserEyeHeight() > 0.0f) {
- baseScale = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT;
+ baseAndAvatarScale *= getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT;
}
- glm::vec3 desiredCg = dampenCgMovement(currentCg, baseScale);
+ glm::vec3 desiredCg = dampenCgMovement(currentCg, baseAndAvatarScale);
// compute hips position to maintain desiredCg
glm::vec3 counterBalancedForHead = (totalMass + DEFAULT_AVATAR_HIPS_MASS) * desiredCg;
@@ -4916,9 +4999,10 @@ glm::vec3 MyAvatar::computeCounterBalance() {
// this is to be sure that the feet don't lift off the floor.
// add 5 centimeters to allow for going up on the toes.
- if (counterBalancedCg.y > (tposeHips.y + 0.05f)) {
+ float maxCounterBalancedCGY = (tposeHips.y + 0.05f) * baseAndAvatarScale;
+ if (counterBalancedCg.y > maxCounterBalancedCGY) {
// if the height is higher than default hips, clamp to default hips
- counterBalancedCg.y = tposeHips.y + 0.05f;
+ counterBalancedCg.y = maxCounterBalancedCGY;
}
return counterBalancedCg;
}
@@ -4949,7 +5033,7 @@ static void drawBaseOfSupport(float baseOfSupportScale, float footLocal, glm::ma
float clampBack = DEFAULT_AVATAR_SUPPORT_BASE_BACK * baseOfSupportScale;
float clampLeft = DEFAULT_AVATAR_SUPPORT_BASE_LEFT * baseOfSupportScale;
float clampRight = DEFAULT_AVATAR_SUPPORT_BASE_RIGHT * baseOfSupportScale;
- float floor = footLocal + 0.05f;
+ float floor = footLocal;
// transform the base of support corners to world space
glm::vec3 frontRight = transformPoint(avatarToWorld, { clampRight, floor, clampFront });
@@ -4980,7 +5064,7 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() {
glm::mat4 avatarHeadMat = glm::inverse(avatarToWorldMat) * sensorToWorldMat * sensorHeadMat;
if (_enableDebugDrawBaseOfSupport) {
- float scaleBaseOfSupport = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT;
+ float scaleBaseOfSupport = (getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT) * getAvatarScale();
glm::vec3 rightFootPositionLocal = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot"));
drawBaseOfSupport(scaleBaseOfSupport, rightFootPositionLocal.y, avatarToWorldMat);
}
@@ -4996,26 +5080,16 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() {
return worldToSensorMat * avatarToWorldMat * avatarHipsMat;
}
-static bool isInsideLine(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
- return (((b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x)) > 0);
-}
-
-static bool withinBaseOfSupport(const controller::Pose& head) {
- float userScale = 1.0f;
-
- glm::vec3 frontLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD);
- glm::vec3 frontRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD);
- glm::vec3 backLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD);
- glm::vec3 backRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD);
-
- bool isWithinSupport = false;
- if (head.isValid()) {
- bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, head.getTranslation());
- bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, head.getTranslation());
- bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, head.getTranslation()) &&
- isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation()));
- isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase);
+static bool withinBaseOfSupport(const controller::Pose& head, const float avatarScale) {
+ if (!head.isValid()) {
+ return false;
}
+
+ vec3 headPosScaled = head.getTranslation() / avatarScale;
+ bool isWithinSupport = (headPosScaled.x > -DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD) &&
+ (headPosScaled.x < DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD) &&
+ (headPosScaled.z > -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD) &&
+ (headPosScaled.z < DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD);
return isWithinSupport;
}
@@ -5031,10 +5105,10 @@ static bool headAngularVelocityBelowThreshold(const controller::Pose& head) {
return isBelowThreshold;
}
-static bool isWithinThresholdHeightMode(const controller::Pose& head, const float& newMode, const float& scale) {
+static bool isWithinThresholdHeightMode(const controller::Pose& head, const float newMode, const float avatarScale) {
bool isWithinThreshold = true;
if (head.isValid()) {
- isWithinThreshold = (head.getTranslation().y - newMode) > (DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD * scale);
+ isWithinThreshold = head.getTranslation().y > ((DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD + newMode) * avatarScale);
}
return isWithinThreshold;
}
@@ -5058,7 +5132,7 @@ float MyAvatar::computeStandingHeightMode(const controller::Pose& head) {
modeInMeters = ((float)mode) / CENTIMETERS_PER_METER;
if (!(modeInMeters > getCurrentStandingHeight())) {
// if not greater check for a reset
- if (getResetMode() && getControllerPoseInAvatarFrame(controller::Action::HEAD).isValid()) {
+ if (getResetMode() && _isBodyPartTracked._head) {
setResetMode(false);
float resetModeInCentimeters = glm::floor((head.getTranslation().y - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER);
modeInMeters = (resetModeInCentimeters / CENTIMETERS_PER_METER);
@@ -5115,12 +5189,12 @@ static bool handAngularVelocityBelowThreshold(const controller::Pose& leftHand,
(rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD));
}
-static bool headVelocityGreaterThanThreshold(const controller::Pose& head) {
+static bool headVelocityGreaterThanThreshold(const controller::Pose& head, const float avatarScale) {
float headVelocityMagnitude = 0.0f;
if (head.isValid()) {
headVelocityMagnitude = glm::length(head.getVelocity());
}
- return headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD;
+ return headVelocityMagnitude > (DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD * avatarScale);
}
glm::quat MyAvatar::computeAverageHeadRotation(const controller::Pose& head) {
@@ -5144,6 +5218,7 @@ float MyAvatar::getUserHeight() const {
void MyAvatar::setUserHeight(float value) {
_userHeight.set(value);
+ centerBodyInternal(false);
float sensorToWorldScale = getEyeHeight() / getUserEyeHeight();
emit sensorToWorldScaleChanged(sensorToWorldScale);
@@ -5159,16 +5234,45 @@ bool MyAvatar::getIsInWalkingState() const {
return _isInWalkingState;
}
+// Determine if the user is sitting in the real world.
bool MyAvatar::getIsInSittingState() const {
return _isInSittingState.get();
}
+// Deprecated, will be removed.
MyAvatar::SitStandModelType MyAvatar::getUserRecenterModel() const {
- return _userRecenterModel.get();
+ qCDebug(interfaceapp)
+ << "MyAvatar.getUserRecenterModel is deprecated and will be removed.";
+
+ // The legacy SitStandModelType corresponding to each AllowAvatarLeaningPreference.
+ std::array(AllowAvatarLeaningPreference::Count)> legacySitStandModels = {
+ SitStandModelType::Auto, // AllowAvatarLeaningPreference::WhenUserIsStanding
+ SitStandModelType::ForceStand, // AllowAvatarLeaningPreference::Always
+ SitStandModelType::ForceSit, // AllowAvatarLeaningPreference::Never
+ SitStandModelType::DisableHMDLean // AllowAvatarLeaningPreference::AlwaysNoRecenter
+ };
+
+ return legacySitStandModels[static_cast(_allowAvatarLeaningPreference.get())];
}
+// Deprecated, will be removed.
bool MyAvatar::getIsSitStandStateLocked() const {
- return _lockSitStandState.get();
+ qCDebug(interfaceapp) << "MyAvatar.getIsSitStandStateLocked is deprecated and will be removed.";
+
+ // In the old code, the record of the user's sit/stand state was locked except when using
+ // SitStandModelType::Auto or SitStandModelType::DisableHMDLean.
+ return (_allowAvatarStandingPreference.get() != AllowAvatarStandingPreference::WhenUserIsStanding) &&
+ (_allowAvatarLeaningPreference.get() != AllowAvatarLeaningPreference::AlwaysNoRecenter);
+}
+
+// Get the user preference of when MyAvatar may stand.
+MyAvatar::AllowAvatarStandingPreference MyAvatar::getAllowAvatarStandingPreference() const {
+ return _allowAvatarStandingPreference.get();
+}
+
+// Get the user preference of when MyAvatar may lean.
+MyAvatar::AllowAvatarLeaningPreference MyAvatar::getAllowAvatarLeaningPreference() const {
+ return _allowAvatarLeaningPreference.get();
}
float MyAvatar::getWalkSpeed() const {
@@ -5221,59 +5325,61 @@ void MyAvatar::setIsInWalkingState(bool isWalking) {
_isInWalkingState = isWalking;
}
+// Specify whether the user is sitting or standing in the real world.
void MyAvatar::setIsInSittingState(bool isSitting) {
+ // In updateSitStandState, we only change state if this timer is above a threshold (STANDING_TIMEOUT, SITTING_TIMEOUT).
+ // This avoids changing state if the user sits and stands up quickly.
_sitStandStateTimer = 0.0f;
- _squatTimer = 0.0f;
- // on reset height we need the count to be more than one in case the user sits and stands up quickly.
+
_isInSittingState.set(isSitting);
setResetMode(true);
- if (isSitting) {
- setCenterOfGravityModelEnabled(false);
- } else {
- setCenterOfGravityModelEnabled(true);
- }
setSitStandStateChange(true);
}
+// Deprecated, will be removed.
void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) {
-
- _userRecenterModel.set(modelName);
+ qCDebug(interfaceapp)
+ << "MyAvatar.setUserRecenterModel is deprecated and will be removed.";
switch (modelName) {
- case MyAvatar::SitStandModelType::ForceSit:
- setHMDLeanRecenterEnabled(true);
- setIsInSittingState(true);
- setIsSitStandStateLocked(true);
+ case SitStandModelType::ForceSit:
+ setAllowAvatarStandingPreference(AllowAvatarStandingPreference::Always);
+ setAllowAvatarLeaningPreference(AllowAvatarLeaningPreference::Never);
break;
- case MyAvatar::SitStandModelType::ForceStand:
- setHMDLeanRecenterEnabled(true);
- setIsInSittingState(false);
- setIsSitStandStateLocked(true);
+ case SitStandModelType::ForceStand:
+ setAllowAvatarStandingPreference(AllowAvatarStandingPreference::Always);
+ setAllowAvatarLeaningPreference(AllowAvatarLeaningPreference::Always);
break;
- case MyAvatar::SitStandModelType::Auto:
+ case SitStandModelType::Auto:
default:
- setHMDLeanRecenterEnabled(true);
- setIsInSittingState(false);
- setIsSitStandStateLocked(false);
+ setAllowAvatarStandingPreference(AllowAvatarStandingPreference::Always);
+ setAllowAvatarLeaningPreference(AllowAvatarLeaningPreference::WhenUserIsStanding);
break;
- case MyAvatar::SitStandModelType::DisableHMDLean:
- setHMDLeanRecenterEnabled(false);
- setIsInSittingState(false);
- setIsSitStandStateLocked(false);
+ case SitStandModelType::DisableHMDLean:
+ setAllowAvatarStandingPreference(AllowAvatarStandingPreference::WhenUserIsStanding);
+ setAllowAvatarLeaningPreference(AllowAvatarLeaningPreference::AlwaysNoRecenter);
break;
}
}
+// Set the user preference of when the avatar may stand.
+void MyAvatar::setAllowAvatarStandingPreference(const MyAvatar::AllowAvatarStandingPreference preference) {
+ _allowAvatarStandingPreference.set(preference);
+
+ // Set the correct vertical position for the avatar body relative to the HMD,
+ // according to the newly-selected avatar standing preference.
+ centerBodyInternal(false);
+}
+
+// Deprecated, will be removed.
void MyAvatar::setIsSitStandStateLocked(bool isLocked) {
- _lockSitStandState.set(isLocked);
- _sitStandStateTimer = 0.0f;
- _squatTimer = 0.0f;
- _averageUserHeightSensorSpace = _userHeight.get();
- _tippingPoint = _userHeight.get();
- if (!isLocked) {
- // always start the auto transition mode in standing state.
- setIsInSittingState(false);
- }
+ Q_UNUSED(isLocked);
+ qCDebug(interfaceapp) << "MyAvatar.setIsSitStandStateLocked is deprecated and will be removed.";
+}
+
+// Set the user preference of when the avatar may lean.
+void MyAvatar::setAllowAvatarLeaningPreference(const MyAvatar::AllowAvatarLeaningPreference preference) {
+ _allowAvatarLeaningPreference.set(preference);
}
void MyAvatar::setWalkSpeed(float value) {
@@ -5402,10 +5508,12 @@ float MyAvatar::getAnalogPlusSprintSpeed() const {
return _analogPlusSprintSpeed.get();
}
+// Indicate whether the user's real-world sit/stand state has changed or not.
void MyAvatar::setSitStandStateChange(bool stateChanged) {
_sitStandStateChange = stateChanged;
}
+// Determine if the user's real-world sit/stand state has changed.
float MyAvatar::getSitStandStateChange() const {
return _sitStandStateChange;
}
@@ -5499,65 +5607,84 @@ MyAvatar::FollowHelper::FollowHelper() {
}
void MyAvatar::FollowHelper::deactivate() {
- for (int i = 0; i < NumFollowTypes; i++) {
- deactivate((FollowType)i);
+ for (uint i = 0; i < static_cast(CharacterController::FollowType::Count); i++) {
+ deactivate(static_cast(i));
}
}
-void MyAvatar::FollowHelper::deactivate(FollowType type) {
- assert(type >= 0 && type < NumFollowTypes);
+void MyAvatar::FollowHelper::deactivate(CharacterController::FollowType type) {
+ assert(static_cast(type) >= 0 && type < CharacterController::FollowType::Count);
_timeRemaining[(int)type] = 0.0f;
}
-void MyAvatar::FollowHelper::activate(FollowType type) {
- assert(type >= 0 && type < NumFollowTypes);
+// snapFollow: true to snap immediately to the desired transform with regard to 'type',
+// eg. activate(FollowType::Rotation, true) snaps the FollowHelper's rotation immediately
+// to the rotation of its _followDesiredBodyTransform.
+void MyAvatar::FollowHelper::activate(CharacterController::FollowType type, const bool snapFollow) {
+ assert(static_cast(type) >= 0 && type < CharacterController::FollowType::Count);
+
// TODO: Perhaps, the follow time should be proportional to the displacement.
- _timeRemaining[(int)type] = FOLLOW_TIME;
+ _timeRemaining[(int)type] = snapFollow ? CharacterController::FOLLOW_TIME_IMMEDIATE_SNAP : FOLLOW_TIME;
}
-bool MyAvatar::FollowHelper::isActive(FollowType type) const {
- assert(type >= 0 && type < NumFollowTypes);
+bool MyAvatar::FollowHelper::isActive(CharacterController::FollowType type) const {
+ assert(static_cast(type) >= 0 && type < CharacterController::FollowType::Count);
return _timeRemaining[(int)type] > 0.0f;
}
bool MyAvatar::FollowHelper::isActive() const {
- for (int i = 0; i < NumFollowTypes; i++) {
- if (isActive((FollowType)i)) {
+ for (uint i = 0; i < static_cast(CharacterController::FollowType::Count); i++) {
+ if (isActive(static_cast(i))) {
return true;
}
}
return false;
}
-float MyAvatar::FollowHelper::getMaxTimeRemaining() const {
- float max = 0.0f;
- for (int i = 0; i < NumFollowTypes; i++) {
- if (_timeRemaining[i] > max) {
- max = _timeRemaining[i];
+void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) {
+ for (auto& time : _timeRemaining) {
+ if (time == CharacterController::FOLLOW_TIME_IMMEDIATE_SNAP) {
+ time = 0.0f;
+ } else {
+ time -= dt;
}
}
- return max;
}
-void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) {
- for (int i = 0; i < NumFollowTypes; i++) {
- _timeRemaining[i] -= dt;
+// shouldSnapOut: (out) true if the FollowHelper should snap immediately to its desired rotation.
+bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix,
+ bool& shouldSnapOut) const {
+ // If hips are under direct control (tracked), they give our desired body rotation and we snap to it every frame.
+ if (myAvatar.areHipsTracked()) {
+ shouldSnapOut = true;
+ return true;
+ } else {
+ shouldSnapOut = false;
}
-}
-bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold());
glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix);
return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD;
}
-bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
+// Determine if the horizontal following should activate, for a user who is sitting in the real world.
+bool MyAvatar::FollowHelper::shouldActivateHorizontal_userSitting(const MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix) const {
+ if (!myAvatar.isAllowedToLean()) {
+ controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD);
+ if (!withinBaseOfSupport(currentHeadPose, myAvatar.getAvatarScale())) {
+ return true;
+ }
+ }
+
// -z axis of currentBodyMatrix in world space.
glm::vec3 forward = glm::normalize(glm::vec3(-currentBodyMatrix[0][2], -currentBodyMatrix[1][2], -currentBodyMatrix[2][2]));
// x axis of currentBodyMatrix in world space.
glm::vec3 right = glm::normalize(glm::vec3(currentBodyMatrix[0][0], currentBodyMatrix[1][0], currentBodyMatrix[2][0]));
glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix);
- controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD);
float forwardLeanAmount = glm::dot(forward, offset);
float lateralLeanAmount = glm::dot(right, offset);
@@ -5567,11 +5694,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar,
const float MAX_BACKWARD_LEAN = 0.1f;
bool stepDetected = false;
- if (myAvatar.getIsInSittingState()) {
- if (!withinBaseOfSupport(currentHeadPose)) {
- stepDetected = true;
- }
- } else if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) {
+ if (forwardLeanAmount > MAX_FORWARD_LEAN) {
stepDetected = true;
} else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) {
stepDetected = true;
@@ -5581,52 +5704,82 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar,
return stepDetected;
}
-bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const {
-
- // get the current readings
- controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD);
- controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND);
- controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND);
- controller::Pose currentHeadSensorPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD);
-
- bool stepDetected = false;
- float myScale = myAvatar.getAvatarScale();
+// Determine if the horizontal following should activate, for a user who is standing in the real world.
+// resetModeOut: (out) true if setResetMode(true) should be called if this function returns true.
+// goToWalkingStateOut: (out) true if setIsInWalkingState(true) should be called if this function returns true.
+bool MyAvatar::FollowHelper::shouldActivateHorizontal_userStanding(
+ const MyAvatar& myAvatar,
+ bool& resetModeOut,
+ bool& goToWalkingStateOut) const {
if (myAvatar.getIsInWalkingState()) {
- stepDetected = true;
- } else {
- if (!withinBaseOfSupport(currentHeadPose) &&
- headAngularVelocityBelowThreshold(currentHeadPose) &&
- isWithinThresholdHeightMode(currentHeadSensorPose, myAvatar.getCurrentStandingHeight(), myScale) &&
- handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) &&
- handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) &&
- headVelocityGreaterThanThreshold(currentHeadPose) &&
- isHeadLevel(currentHeadPose, myAvatar.getAverageHeadRotation())) {
- // a step is detected
+ return true;
+ }
+
+ controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD);
+ bool stepDetected = false;
+ float avatarScale = myAvatar.getAvatarScale();
+
+ if (!withinBaseOfSupport(currentHeadPose, avatarScale)) {
+ if (!myAvatar.isAllowedToLean()) {
stepDetected = true;
- if (glm::length(currentHeadPose.velocity) > DEFAULT_AVATAR_WALK_SPEED_THRESHOLD) {
- myAvatar.setIsInWalkingState(true);
- }
} else {
- glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips"));
- glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head"));
- glm::vec3 currentHeadPosition = currentHeadPose.getTranslation();
- float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition);
- if (!isActive(Horizontal) &&
- (!isActive(Vertical)) &&
- (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + (DEFAULT_AVATAR_SPINE_STRETCH_LIMIT * anatomicalHeadToHipsDistance)))) {
- myAvatar.setResetMode(true);
+ // get the current readings
+ controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND);
+ controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND);
+ controller::Pose currentHeadSensorPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD);
+
+ if (headAngularVelocityBelowThreshold(currentHeadPose) &&
+ isWithinThresholdHeightMode(currentHeadSensorPose, myAvatar.getCurrentStandingHeight(), avatarScale) &&
+ handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) &&
+ handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) &&
+ headVelocityGreaterThanThreshold(currentHeadPose, avatarScale) &&
+ isHeadLevel(currentHeadPose, myAvatar.getAverageHeadRotation())) {
+ // a step is detected
stepDetected = true;
- if (glm::length(currentHeadPose.velocity) > DEFAULT_AVATAR_WALK_SPEED_THRESHOLD) {
- myAvatar.setIsInWalkingState(true);
+ }
+ }
+ }
+
+ if (!stepDetected) {
+ glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips"));
+ glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head"));
+ glm::vec3 currentHeadPosition = currentHeadPose.getTranslation();
+ float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition);
+ if (!isActive(CharacterController::FollowType::Horizontal) && (!isActive(CharacterController::FollowType::Vertical)) &&
+ (glm::length(currentHeadPosition - defaultHipsPosition) >
+ (anatomicalHeadToHipsDistance + (DEFAULT_AVATAR_SPINE_STRETCH_LIMIT * anatomicalHeadToHipsDistance)))) {
+ resetModeOut = true;
+ stepDetected = true;
+ if (currentHeadPose.isValid()) {
+ if (glm::length(currentHeadPose.velocity) > (DEFAULT_AVATAR_WALK_SPEED_THRESHOLD * avatarScale)) {
+ goToWalkingStateOut = true;
}
}
}
}
+
return stepDetected;
}
-bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
+// Determine if the horizontal following should activate.
+// resetModeOut: (out) true if setResetMode(true) should be called if this function returns true.
+// goToWalkingStateOut: (out) true if setIsInWalkingState(true) should be called if this function returns true.
+bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix,
+ bool& resetModeOut,
+ bool& goToWalkingStateOut) const {
+ if (myAvatar.getIsInSittingState()) {
+ return shouldActivateHorizontal_userSitting(myAvatar, desiredBodyMatrix, currentBodyMatrix);
+ } else {
+ return shouldActivateHorizontal_userStanding(myAvatar, resetModeOut, goToWalkingStateOut);
+ }
+}
+
+bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix) const {
const float CYLINDER_TOP = 2.0f;
const float CYLINDER_BOTTOM = -1.5f;
const float SITTING_BOTTOM = -0.02f;
@@ -5638,9 +5791,6 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co
returnValue = true;
} else {
if (myAvatar.getIsInSittingState()) {
- if (myAvatar.getIsSitStandStateLocked()) {
- returnValue = (offset.y > CYLINDER_TOP);
- }
if (offset.y < SITTING_BOTTOM) {
// we recenter more easily when in sitting state.
returnValue = true;
@@ -5648,60 +5798,89 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co
} else {
// in the standing state
returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM);
- // finally check for squats in standing
- if (_squatDetected) {
- returnValue = true;
- }
}
}
return returnValue;
}
-void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix,
- const glm::mat4& currentBodyMatrix, bool hasDriveInput) {
+void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix,
+ bool hasDriveInput) {
+ if (myAvatar.getHMDLeanRecenterEnabled()) {
- if (myAvatar.getHMDLeanRecenterEnabled() &&
- qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) {
- if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) {
- activate(Rotation);
- myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing());
- }
- if (myAvatar.getCenterOfGravityModelEnabled()) {
- if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) {
- activate(Horizontal);
- if (myAvatar.getEnableStepResetRotation()) {
- activate(Rotation);
- myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing());
- }
+ // Rotation recenter
+
+ {
+ bool snapFollow = false;
+ if (!isActive(CharacterController::FollowType::Rotation) &&
+ (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix, snapFollow) || hasDriveInput)) {
+ activate(CharacterController::FollowType::Rotation, snapFollow);
+ myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing());
}
+ }
+
+ // Lean recenter
+
+ if ((myAvatar.areFeetTracked() || getForceActivateHorizontal()) && !isActive(CharacterController::FollowType::Horizontal)) {
+ activate(CharacterController::FollowType::Horizontal, myAvatar.areFeetTracked());
+ setForceActivateHorizontal(false);
} else {
- // center of gravity model is not enabled
- if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) {
- activate(Horizontal);
- if (myAvatar.getEnableStepResetRotation() && !myAvatar.getIsInSittingState()) {
- activate(Rotation);
- myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing());
+ if ((myAvatar.getAllowAvatarLeaningPreference() != MyAvatar::AllowAvatarLeaningPreference::AlwaysNoRecenter) &&
+ qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) {
+
+ bool resetModeOut = false;
+ bool goToWalkingStateOut = false;
+
+ // True if the user can turn their body while sitting (eg. swivel chair).
+ // Todo?: We could expose this as an option.
+ // (Regardless, rotation recentering does kick-in if they turn too far).
+ constexpr bool USER_CAN_TURN_BODY_WHILE_SITTING = false;
+
+ if (!isActive(CharacterController::FollowType::Horizontal) &&
+ (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix, resetModeOut,
+ goToWalkingStateOut) ||
+ hasDriveInput)) {
+ activate(CharacterController::FollowType::Horizontal, false);
+ if (myAvatar.getEnableStepResetRotation() &&
+ (USER_CAN_TURN_BODY_WHILE_SITTING || !myAvatar.getIsInSittingState())) {
+ activate(CharacterController::FollowType::Rotation, false);
+ myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing());
+ }
+
+ if (resetModeOut) {
+ myAvatar.setResetMode(true);
+ }
+
+ if (goToWalkingStateOut) {
+ myAvatar.setIsInWalkingState(true);
+ }
}
}
}
- if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) {
- activate(Vertical);
- if (_squatDetected) {
- _squatDetected = false;
+
+ // Vertical recenter
+
+ if (myAvatar.getHMDCrouchRecenterEnabled() && qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) {
+ if (!isActive(CharacterController::FollowType::Vertical) &&
+ (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) {
+ activate(CharacterController::FollowType::Vertical, false);
}
}
} else {
- if (!isActive(Rotation) && getForceActivateRotation()) {
- activate(Rotation);
+ // Forced activations can be requested by MyAvatar::triggerVerticalRecenter, callable from scripts.
+
+ if (!isActive(CharacterController::FollowType::Rotation) && getForceActivateRotation()) {
+ activate(CharacterController::FollowType::Rotation, true);
myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing());
setForceActivateRotation(false);
}
- if (!isActive(Horizontal) && getForceActivateHorizontal()) {
- activate(Horizontal);
+ if (!isActive(CharacterController::FollowType::Horizontal) && getForceActivateHorizontal()) {
+ activate(CharacterController::FollowType::Horizontal, true);
setForceActivateHorizontal(false);
}
- if (!isActive(Vertical) && getForceActivateVertical()) {
- activate(Vertical);
+ if (!isActive(CharacterController::FollowType::Vertical) && getForceActivateVertical()) {
+ activate(CharacterController::FollowType::Vertical, true);
setForceActivateVertical(false);
}
}
@@ -5721,21 +5900,21 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat
// remove scale present from sensorToWorldMatrix
followWorldPose.scale() = glm::vec3(1.0f);
- if (isActive(Rotation)) {
- //use the hmd reading for the hips follow
- followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix);
+ if (isActive(CharacterController::FollowType::Rotation)) {
+ //use the hmd reading for the hips follow
+ followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix);
}
- if (isActive(Horizontal)) {
+ if (isActive(CharacterController::FollowType::Horizontal)) {
glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix);
followWorldPose.trans().x = desiredTranslation.x;
followWorldPose.trans().z = desiredTranslation.z;
}
- if (isActive(Vertical)) {
+ if (isActive(CharacterController::FollowType::Vertical)) {
glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix);
followWorldPose.trans().y = desiredTranslation.y;
}
- myAvatar.getCharacterController()->setFollowParameters(followWorldPose, getMaxTimeRemaining());
+ myAvatar.getCharacterController()->setFollowParameters(followWorldPose);
}
glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix) {
@@ -5755,10 +5934,13 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(MyAvatar& myAvatar, const gl
glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix),
sensorLinearDisplacement + extractTranslation(currentBodyMatrix));
- if (myAvatar.getSitStandStateChange()) {
- myAvatar.setSitStandStateChange(false);
- deactivate(Vertical);
- setTranslation(newBodyMat, extractTranslation(myAvatar.deriveBodyFromHMDSensor()));
+
+ if (myAvatar.getHMDCrouchRecenterEnabled()) {
+ if (myAvatar.getSitStandStateChange()) {
+ myAvatar.setSitStandStateChange(false);
+ deactivate(CharacterController::FollowType::Vertical);
+ setTranslation(newBodyMat, extractTranslation(myAvatar.deriveBodyFromHMDSensor()));
+ }
}
return newBodyMat;
} else {
@@ -6127,7 +6309,7 @@ void MyAvatar::updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose&
}
bool MyAvatar::isRecenteringHorizontally() const {
- return _follow.isActive(FollowHelper::Horizontal);
+ return _follow.isActive(CharacterController::FollowType::Horizontal);
}
const MyHead* MyAvatar::getMyHead() const {
@@ -6583,7 +6765,7 @@ void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) {
setHMDLeanRecenterEnabled(false);
// Disable movement
setSitDriveKeysStatus(false);
- centerBody();
+ centerBodyInternal(true);
int hipIndex = getJointIndex("Hips");
clearPinOnJoint(hipIndex);
pinJoint(hipIndex, position, rotation);
@@ -6601,7 +6783,7 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) {
_characterController.setSeated(false);
setCollisionsEnabled(true);
setHMDLeanRecenterEnabled(true);
- centerBody();
+ centerBodyInternal(false);
slamPosition(position);
setWorldOrientation(rotation);
@@ -6906,6 +7088,19 @@ bool MyAvatar::isJumping() {
_characterController.getState() == CharacterController::State::Takeoff) && !isFlying();
}
+// Determine if the avatar is allowed to lean in its current situation.
+bool MyAvatar::isAllowedToLean() const {
+ return (getAllowAvatarLeaningPreference() == MyAvatar::AllowAvatarLeaningPreference::Always) ||
+ ((getAllowAvatarLeaningPreference() == MyAvatar::AllowAvatarLeaningPreference::WhenUserIsStanding) &&
+ !getIsInSittingState());
+}
+
+// Determine if crouch recentering is enabled (making the avatar stand when the user is sitting in the real world).
+bool MyAvatar::getHMDCrouchRecenterEnabled() const {
+ return (!_characterController.getSeated() &&
+ (_allowAvatarStandingPreference.get() == AllowAvatarStandingPreference::Always) && !_isBodyPartTracked._feet);
+}
+
bool MyAvatar::setPointAt(const glm::vec3& pointAtTarget) {
if (QThread::currentThread() != thread()) {
bool result = false;
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 3140c68f88..3d278cf983 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -283,15 +283,16 @@ class MyAvatar : public Avatar {
* the value.
* @property {number} analogPlusSprintSpeed - The sprint (run) speed of your avatar for the "AnalogPlus" control scheme.
* @property {MyAvatar.SitStandModelType} userRecenterModel - Controls avatar leaning and recentering behavior.
- * @property {number} isInSittingState - true
if the user wearing the HMD is determined to be sitting
- * (avatar leaning is disabled, recentering is enabled), false
if the user wearing the HMD is
- * determined to be standing (avatar leaning is enabled, and avatar recenters if it leans too far).
- * If userRecenterModel == 2
(i.e., "auto") the property value automatically updates as the user sits
- * or stands, unless isSitStandStateLocked == true
. Setting the property value overrides the current
- * sitting / standing state, which is updated when the user next sits or stands unless
- * isSitStandStateLocked == true
.
+ * Deprecated: This property is deprecated and will be removed.
+ * @property {boolean} isInSittingState - true
if the user wearing the HMD is determined to be sitting;
+ * false
if the user wearing the HMD is determined to be standing. This can affect whether the avatar
+ * is allowed to stand, lean or recenter its footing, depending on user preferences.
+ * The property value automatically updates as the user sits or stands. Setting the property value overrides the current
+ * sitting / standing state, which is updated when the user next sits or stands.
* @property {boolean} isSitStandStateLocked - true
to lock the avatar sitting/standing state, i.e., use this
* to disable automatically changing state.
+ * Deprecated: This property is deprecated and will be removed.
+ * See also: getUserRecenterModel
and setUserRecenterModel
.
* @property {boolean} allowTeleporting - true
if teleporting is enabled in the Interface settings,
* false
if it isn't. Read-only.
*
@@ -413,8 +414,8 @@ class MyAvatar : public Avatar {
Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed NOTIFY walkBackwardSpeedChanged);
Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed NOTIFY sprintSpeedChanged);
Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState);
- Q_PROPERTY(MyAvatar::SitStandModelType userRecenterModel READ getUserRecenterModel WRITE setUserRecenterModel);
- Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked);
+ Q_PROPERTY(MyAvatar::SitStandModelType userRecenterModel READ getUserRecenterModel WRITE setUserRecenterModel); // Deprecated
+ Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked); // Deprecated
Q_PROPERTY(bool allowTeleporting READ getAllowTeleporting)
const QString DOMINANT_LEFT_HAND = "left";
@@ -519,6 +520,7 @@ public:
/**jsdoc
* Specifies different avatar leaning and recentering behaviors.
+ * Deprecated: This type is deprecated and will be removed.
*
*
* Value | Name | Description |
@@ -549,6 +551,29 @@ public:
};
Q_ENUM(SitStandModelType)
+ // Note: The option strings in setupPreferences (PreferencesDialog.cpp) must match this order.
+ enum class AllowAvatarStandingPreference : uint {
+ WhenUserIsStanding,
+ Always,
+ Count,
+ Default = Always
+ };
+ Q_ENUM(AllowAvatarStandingPreference)
+
+ // Note: The option strings in setupPreferences (PreferencesDialog.cpp) must match this order.
+ enum class AllowAvatarLeaningPreference : uint {
+ WhenUserIsStanding,
+ Always,
+ Never,
+ AlwaysNoRecenter, // experimental
+ Count,
+ Default = WhenUserIsStanding
+ };
+ Q_ENUM(AllowAvatarLeaningPreference)
+
+ static const std::array allowAvatarStandingPreferenceStrings;
+ static const std::array allowAvatarLeaningPreferenceStrings;
+
explicit MyAvatar(QThread* thread);
virtual ~MyAvatar();
@@ -1417,7 +1442,6 @@ public:
controller::Pose getControllerPoseInSensorFrame(controller::Action action) const;
controller::Pose getControllerPoseInWorldFrame(controller::Action action) const;
controller::Pose getControllerPoseInAvatarFrame(controller::Action action) const;
- glm::quat getOffHandRotation() const;
bool hasDriveInput() const;
@@ -1596,7 +1620,7 @@ public:
* @function MyAvatar.getAvatarScale
* @returns {number} The target scale for the avatar, range 0.005
– 1000.0
.
*/
- Q_INVOKABLE float getAvatarScale();
+ Q_INVOKABLE float getAvatarScale() const;
/**jsdoc
* Sets the target scale of the avatar. The target scale is the desired scale of the avatar without any restrictions on
@@ -1709,7 +1733,7 @@ public:
// derive avatar body position and orientation from the current HMD Sensor location.
// results are in sensor frame (-z forward)
- glm::mat4 deriveBodyFromHMDSensor() const;
+ glm::mat4 deriveBodyFromHMDSensor(const bool forceFollowYPos = false) const;
glm::mat4 getSpine2RotationRigSpace() const;
@@ -1753,10 +1777,14 @@ public:
bool getIsInWalkingState() const;
void setIsInSittingState(bool isSitting);
bool getIsInSittingState() const;
- void setUserRecenterModel(MyAvatar::SitStandModelType modelName);
- MyAvatar::SitStandModelType getUserRecenterModel() const;
- void setIsSitStandStateLocked(bool isLocked);
- bool getIsSitStandStateLocked() const;
+ void setUserRecenterModel(MyAvatar::SitStandModelType modelName); // Deprecated, will be removed.
+ MyAvatar::SitStandModelType getUserRecenterModel() const; // Deprecated, will be removed.
+ void setIsSitStandStateLocked(bool isLocked); // Deprecated, will be removed.
+ bool getIsSitStandStateLocked() const; // Deprecated, will be removed.
+ void setAllowAvatarStandingPreference(const AllowAvatarStandingPreference preference);
+ AllowAvatarStandingPreference getAllowAvatarStandingPreference() const;
+ void setAllowAvatarLeaningPreference(const AllowAvatarLeaningPreference preference);
+ AllowAvatarLeaningPreference getAllowAvatarLeaningPreference() const;
void setWalkSpeed(float value);
float getWalkSpeed() const;
void setWalkBackwardSpeed(float value);
@@ -1989,6 +2017,10 @@ public:
glm::vec3 getLookAtPivotPoint();
glm::vec3 getCameraEyesPosition(float deltaTime);
bool isJumping();
+ bool getHMDCrouchRecenterEnabled() const;
+ bool isAllowedToLean() const;
+ bool areFeetTracked() const { return _isBodyPartTracked._feet; }; // Determine if the feet are under direct control.
+ bool areHipsTracked() const { return _isBodyPartTracked._hips; }; // Determine if the hips are under direct control.
public slots:
@@ -2709,6 +2741,16 @@ private:
bool _isBraking { false };
bool _isAway { false };
+ // Indicates which parts of the body are under direct control (tracked).
+ struct {
+ bool _feet { false }; // Left or right foot.
+ bool _feetPreviousUpdate{ false };// Value of _feet on the previous update.
+ bool _hips{ false };
+ bool _leftHand{ false };
+ bool _rightHand{ false };
+ bool _head{ false };
+ } _isBodyPartTracked;
+
float _boomLength { ZOOM_DEFAULT };
float _yawSpeed; // degrees/sec
float _pitchSpeed; // degrees/sec
@@ -2791,6 +2833,7 @@ private:
void resetLookAtRotation(const glm::vec3& avatarPosition, const glm::quat& avatarOrientation);
void resetPointAt();
static glm::vec3 aimToBlendValues(const glm::vec3& aimVector, const glm::quat& frameOrientation);
+ void centerBodyInternal(const bool forceFollowYPos = false);
// Avatar Preferences
QUrl _fullAvatarURLFromPreferences;
@@ -2841,26 +2884,21 @@ private:
struct FollowHelper {
FollowHelper();
- enum FollowType {
- Rotation = 0,
- Horizontal,
- Vertical,
- NumFollowTypes
- };
- float _timeRemaining[NumFollowTypes];
+ CharacterController::FollowTimePerType _timeRemaining;
void deactivate();
- void deactivate(FollowType type);
- void activate();
- void activate(FollowType type);
+ void deactivate(CharacterController::FollowType type);
+ void activate(CharacterController::FollowType type, const bool snapFollow);
bool isActive() const;
- bool isActive(FollowType followType) const;
- float getMaxTimeRemaining() const;
+ bool isActive(CharacterController::FollowType followType) const;
void decrementTimeRemaining(float dt);
- bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
+ bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool& shouldSnapOut) const;
bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
- bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
- bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const;
+ bool shouldActivateHorizontal(const MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix,
+ bool& resetModeOut,
+ bool& goToWalkingStateOut) const;
void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput);
glm::mat4 postPhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix);
bool getForceActivateRotation() const;
@@ -2871,16 +2909,23 @@ private:
void setForceActivateHorizontal(bool val);
bool getToggleHipsFollowing() const;
void setToggleHipsFollowing(bool followHead);
- bool _squatDetected { false };
std::atomic _forceActivateRotation { false };
std::atomic _forceActivateVertical { false };
std::atomic _forceActivateHorizontal { false };
std::atomic _toggleHipsFollowing { true };
+
+ private:
+ bool shouldActivateHorizontal_userSitting(const MyAvatar& myAvatar,
+ const glm::mat4& desiredBodyMatrix,
+ const glm::mat4& currentBodyMatrix) const;
+ bool shouldActivateHorizontal_userStanding(const MyAvatar& myAvatar,
+ bool& resetModeOut,
+ bool& goToWalkingStateOut) const;
};
FollowHelper _follow;
- bool isFollowActive(FollowHelper::FollowType followType) const;
+ bool isFollowActive(CharacterController::FollowType followType) const;
bool _goToPending { false };
bool _physicsSafetyPending { false };
@@ -2922,6 +2967,9 @@ private:
bool _centerOfGravityModelEnabled { true };
bool _hmdLeanRecenterEnabled { true };
+ bool _hmdCrouchRecenterEnabled {
+ true
+ }; // Is MyAvatar allowed to recenter vertically (stand) when the user is sitting in the real world.
bool _sprint { false };
AnimPose _prePhysicsRoomPose;
@@ -2953,7 +3001,6 @@ private:
ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT };
float _averageUserHeightSensorSpace { _userHeight.get() };
bool _sitStandStateChange { false };
- ThreadSafeValueCache _lockSitStandState { false };
// max unscaled forward movement speed
ThreadSafeValueCache _defaultWalkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED };
@@ -2969,9 +3016,13 @@ private:
float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR };
bool _isInWalkingState { false };
ThreadSafeValueCache _isInSittingState { false };
- ThreadSafeValueCache _userRecenterModel { MyAvatar::SitStandModelType::Auto };
+ ThreadSafeValueCache _allowAvatarStandingPreference{
+ MyAvatar::AllowAvatarStandingPreference::Default
+ }; // The user preference of when MyAvatar may stand.
+ ThreadSafeValueCache _allowAvatarLeaningPreference{
+ MyAvatar::AllowAvatarLeaningPreference::Default
+ }; // The user preference of when MyAvatar may lean.
float _sitStandStateTimer { 0.0f };
- float _squatTimer { 0.0f };
float _tippingPoint { _userHeight.get() };
// load avatar scripts once when rig is ready
@@ -3012,7 +3063,8 @@ private:
Setting::Handle _controlSchemeIndexSetting;
std::vector> _avatarEntityIDSettings;
std::vector> _avatarEntityDataSettings;
- Setting::Handle _userRecenterModelSetting;
+ Setting::Handle _allowAvatarStandingPreferenceSetting;
+ Setting::Handle _allowAvatarLeaningPreferenceSetting;
// AvatarEntities stuff:
// We cache the "map of unfortunately-formatted-binary-blobs" because they are expensive to compute
diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp
index 997dcfe685..0d382934b8 100755
--- a/interface/src/avatar/MyCharacterController.cpp
+++ b/interface/src/avatar/MyCharacterController.cpp
@@ -26,7 +26,9 @@ void MyCharacterController::RayShotgunResult::reset() {
walkable = true;
}
-MyCharacterController::MyCharacterController(std::shared_ptr avatar) {
+MyCharacterController::MyCharacterController(std::shared_ptr avatar,
+ const FollowTimePerType& followTimeRemainingPerType) :
+ CharacterController(followTimeRemainingPerType) {
assert(avatar);
_avatar = avatar;
diff --git a/interface/src/avatar/MyCharacterController.h b/interface/src/avatar/MyCharacterController.h
index eefcc92637..b25c2412a0 100644
--- a/interface/src/avatar/MyCharacterController.h
+++ b/interface/src/avatar/MyCharacterController.h
@@ -23,7 +23,7 @@ class DetailedMotionState;
class MyCharacterController : public CharacterController {
public:
- explicit MyCharacterController(std::shared_ptr avatar);
+ explicit MyCharacterController(std::shared_ptr avatar, const FollowTimePerType& followTimeRemainingPerType);
~MyCharacterController ();
void addToWorld() override;
diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp
index 6fe199aaba..4984c1d335 100755
--- a/interface/src/avatar/MySkeletonModel.cpp
+++ b/interface/src/avatar/MySkeletonModel.cpp
@@ -65,13 +65,21 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) {
return result;
}
+ // Use the center-of-gravity model if the user and the avatar are standing, unless flying or walking.
+ // If artificial standing is disabled, use center-of-gravity regardless of the user's sit/stand state.
+ bool useCenterOfGravityModel =
+ myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !myAvatar->getIsInWalkingState() &&
+ (!myAvatar->getHMDCrouchRecenterEnabled() || !myAvatar->getIsInSittingState()) &&
+ myAvatar->getHMDLeanRecenterEnabled() &&
+ (myAvatar->getAllowAvatarLeaningPreference() != MyAvatar::AllowAvatarLeaningPreference::AlwaysNoRecenter);
+
glm::mat4 hipsMat;
- if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !(myAvatar->getIsInWalkingState()) && !(myAvatar->getIsInSittingState()) && myAvatar->getHMDLeanRecenterEnabled()) {
+ if (useCenterOfGravityModel) {
// then we use center of gravity model
hipsMat = myAvatar->deriveBodyUsingCgModel();
} else {
// otherwise use the default of putting the hips under the head
- hipsMat = myAvatar->deriveBodyFromHMDSensor();
+ hipsMat = myAvatar->deriveBodyFromHMDSensor(true);
}
glm::vec3 hipsPos = extractTranslation(hipsMat);
glm::quat hipsRot = glmExtractRotation(hipsMat);
@@ -82,7 +90,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) {
// dampen hips rotation, by mixing it with the avatar orientation in sensor space
// turning this off for center of gravity model because it is already mixed in there
- if (!(myAvatar->getCenterOfGravityModelEnabled())) {
+ if (!useCenterOfGravityModel) {
const float MIX_RATIO = 0.5f;
hipsRot = safeLerp(glmExtractRotation(avatarToSensorMat), hipsRot, MIX_RATIO);
}
diff --git a/interface/src/ui/AnimStats.cpp b/interface/src/ui/AnimStats.cpp
index 2a355e48d1..fff69cb1c0 100644
--- a/interface/src/ui/AnimStats.cpp
+++ b/interface/src/ui/AnimStats.cpp
@@ -67,13 +67,13 @@ void AnimStats::updateStats(bool force) {
// print if we are recentering or not.
_recenterText = "Recenter: ";
- if (myAvatar->isFollowActive(MyAvatar::FollowHelper::Rotation)) {
+ if (myAvatar->isFollowActive(CharacterController::FollowType::Rotation)) {
_recenterText += "Rotation ";
}
- if (myAvatar->isFollowActive(MyAvatar::FollowHelper::Horizontal)) {
+ if (myAvatar->isFollowActive(CharacterController::FollowType::Horizontal)) {
_recenterText += "Horizontal ";
}
- if (myAvatar->isFollowActive(MyAvatar::FollowHelper::Vertical)) {
+ if (myAvatar->isFollowActive(CharacterController::FollowType::Vertical)) {
_recenterText += "Vertical ";
}
emit recenterTextChanged();
diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp
index 79d9ebaa5c..9c53060f31 100644
--- a/interface/src/ui/PreferencesDialog.cpp
+++ b/interface/src/ui/PreferencesDialog.cpp
@@ -422,40 +422,40 @@ void setupPreferences() {
preferences->addPreference(preference);
}
{
- auto getter = [myAvatar]()->int {
- switch (myAvatar->getUserRecenterModel()) {
- case MyAvatar::SitStandModelType::Auto:
- default:
- return 0;
- case MyAvatar::SitStandModelType::ForceSit:
- return 1;
- case MyAvatar::SitStandModelType::ForceStand:
- return 2;
- case MyAvatar::SitStandModelType::DisableHMDLean:
- return 3;
- }
+ IntPreference::Getter getter = [myAvatar]() -> int {
+ return static_cast(myAvatar->getAllowAvatarStandingPreference());
};
- auto setter = [myAvatar](int value) {
- switch (value) {
- case 0:
- default:
- myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto);
- break;
- case 1:
- myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit);
- break;
- case 2:
- myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceStand);
- break;
- case 3:
- myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean);
- break;
- }
+
+ IntPreference::Setter setter = [myAvatar](const int& value) {
+ myAvatar->setAllowAvatarStandingPreference(static_cast(value));
};
- auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Force Stand / Disable Recenter", getter, setter);
+
+ auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Allow my avatar to stand", getter, setter);
QStringList items;
- items << "Auto - turns on avatar leaning when standing in real world" << "Seated - disables all avatar leaning while sitting in real world" << "Standing - enables avatar leaning while sitting in real world" << "Disabled - allows avatar sitting on the floor [Experimental]";
- preference->setHeading("Avatar leaning behavior");
+ items << "When I'm standing"
+ << "Always"; // Must match the order in MyAvatar::AllowAvatarStandingPreference.
+ assert(items.size() == static_cast(MyAvatar::AllowAvatarStandingPreference::Count));
+ preference->setHeading("Allow my avatar to stand:");
+ preference->setItems(items);
+ preferences->addPreference(preference);
+ }
+ {
+ IntPreference::Getter getter = [myAvatar]() -> int {
+ return static_cast(myAvatar->getAllowAvatarLeaningPreference());
+ };
+
+ IntPreference::Setter setter = [myAvatar](const int& value) {
+ myAvatar->setAllowAvatarLeaningPreference(static_cast(value));
+ };
+
+ auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Allow my avatar to lean", getter, setter);
+ QStringList items;
+ items << "When I'm standing"
+ << "Always"
+ << "Never"
+ << "Always, no recenter (Experimental)"; // Must match the order in MyAvatar::AllowAvatarLeaningPreference.
+ assert(items.size() == static_cast(MyAvatar::AllowAvatarLeaningPreference::Count));
+ preference->setHeading("Allow my avatar to lean:");
preference->setItems(items);
preferences->addPreference(preference);
}
diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp
index 06fe558964..6dc378a32f 100644
--- a/libraries/animation/src/Rig.cpp
+++ b/libraries/animation/src/Rig.cpp
@@ -1855,6 +1855,16 @@ glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const HFMJoin
return position;
}
+// Get the scale factor to convert distances in the geometry frame into the unscaled rig frame.
+// Typically it will be the unit conversion from cm to m.
+float Rig::GetScaleFactorGeometryToUnscaledRig() const {
+ // Normally the model offset transform will contain the avatar scale factor; we explicitly remove it here.
+ AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), getModelOffsetPose().rot(), getModelOffsetPose().trans());
+ AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * getGeometryOffsetPose();
+
+ return geomToRigWithoutAvatarScale.scale().x; // in practice this is always a uniform scale factor.
+}
+
void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool hipsEstimated,
bool leftArmEnabled, bool rightArmEnabled, bool headEnabled, float dt,
const AnimPose& leftHandPose, const AnimPose& rightHandPose,
@@ -2703,10 +2713,10 @@ void Rig::computeAvatarBoundingCapsule(
Extents totalExtents;
totalExtents.reset();
- // HACK by convention our Avatars are always modeled such that y=0 is the ground plane.
- // add the zero point so that our avatars will always have bounding volumes that are flush with the ground
+ // HACK by convention our Avatars are always modeled such that y=0 (GEOMETRY_GROUND_Y) is the ground plane.
+ // add the ground point so that our avatars will always have bounding volumes that are flush with the ground
// even if they do not have legs (default robot)
- totalExtents.addPoint(glm::vec3(0.0f));
+ totalExtents.addPoint(glm::vec3(0.0f, GEOMETRY_GROUND_Y, 0.0f));
// To reduce the radius of the bounding capsule to be tight with the torso, we only consider joints
// from the head to the hips when computing the rest of the bounding capsule.
@@ -2747,24 +2757,20 @@ void Rig::initFlow(bool isActive) {
}
}
+// Get the vertical position of eye joints, in the rig coordinate frame, ignoring the avatar scale.
float Rig::getUnscaledEyeHeight() const {
// Normally the model offset transform will contain the avatar scale factor, we explicitly remove it here.
AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), getModelOffsetPose().rot(), getModelOffsetPose().trans());
AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * getGeometryOffsetPose();
- // This factor can be used to scale distances in the geometry frame into the unscaled rig frame.
- // Typically it will be the unit conversion from cm to m.
- float scaleFactor = geomToRigWithoutAvatarScale.scale().x; // in practice this always a uniform scale factor.
+ // Factor to scale distances in the geometry frame into the unscaled rig frame.
+ float scaleFactor = GetScaleFactorGeometryToUnscaledRig();
int headTopJoint = indexOfJoint("HeadTop_End");
int headJoint = indexOfJoint("Head");
int eyeJoint = indexOfJoint("LeftEye") != -1 ? indexOfJoint("LeftEye") : indexOfJoint("RightEye");
int toeJoint = indexOfJoint("LeftToeBase") != -1 ? indexOfJoint("LeftToeBase") : indexOfJoint("RightToeBase");
- // Makes assumption that the y = 0 plane in geometry is the ground plane.
- // We also make that assumption in Rig::computeAvatarBoundingCapsule()
- const float GROUND_Y = 0.0f;
-
// Values from the skeleton are in the geometry coordinate frame.
auto skeleton = getAnimSkeleton();
if (eyeJoint >= 0 && toeJoint >= 0) {
@@ -2772,8 +2778,8 @@ float Rig::getUnscaledEyeHeight() const {
float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y;
return scaleFactor * eyeHeight;
} else if (eyeJoint >= 0) {
- // Measure Eye joint to y = 0 plane.
- float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - GROUND_Y;
+ // Measure Eye joint to ground plane.
+ float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - GEOMETRY_GROUND_Y;
return scaleFactor * eyeHeight;
} else if (headTopJoint >= 0 && toeJoint >= 0) {
// Measure from ToeBase joint to HeadTop_End joint, then remove forehead distance.
@@ -2783,19 +2789,36 @@ float Rig::getUnscaledEyeHeight() const {
} else if (headTopJoint >= 0) {
// Measure from HeadTop_End joint to the ground, then remove forehead distance.
const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT;
- float headHeight = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - GROUND_Y;
+ float headHeight = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - GEOMETRY_GROUND_Y;
return scaleFactor * (headHeight - headHeight * ratio);
} else if (headJoint >= 0) {
// Measure Head joint to the ground, then add in distance from neck to eye.
const float DEFAULT_AVATAR_NECK_TO_EYE = DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
const float ratio = DEFAULT_AVATAR_NECK_TO_EYE / DEFAULT_AVATAR_NECK_HEIGHT;
- float neckHeight = skeleton->getAbsoluteDefaultPose(headJoint).trans().y - GROUND_Y;
+ float neckHeight = skeleton->getAbsoluteDefaultPose(headJoint).trans().y - GEOMETRY_GROUND_Y;
return scaleFactor * (neckHeight + neckHeight * ratio);
} else {
return DEFAULT_AVATAR_EYE_HEIGHT;
}
}
+// Get the vertical position of the hips joint, in the rig coordinate frame, ignoring the avatar scale.
+float Rig::getUnscaledHipsHeight() const {
+ // This factor can be used to scale distances in the geometry frame into the unscaled rig frame.
+ float scaleFactor = GetScaleFactorGeometryToUnscaledRig();
+
+ int hipsJoint = indexOfJoint("Hips");
+
+ // Values from the skeleton are in the geometry coordinate frame.
+ if (hipsJoint >= 0) {
+ // Measure hip joint to ground plane.
+ float hipsHeight = getAnimSkeleton()->getAbsoluteDefaultPose(hipsJoint).trans().y - GEOMETRY_GROUND_Y;
+ return scaleFactor * hipsHeight;
+ } else {
+ return DEFAULT_AVATAR_HIPS_HEIGHT;
+ }
+}
+
void Rig::setDirectionalBlending(const QString& targetName, const glm::vec3& blendingTarget, const QString& alphaName, float alpha) {
_animVars.set(targetName, blendingTarget);
_animVars.set(alphaName, alpha);
diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h
index 60a2602316..c58be799cf 100644
--- a/libraries/animation/src/Rig.h
+++ b/libraries/animation/src/Rig.h
@@ -251,6 +251,7 @@ public:
Flow& getFlow() { return _internalFlow; }
float getUnscaledEyeHeight() const;
+ float getUnscaledHipsHeight() const;
void buildAbsoluteRigPoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePosesOut) const;
int getOverrideJointCount() const;
@@ -287,6 +288,11 @@ protected:
glm::vec3 deflectHandFromTorso(const glm::vec3& handPosition, const HFMJointShapeInfo& hipsShapeInfo, const HFMJointShapeInfo& spineShapeInfo,
const HFMJointShapeInfo& spine1ShapeInfo, const HFMJointShapeInfo& spine2ShapeInfo) const;
+ // Get the scale factor to convert distances in the geometry frame into the unscaled rig frame.
+ float GetScaleFactorGeometryToUnscaledRig() const;
+
+ // The ground plane Y position in geometry space.
+ static constexpr float GEOMETRY_GROUND_Y = 0.0f;
AnimPose _modelOffset; // model to rig space
AnimPose _geometryOffset; // geometry to model space (includes unit offset & fst offsets)
diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp
index e222692aea..777a3d3f87 100755
--- a/libraries/physics/src/CharacterController.cpp
+++ b/libraries/physics/src/CharacterController.cpp
@@ -107,12 +107,12 @@ CharacterController::CharacterMotor::CharacterMotor(const glm::vec3& vel, const
static uint32_t _numCharacterControllers { 0 };
-CharacterController::CharacterController() {
+CharacterController::CharacterController(const FollowTimePerType& followTimeRemainingPerType) :
+ _followTimeRemainingPerType(followTimeRemainingPerType) {
_floorDistance = _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT;
_targetVelocity.setValue(0.0f, 0.0f, 0.0f);
_followDesiredBodyTransform.setIdentity();
- _followTimeRemaining = 0.0f;
_state = State::Hover;
_isPushingUp = false;
_rayHitStartTime = 0;
@@ -350,64 +350,103 @@ void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar
btVector3 velocity = _rigidBody->getLinearVelocity() - _parentVelocity;
computeNewVelocity(dt, velocity);
- const float MINIMUM_TIME_REMAINING = 0.005f;
- const float MAX_DISPLACEMENT = 0.5f * _radius;
- _followTimeRemaining -= dt;
- if (_followTimeRemaining >= MINIMUM_TIME_REMAINING) {
- btTransform bodyTransform = _rigidBody->getWorldTransform();
+ constexpr float MINIMUM_TIME_REMAINING = 0.005f;
+ static_assert(FOLLOW_TIME_IMMEDIATE_SNAP > MINIMUM_TIME_REMAINING, "The code below assumes this condition is true.");
+ bool hasFollowTimeRemaining = false;
+ for (float followTime : _followTimeRemainingPerType) {
+ if (followTime > MINIMUM_TIME_REMAINING) {
+ hasFollowTimeRemaining = true;
+ break;
+ }
+ }
+
+ if (hasFollowTimeRemaining) {
+ const float MAX_DISPLACEMENT = 0.5f * _radius;
+
+ btTransform bodyTransform = _rigidBody->getWorldTransform();
btVector3 startPos = bodyTransform.getOrigin();
btVector3 deltaPos = _followDesiredBodyTransform.getOrigin() - startPos;
- btVector3 vel = deltaPos / _followTimeRemaining;
- btVector3 linearDisplacement = clampLength(vel * dt, MAX_DISPLACEMENT); // clamp displacement to prevent tunneling.
+
+ btVector3 linearDisplacement(0.0f, 0.0f, 0.0f);
+ {
+ float horizontalTime = _followTimeRemainingPerType[static_cast(FollowType::Horizontal)];
+ float verticalTime = _followTimeRemainingPerType[static_cast(FollowType::Vertical)];
+
+ if (horizontalTime == FOLLOW_TIME_IMMEDIATE_SNAP) {
+ linearDisplacement.setX(deltaPos.x());
+ linearDisplacement.setZ(deltaPos.z());
+ } else if (horizontalTime > MINIMUM_TIME_REMAINING) {
+ linearDisplacement.setX((deltaPos.x() * dt) / horizontalTime);
+ linearDisplacement.setZ((deltaPos.z() * dt) / horizontalTime);
+ }
+
+ if (verticalTime == FOLLOW_TIME_IMMEDIATE_SNAP) {
+ linearDisplacement.setY(deltaPos.y());
+ } else if (verticalTime > MINIMUM_TIME_REMAINING) {
+ linearDisplacement.setY((deltaPos.y() * dt) / verticalTime);
+ }
+
+ linearDisplacement = clampLength(linearDisplacement, MAX_DISPLACEMENT); // clamp displacement to prevent tunneling.
+ }
+
btVector3 endPos = startPos + linearDisplacement;
// resolve the simple linearDisplacement
_followLinearDisplacement += linearDisplacement;
// now for the rotational part...
+
btQuaternion startRot = bodyTransform.getRotation();
- btQuaternion desiredRot = _followDesiredBodyTransform.getRotation();
// startRot as default rotation
btQuaternion endRot = startRot;
- // the dot product between two quaternions is equal to +/- cos(angle/2)
- // where 'angle' is that of the rotation between them
- float qDot = desiredRot.dot(startRot);
+ float rotationTime = _followTimeRemainingPerType[static_cast(FollowType::Rotation)];
+ if (rotationTime > MINIMUM_TIME_REMAINING) {
+ btQuaternion desiredRot = _followDesiredBodyTransform.getRotation();
- // when the abs() value of the dot product is approximately 1.0
- // then the two rotations are effectively adjacent
- const float MIN_DOT_PRODUCT_OF_ADJACENT_QUATERNIONS = 0.99999f; // corresponds to approx 0.5 degrees
- if (fabsf(qDot) < MIN_DOT_PRODUCT_OF_ADJACENT_QUATERNIONS) {
- if (qDot < 0.0f) {
- // the quaternions are actually on opposite hyperhemispheres
- // so we move one to agree with the other and negate qDot
- desiredRot = -desiredRot;
- qDot = -qDot;
+ // the dot product between two quaternions is equal to +/- cos(angle/2)
+ // where 'angle' is that of the rotation between them
+ float qDot = desiredRot.dot(startRot);
+
+ // when the abs() value of the dot product is approximately 1.0
+ // then the two rotations are effectively adjacent
+ const float MIN_DOT_PRODUCT_OF_ADJACENT_QUATERNIONS = 0.99999f; // corresponds to approx 0.5 degrees
+ if (fabsf(qDot) < MIN_DOT_PRODUCT_OF_ADJACENT_QUATERNIONS) {
+ if (qDot < 0.0f) {
+ // the quaternions are actually on opposite hyperhemispheres
+ // so we move one to agree with the other and negate qDot
+ desiredRot = -desiredRot;
+ qDot = -qDot;
+ }
+ btQuaternion deltaRot = desiredRot * startRot.inverse();
+
+ // the axis is the imaginary part, but scaled by sin(angle/2)
+ btVector3 axis(deltaRot.getX(), deltaRot.getY(), deltaRot.getZ());
+ axis /= sqrtf(1.0f - qDot * qDot);
+
+ // compute the angle we will resolve for this dt, but don't overshoot
+ float angle = 2.0f * acosf(qDot);
+
+ if (rotationTime != FOLLOW_TIME_IMMEDIATE_SNAP) {
+ if (dt < rotationTime) {
+ angle *= dt / rotationTime;
+ }
+ }
+
+ // accumulate rotation
+ deltaRot = btQuaternion(axis, angle);
+ _followAngularDisplacement = (deltaRot * _followAngularDisplacement).normalize();
+
+ // in order to accumulate displacement of avatar position, we need to take _shapeLocalOffset into account.
+ btVector3 shapeLocalOffset = glmToBullet(_shapeLocalOffset);
+
+ endRot = deltaRot * startRot;
+ btVector3 swingDisplacement =
+ rotateVector(endRot, -shapeLocalOffset) - rotateVector(startRot, -shapeLocalOffset);
+ _followLinearDisplacement += swingDisplacement;
}
- btQuaternion deltaRot = desiredRot * startRot.inverse();
-
- // the axis is the imaginary part, but scaled by sin(angle/2)
- btVector3 axis(deltaRot.getX(), deltaRot.getY(), deltaRot.getZ());
- axis /= sqrtf(1.0f - qDot * qDot);
-
- // compute the angle we will resolve for this dt, but don't overshoot
- float angle = 2.0f * acosf(qDot);
- if (dt < _followTimeRemaining) {
- angle *= dt / _followTimeRemaining;
- }
-
- // accumulate rotation
- deltaRot = btQuaternion(axis, angle);
- _followAngularDisplacement = (deltaRot * _followAngularDisplacement).normalize();
-
- // in order to accumulate displacement of avatar position, we need to take _shapeLocalOffset into account.
- btVector3 shapeLocalOffset = glmToBullet(_shapeLocalOffset);
-
- endRot = deltaRot * startRot;
- btVector3 swingDisplacement = rotateVector(endRot, -shapeLocalOffset) - rotateVector(startRot, -shapeLocalOffset);
- _followLinearDisplacement += swingDisplacement;
}
_rigidBody->setWorldTransform(btTransform(endRot, endPos));
}
@@ -606,8 +645,7 @@ void CharacterController::setParentVelocity(const glm::vec3& velocity) {
_parentVelocity = glmToBullet(velocity);
}
-void CharacterController::setFollowParameters(const glm::mat4& desiredWorldBodyMatrix, float timeRemaining) {
- _followTimeRemaining = timeRemaining;
+void CharacterController::setFollowParameters(const glm::mat4& desiredWorldBodyMatrix) {
_followDesiredBodyTransform = glmToBullet(desiredWorldBodyMatrix) * btTransform(btQuaternion::getIdentity(), glmToBullet(_shapeLocalOffset));
}
diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h
index e7ad3ddfa8..8242ae4b97 100755
--- a/libraries/physics/src/CharacterController.h
+++ b/libraries/physics/src/CharacterController.h
@@ -53,7 +53,20 @@ const btScalar MIN_CHARACTER_MOTOR_TIMESCALE = 0.05f;
class CharacterController : public btCharacterControllerInterface {
public:
- CharacterController();
+ enum class FollowType : uint8_t {
+ Rotation,
+ Horizontal,
+ Vertical,
+ Count
+ };
+
+ // Remaining follow time for each FollowType
+ typedef std::array(FollowType::Count)> FollowTimePerType;
+
+ // Follow time value meaning that we should snap immediately to the target.
+ static constexpr float FOLLOW_TIME_IMMEDIATE_SNAP = FLT_MAX;
+
+ CharacterController(const FollowTimePerType& followTimeRemainingPerType);
virtual ~CharacterController();
bool needsRemoval() const;
bool needsAddition() const;
@@ -99,7 +112,8 @@ public:
void getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const;
void setParentVelocity(const glm::vec3& parentVelocity);
- void setFollowParameters(const glm::mat4& desiredWorldMatrix, float timeRemaining);
+
+ void setFollowParameters(const glm::mat4& desiredWorldMatrix);
float getFollowTime() const { return _followTime; }
glm::vec3 getFollowLinearDisplacement() const;
glm::quat getFollowAngularDisplacement() const;
@@ -144,7 +158,7 @@ public:
void setPendingFlagsUpdateCollisionMask(){ _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK; }
void setSeated(bool isSeated) { _isSeated = isSeated; }
- bool getSeated() { return _isSeated; }
+ bool getSeated() const { return _isSeated; }
void resetStuckCounter() { _numStuckSubsteps = 0; }
@@ -178,7 +192,7 @@ protected:
btVector3 _preSimulationVelocity;
btVector3 _velocityChange;
btTransform _followDesiredBodyTransform;
- btScalar _followTimeRemaining;
+ const FollowTimePerType& _followTimeRemainingPerType;
btTransform _characterBodyTransform;
btVector3 _position;
btQuaternion _rotation;
diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h
index 4a79f6b487..a25142b2ad 100644
--- a/libraries/shared/src/AvatarConstants.h
+++ b/libraries/shared/src/AvatarConstants.h
@@ -20,6 +20,7 @@ const float DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD = 0.11f; // meters
const float DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD = 0.185f; // meters
const float DEFAULT_AVATAR_NECK_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD;
const float DEFAULT_AVATAR_EYE_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
+const float DEFAULT_AVATAR_HIPS_HEIGHT = 1.01327407f; // meters
const float DEFAULT_SPINE2_SPLINE_PROPORTION = 0.71f;
const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT = -0.25f;
const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT = 0.25f;