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.

* * * @@ -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.0051000.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;
ValueNameDescription