diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b3231b906d..e5413105f8 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 @@ -487,14 +497,17 @@ void MyAvatar::resetSensorsAndBody() { reset(true, false, true); } -void MyAvatar::centerBody() { +// forceFollowYPos: true to force the body matrix to be affected by the HMD's +// vertical position, even if crouch recentering is disabled. +void MyAvatar::centerBody(const bool forceFollowYPos) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "centerBody"); return; } // 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 +584,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() && 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 { - // 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); } } @@ -692,9 +704,9 @@ void MyAvatar::update(float deltaTime) { } 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)) { + if (getHMDCrouchRecenterEnabled() && + getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && + (angleSpine2 > COSINE_THIRTY_DEGREES)) { _squatTimer += deltaTime; if (_squatTimer > SQUATTY_TIMEOUT) { @@ -832,7 +844,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 +1289,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 +1327,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 +2023,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 +2688,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 +2708,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) { @@ -3793,54 +3790,115 @@ 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; + glm::vec3 forward; + glm::vec3 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; + const int movementReference = getMovementReference(); + CameraMode cameraMode = qApp->getCamera().getMode(); + + bool vectorsAreInAvatarFrame = true; + bool removeLocalYComponent = false; + + const bool HMDHandRelativeMovement = + qApp->isHMDMode() && (movementReference == LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE || + movementReference == LocomotionRelativeMovementMode::MOVEMENT_HAND_RELATIVE_LEVELED); + + const bool desktopLookatOrSelfieMode = + !qApp->isHMDMode() && (cameraMode == CAMERA_MODE_FIRST_PERSON_LOOK_AT || cameraMode == CAMERA_MODE_LOOK_AT || + cameraMode == CAMERA_MODE_SELFIE); + + const bool hoveringOrCollisionless = _characterController.getState() == CharacterController::State::Hover || + _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS; + + if (HMDHandRelativeMovement) { + const controller::Action directionHand = + (getDominantHand() == DOMINANT_RIGHT_HAND) ? controller::Action::LEFT_HAND : controller::Action::RIGHT_HAND; + const controller::Pose handPoseInAvatarFrame = getControllerPoseInAvatarFrame(directionHand); + + if (handPoseInAvatarFrame.isValid()) { + const glm::vec3 controllerForward(0.0f, 1.0f, 0.0f); + const 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. + const 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; } @@ -4724,7 +4782,9 @@ void MyAvatar::triggerRotationRecenter() { } // 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); @@ -4760,8 +4820,30 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { float invSensorToWorldScale = getUserEyeHeight() / getEyeHeight(); glm::vec3 bodyPos = headPosition + invSensorToWorldScale * (headToNeck + neckToRoot); + glm::quat bodyQuat; - return createMatFromQuatAndPos(headOrientationYawOnly, bodyPos); + const controller::Pose hipsControllerPose = getControllerPoseInSensorFrame(controller::Action::HIPS); + if (hipsControllerPose.isValid()) { + const glm::quat hipsOrientation = hipsControllerPose.rotation * Quaternions::Y_180; + const glm::quat hipsOrientationYawOnly = cancelOutRollAndPitch(hipsOrientation); + + const 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. + bodyPos.y = rig.getUnscaledHipsHeight(); + } + + glm::mat4 bodyMat = createMatFromQuatAndPos(bodyQuat, bodyPos); + + return bodyMat; } glm::mat4 MyAvatar::getSpine2RotationRigSpace() const { @@ -5159,16 +5241,19 @@ bool MyAvatar::getIsInWalkingState() const { return _isInWalkingState; } +// Determine if the user is sitting in the real world. bool MyAvatar::getIsInSittingState() const { return _isInSittingState.get(); } -MyAvatar::SitStandModelType MyAvatar::getUserRecenterModel() const { - return _userRecenterModel.get(); +// Get the user preference of when MyAvatar may stand. +MyAvatar::AllowAvatarStandingPreference MyAvatar::getAllowAvatarStandingPreference() const { + return _allowAvatarStandingPreference.get(); } -bool MyAvatar::getIsSitStandStateLocked() const { - return _lockSitStandState.get(); +// Get the user preference of when MyAvatar may lean. +MyAvatar::AllowAvatarLeaningPreference MyAvatar::getAllowAvatarLeaningPreference() const { + return _allowAvatarLeaningPreference.get(); } float MyAvatar::getWalkSpeed() const { @@ -5221,59 +5306,29 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { _isInWalkingState = isWalking; } +// Specify whether the user is sitting or standing in the real world. void MyAvatar::setIsInSittingState(bool isSitting) { _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); - } + setCenterOfGravityModelEnabled(!isSitting); setSitStandStateChange(true); } -void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { +// Set the user preference of when the avatar may stand. +void MyAvatar::setAllowAvatarStandingPreference(const MyAvatar::AllowAvatarStandingPreference preference) { + _allowAvatarStandingPreference.set(preference); - _userRecenterModel.set(modelName); - - switch (modelName) { - case MyAvatar::SitStandModelType::ForceSit: - setHMDLeanRecenterEnabled(true); - setIsInSittingState(true); - setIsSitStandStateLocked(true); - break; - case MyAvatar::SitStandModelType::ForceStand: - setHMDLeanRecenterEnabled(true); - setIsInSittingState(false); - setIsSitStandStateLocked(true); - break; - case MyAvatar::SitStandModelType::Auto: - default: - setHMDLeanRecenterEnabled(true); - setIsInSittingState(false); - setIsSitStandStateLocked(false); - break; - case MyAvatar::SitStandModelType::DisableHMDLean: - setHMDLeanRecenterEnabled(false); - setIsInSittingState(false); - setIsSitStandStateLocked(false); - break; - } + // Set the correct vertical position for the avatar body relative to the HMD, + // according to the newly-selected avatar standing preference. + centerBody(false); } -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); - } +// 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 +5457,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 +5556,82 @@ MyAvatar::FollowHelper::FollowHelper() { } void MyAvatar::FollowHelper::deactivate() { - for (int i = 0; i < NumFollowTypes; i++) { - deactivate((FollowType)i); + for (uint i = 0, e = static_cast(CharacterController::FollowType::Count); i < e; ++i) { + deactivate((CharacterController::FollowType)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, e = static_cast(CharacterController::FollowType::Count); i < e; ++i) { + if (isActive((CharacterController::FollowType)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 (uint i = 0, e = static_cast(CharacterController::FollowType::Count); i < e; ++i) { + if (_timeRemaining[i] == CharacterController::FOLLOW_TIME_IMMEDIATE_SNAP) { + _timeRemaining[i] = 0.f; + } + else { + _timeRemaining[i] -= 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 { + shouldSnapOut = false; + + // 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; } -} -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 { + if (!myAvatar.isAllowedToLean()) { + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); + if (!withinBaseOfSupport(currentHeadPose)) { + 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 +5641,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; @@ -5583,50 +5653,60 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, 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(); - 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; + + if (!withinBaseOfSupport(currentHeadPose)) { + 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); + float myScale = myAvatar.getAvatarScale(); + + // 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(), myScale) && + handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) && + handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) && + headVelocityGreaterThanThreshold(currentHeadPose) && + 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)))) { + myAvatar.setResetMode(true); + stepDetected = true; + } + } + + if (stepDetected) { + if (glm::length(currentHeadPose.velocity) > DEFAULT_AVATAR_WALK_SPEED_THRESHOLD) { + myAvatar.setIsInWalkingState(true); + } + } + return stepDetected; } -bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { +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 +5718,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; @@ -5657,51 +5734,80 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co 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) { + const bool feetAreTracked = myAvatar.areFeetTracked(); - 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()); - } + if (myAvatar.getHMDLeanRecenterEnabled()) { + + // 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()); } + } + + // Horizontal and rotation recenter + + if ((feetAreTracked || getForceActivateHorizontal()) && !isActive(CharacterController::FollowType::Horizontal)) { + activate(CharacterController::FollowType::Horizontal, feetAreTracked); + 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) { + if (myAvatar.getCenterOfGravityModelEnabled()) { + if (!isActive(CharacterController::FollowType::Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { + activate(CharacterController::FollowType::Horizontal, false); + if (myAvatar.getEnableStepResetRotation()) { + activate(CharacterController::FollowType::Rotation, false); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); + } + } + } else { + // center of gravity model is not enabled + if (!isActive(CharacterController::FollowType::Horizontal) && + (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { + activate(CharacterController::FollowType::Horizontal, false); + if (myAvatar.getEnableStepResetRotation() && !myAvatar.getIsInSittingState()) { + activate(CharacterController::FollowType::Rotation, false); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); + } + } } } } - 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); + if (_squatDetected) { + _squatDetected = 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 +5827,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,11 +5861,15 @@ 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 { return currentBodyMatrix; @@ -6127,7 +6237,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 +6693,7 @@ void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) { setHMDLeanRecenterEnabled(false); // Disable movement setSitDriveKeysStatus(false); - centerBody(); + centerBody(true); int hipIndex = getJointIndex("Hips"); clearPinOnJoint(hipIndex); pinJoint(hipIndex, position, rotation); @@ -6601,7 +6711,7 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { _characterController.setSeated(false); setCollisionsEnabled(true); setHMDLeanRecenterEnabled(true); - centerBody(); + centerBody(false); slamPosition(position); setWorldOrientation(rotation); @@ -6906,6 +7016,30 @@ 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 the feet are under direct control (tracked). +bool MyAvatar::areFeetTracked() const { + // Foot tracking only activates when both feet are tracked, so we only need to test one. + return getControllerPoseInSensorFrame(controller::Action::LEFT_FOOT).isValid(); +} + +// Determine if the hips are under direct control (tracked). +bool MyAvatar::areHipsTracked() const { + return getControllerPoseInSensorFrame(controller::Action::HIPS).isValid(); +} + +// 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) && !areFeetTracked()); +} + 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..9e817b5167 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -282,16 +282,11 @@ class MyAvatar : public Avatar { *

Warning: Setting this value also sets the value of analogPlusSprintSpeed to twice * 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. - * @property {boolean} isSitStandStateLocked - true to lock the avatar sitting/standing state, i.e., use this - * to disable automatically changing state. + * @property {number} 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} allowTeleporting - true if teleporting is enabled in the Interface settings, * false if it isn't. Read-only. * @@ -413,8 +408,6 @@ 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(bool allowTeleporting READ getAllowTeleporting) const QString DOMINANT_LEFT_HAND = "left"; @@ -549,6 +542,31 @@ 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(); @@ -576,7 +594,7 @@ public: * the HMD. * @function MyAvatar.centerBody */ - Q_INVOKABLE void centerBody(); // thread-safe + Q_INVOKABLE void centerBody(const bool forceFollowYPos); // thread-safe /**jsdoc @@ -1417,7 +1435,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; @@ -1709,7 +1726,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 +1770,10 @@ 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 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 +2006,10 @@ public: glm::vec3 getLookAtPivotPoint(); glm::vec3 getCameraEyesPosition(float deltaTime); bool isJumping(); + bool getHMDCrouchRecenterEnabled() const; + bool isAllowedToLean() const; + bool areFeetTracked() const; + bool areHipsTracked() const; public slots: @@ -2841,23 +2862,15 @@ 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; @@ -2880,7 +2893,7 @@ private: FollowHelper _follow; - bool isFollowActive(FollowHelper::FollowType followType) const; + bool isFollowActive(CharacterController::FollowType followType) const; bool _goToPending { false }; bool _physicsSafetyPending { false }; @@ -2922,6 +2935,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 +2969,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,7 +2984,12 @@ 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() }; @@ -3012,7 +3032,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..2d33ee35ca 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -65,13 +65,20 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { return result; } + const bool useCenterOfGravityModel = + myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !myAvatar->getIsInWalkingState() && + !myAvatar->getIsInSittingState() && myAvatar->getHMDLeanRecenterEnabled() && + (myAvatar->getAllowAvatarLeaningPreference() != MyAvatar::AllowAvatarLeaningPreference::AlwaysNoRecenter) && + myAvatar->getHMDCrouchRecenterEnabled(); + 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 { + } + 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 +89,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..12615426f1 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; - } + const 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; - } + + const 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); + } + { + const IntPreference::Getter getter = [myAvatar]() -> int { + return static_cast(myAvatar->getAllowAvatarLeaningPreference()); + }; + + const 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..bcdef45440 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.f, GEOMETRY_GROUND_Y, 0.f)); // 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. + const float scaleFactor = GetScaleFactorGeometryToUnscaledRig(); + + const 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..d14e59dbd2 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; @@ -351,66 +351,108 @@ void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar 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(); + 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, 0, 0); + { + linearDisplacement.setZero(); + + const float horizontalTime = _followTimeRemainingPerType[static_cast(FollowType::Horizontal)]; + const 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); + const 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)); } + _followTime += dt; if (_steppingUp) { @@ -606,8 +648,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..899c99890b 100755 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -53,7 +53,21 @@ 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 +113,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 +159,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 +193,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;