diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index dd25aa4c4b..49b4b1ced4 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -168,7 +168,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { QList avatarList; std::unordered_map avatarDataToNodes; - int listItem = 0; std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) { const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); @@ -176,7 +175,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { // but not have yet sent data that's linked to the node. Check for that case and don't // consider those nodes. if (otherNodeData) { - listItem++; AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer(); avatarList << otherAvatar; avatarDataToNodes[otherAvatar] = otherNode; @@ -185,8 +183,8 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer(); ViewFrustum cameraView = nodeData->getViewFrustom(); - std::priority_queue sortedAvatars = AvatarData::sortAvatars( - avatarList, cameraView, + std::priority_queue sortedAvatars; + AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars, [&](AvatarSharedPointer avatar)->uint64_t{ auto avatarNode = avatarDataToNodes[avatar]; diff --git a/interface/resources/avatar/animations/sitting.fbx b/interface/resources/avatar/animations/sitting.fbx new file mode 100644 index 0000000000..dfb51afb66 Binary files /dev/null and b/interface/resources/avatar/animations/sitting.fbx differ diff --git a/interface/resources/avatar/animations/sitting_idle.fbx b/interface/resources/avatar/animations/sitting_idle.fbx new file mode 100644 index 0000000000..ee03d942cd Binary files /dev/null and b/interface/resources/avatar/animations/sitting_idle.fbx differ diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index b55b9c517d..846f1bec3c 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -335,7 +335,7 @@ Item { } } - // Per-Avatar Gain Slider + // Per-Avatar Gain Slider Slider { id: gainSlider // Size @@ -345,7 +345,7 @@ Item { anchors.verticalCenter: nameCardVUMeter.verticalCenter // Properties visible: !isMyCard && selected - value: pal.gainSliderValueDB[uuid] ? pal.gainSliderValueDB[uuid] : 0.0 + value: Users.getAvatarGain(uuid) minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 @@ -369,7 +369,7 @@ Item { mouse.accepted = false } onReleased: { - // the above mouse.accepted seems to make this + // the above mouse.accepted seems to make this // never get called, nonetheless... mouse.accepted = false } @@ -393,14 +393,9 @@ Item { } function updateGainFromQML(avatarUuid, sliderValue, isReleased) { - if (isReleased || pal.gainSliderValueDB[avatarUuid] !== sliderValue) { - pal.gainSliderValueDB[avatarUuid] = sliderValue; - var data = { - sessionId: avatarUuid, - gain: sliderValue, - isReleased: isReleased - }; - pal.sendToScript({method: 'updateGain', params: data}); + Users.setAvatarGain(avatarUuid, sliderValue); + if (isReleased) { + UserActivityLogger.palAction("avatar_gain_changed", avatarUuid); } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index cf5ea98b81..7ff4e8a4b1 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -2,7 +2,7 @@ // Pal.qml // qml/hifi // -// People Action List +// People Action List // // Created by Howard Stearns on 12/12/2016 // Copyright 2016 High Fidelity, Inc. @@ -37,9 +37,6 @@ Rectangle { property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. property bool iAmAdmin: false - // Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server. - // NOTE: if another script modifies the per-avatar gain, this value won't be accurate! - property var gainSliderValueDB: ({}); HifiConstants { id: hifi } @@ -270,7 +267,7 @@ Rectangle { // Anchors anchors.left: parent.left } - + // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now) // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox // will appear in the "hovered" state. Hovering over the checkbox will fix it. @@ -306,7 +303,7 @@ Rectangle { checked = Qt.binding(function() { return (model[styleData.role])}) } } - + // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) HifiControls.Button { id: actionButton @@ -538,7 +535,7 @@ Rectangle { } } break; - case 'updateAudioLevel': + case 'updateAudioLevel': for (var userId in message.params) { var audioLevel = message.params[userId]; // If the userId is 0, we're updating "myData". @@ -554,9 +551,8 @@ Rectangle { } } break; - case 'clearLocalQMLData': + case 'clearLocalQMLData': ignored = {}; - gainSliderValueDB = {}; break; case 'avatarDisconnected': var sessionID = message.params[0]; diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index f5fe82ef4b..6e1f44f5ac 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -351,7 +351,6 @@ void Avatar::simulate(float deltaTime, bool inView) { _jointDataSimulationRate.increment(); _skeletonModel->simulate(deltaTime, true); - _skeletonModelSimulationRate.increment(); locationChanged(); // joints changed, so if there are any children, update them. _hasNewJointData = false; @@ -367,8 +366,8 @@ void Avatar::simulate(float deltaTime, bool inView) { } else { // a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated. _skeletonModel->simulate(deltaTime, false); - _skeletonModelSimulationRate.increment(); } + _skeletonModelSimulationRate.increment(); } // update animation for display name fade in/out diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index d806c042b9..7417f73102 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -157,15 +157,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { lock.unlock(); PerformanceTimer perfTimer("otherAvatars"); - uint64_t startTime = usecTimestampNow(); auto avatarMap = getHashCopy(); QList avatarList = avatarMap.values(); ViewFrustum cameraView; qApp->copyDisplayViewFrustum(cameraView); - std::priority_queue sortedAvatars = AvatarData::sortAvatars( - avatarList, cameraView, + std::priority_queue sortedAvatars; + AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars, [](AvatarSharedPointer avatar)->uint64_t{ return std::static_pointer_cast(avatar)->getLastRenderUpdateTime(); @@ -194,10 +193,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { }); render::PendingChanges pendingChanges; - const uint64_t RENDER_UPDATE_BUDGET = 1500; // usec - const uint64_t MAX_UPDATE_BUDGET = 2000; // usec - uint64_t renderExpiry = startTime + RENDER_UPDATE_BUDGET; - uint64_t maxExpiry = startTime + MAX_UPDATE_BUDGET; + uint64_t startTime = usecTimestampNow(); + const uint64_t UPDATE_BUDGET = 2000; // usec + uint64_t updateExpiry = startTime + UPDATE_BUDGET; int numAvatarsUpdated = 0; int numAVatarsNotUpdated = 0; @@ -223,7 +221,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY; uint64_t now = usecTimestampNow(); - if (now < renderExpiry) { + if (now < updateExpiry) { // we're within budget bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; if (inView && avatar->hasNewJointData()) { @@ -232,21 +230,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { avatar->simulate(deltaTime, inView); avatar->updateRenderItem(pendingChanges); avatar->setLastRenderUpdateTime(startTime); - } else if (now < maxExpiry) { - // we've spent most of our time budget, but we still simulate() the avatar as it if were out of view - // --> some avatars may freeze until their priority trickles up - bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; - if (inView && avatar->hasNewJointData()) { - numAVatarsNotUpdated++; - } - avatar->simulate(deltaTime, false); } else { - // we've spent ALL of our time budget --> bail on the rest of the avatar updates + // we've spent our full time budget --> bail on the rest of the avatar updates // --> more avatars may freeze until their priority trickles up // --> some scale or fade animations may glitch // --> some avatar velocity measurements may be a little off - // HACK: no time simulate, but we will take the time to count how many were tragically missed + // no time simulate, but we take the time to count how many were tragically missed bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; if (!inView) { break; diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp index 843779dd3b..0c3d863649 100644 --- a/interface/src/avatar/CauterizedModel.cpp +++ b/interface/src/avatar/CauterizedModel.cpp @@ -95,12 +95,6 @@ void CauterizedModel::createCollisionRenderItemSet() { Model::createCollisionRenderItemSet(); } -// Called within Model::simulate call, below. -void CauterizedModel::updateRig(float deltaTime, glm::mat4 parentTransform) { - Model::updateRig(deltaTime, parentTransform); - _needsUpdateClusterMatrices = true; -} - void CauterizedModel::updateClusterMatrices() { PerformanceTimer perfTimer("CauterizedModel::updateClusterMatrices"); diff --git a/interface/src/avatar/CauterizedModel.h b/interface/src/avatar/CauterizedModel.h index 01e0b13650..ba12aee32b 100644 --- a/interface/src/avatar/CauterizedModel.h +++ b/interface/src/avatar/CauterizedModel.h @@ -37,7 +37,6 @@ public: void createVisibleRenderItemSet() override; void createCollisionRenderItemSet() override; - virtual void updateRig(float deltaTime, glm::mat4 parentTransform) override; virtual void updateClusterMatrices() override; void updateRenderItems() override; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 842939d938..969268c549 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -260,6 +260,11 @@ QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { return AvatarData::toByteArrayStateful(dataDetail); } +void MyAvatar::resetSensorsAndBody() { + qApp->getActiveDisplayPlugin()->resetSensors(); + reset(true, false, true); +} + void MyAvatar::centerBody() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "centerBody"); @@ -2483,6 +2488,45 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { } } +bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& orientation) { + auto hipsIndex = getJointIndex("Hips"); + if (index != hipsIndex) { + qWarning() << "Pinning is only supported for the hips joint at the moment."; + return false; + } + + setPosition(position); + setOrientation(orientation); + + _rig->setMaxHipsOffsetLength(0.05f); + + auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index); + if (it == _pinnedJoints.end()) { + _pinnedJoints.push_back(index); + } + + return true; +} + +bool MyAvatar::clearPinOnJoint(int index) { + auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index); + if (it != _pinnedJoints.end()) { + _pinnedJoints.erase(it); + + auto hipsIndex = getJointIndex("Hips"); + if (index == hipsIndex) { + _rig->setMaxHipsOffsetLength(FLT_MAX); + } + + return true; + } + return false; +} + +float MyAvatar::getIKErrorOnLastSolve() const { + return _rig->getIKErrorOnLastSolve(); +} + // thread-safe void MyAvatar::addHoldAction(AvatarActionHold* holdAction) { std::lock_guard guard(_holdActionsMutex); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4f86256a2f..3cc665b533 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -99,6 +99,7 @@ public: void reset(bool andRecenter = false, bool andReload = true, bool andHead = true); + Q_INVOKABLE void resetSensorsAndBody(); Q_INVOKABLE void centerBody(); // thread-safe Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -216,6 +217,11 @@ public: virtual void clearJointData(int index) override; virtual void clearJointsData() override; + Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation); + Q_INVOKABLE bool clearPinOnJoint(int index); + + Q_INVOKABLE float getIKErrorOnLastSolve() const; + Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; } @@ -527,6 +533,8 @@ private: bool didTeleport(); bool getIsAway() const { return _isAway; } void setAway(bool value); + + std::vector _pinnedJoints; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 4b77323bba..476abf8d4b 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -166,7 +166,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState); // evaluate AnimGraph animation and update jointStates. - CauterizedModel::updateRig(deltaTime, parentTransform); + Model::updateRig(deltaTime, parentTransform); Rig::EyeParameters eyeParams; eyeParams.worldHeadOrientation = headParams.worldHeadOrientation; @@ -179,7 +179,9 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateFromEyeParameters(eyeParams); } else { - CauterizedModel::updateRig(deltaTime, parentTransform); + // no need to call Model::updateRig() because otherAvatars get their joint state + // copied directly from AvtarData::_jointData (there are no Rig animations to blend) + _needsUpdateClusterMatrices = true; // This is a little more work than we really want. // diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index adeede17ad..173af3fdf6 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -189,6 +189,7 @@ void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const std::vectorisLowerSpine()) { + if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) { // for these types of targets we only allow twist at the lower-spine // (this prevents the hand targets from bending the spine too much and thereby driving the hips too far) glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans(); @@ -300,8 +301,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe const float MIN_ADJUSTMENT_ANGLE = 1.0e-4f; if (angle > MIN_ADJUSTMENT_ANGLE) { // reduce angle by a fraction (for stability) - const float FRACTION = 0.5f; - angle *= FRACTION; + const float STABILITY_FRACTION = 0.5f; + angle *= STABILITY_FRACTION; deltaRotation = glm::angleAxis(angle, axis); // The swing will re-orient the tip but there will tend to be be a non-zero delta between the tip's @@ -323,7 +324,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe glm::vec3 axis = glm::normalize(deltaRotation * leverArm); swingTwistDecomposition(missingRotation, axis, swingPart, twistPart); float dotSign = copysignf(1.0f, twistPart.w); - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, FRACTION)) * deltaRotation; + const float LIMIT_LEAK_FRACTION = 0.1f; + deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation; } } } @@ -486,7 +488,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars // measure new _hipsOffset for next frame // by looking for discrepancies between where a targeted endEffector is // and where it wants to be (after IK solutions are done) - glm::vec3 newHipsOffset = Vectors::ZERO; + + // use weighted average between HMD and other targets + float HMD_WEIGHT = 10.0f; + float OTHER_WEIGHT = 1.0f; + float totalWeight = 0.0f; + + glm::vec3 additionalHipsOffset = Vectors::ZERO; for (auto& target: targets) { int targetIndex = target.getIndex(); if (targetIndex == _headIndex && _headIndex != -1) { @@ -497,32 +505,61 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; - newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); + additionalHipsOffset += (OTHER_WEIGHT * HEAD_OFFSET_SLAVE_FACTOR) * (under- actual); + totalWeight += OTHER_WEIGHT; } else if (target.getType() == IKTarget::Type::HmdHead) { - // we want to shift the hips to bring the head to its designated position glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - _hipsOffset += target.getTranslation() - actual; - // and ignore all other targets - newHipsOffset = _hipsOffset; - break; + glm::vec3 thisOffset = target.getTranslation() - actual; + glm::vec3 futureHipsOffset = _hipsOffset + thisOffset; + if (glm::length(glm::vec2(futureHipsOffset.x, futureHipsOffset.z)) < _maxHipsOffsetLength) { + // it is imperative to shift the hips and bring the head to its designated position + // so we slam newHipsOffset here and ignore all other targets + additionalHipsOffset = futureHipsOffset - _hipsOffset; + totalWeight = 0.0f; + break; + } else { + additionalHipsOffset += HMD_WEIGHT * (target.getTranslation() - actual); + totalWeight += HMD_WEIGHT; + } } } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); glm::vec3 targetPosition = target.getTranslation(); - newHipsOffset += targetPosition - actualPosition; + additionalHipsOffset += OTHER_WEIGHT * (targetPosition - actualPosition); + totalWeight += OTHER_WEIGHT; } } + if (totalWeight > 1.0f) { + additionalHipsOffset /= totalWeight; + } + + // Add downward pressure on the hips + additionalHipsOffset *= 0.95f; + additionalHipsOffset -= 1.0f; // smooth transitions by relaxing _hipsOffset toward the new value - const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.15f; + const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; - _hipsOffset += (newHipsOffset - _hipsOffset) * tau; + _hipsOffset += additionalHipsOffset * tau; + + // clamp the hips offset + float hipsOffsetLength = glm::length(_hipsOffset); + if (hipsOffsetLength > _maxHipsOffsetLength) { + _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + } + } } } return _relativePoses; } +void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { + // manually adjust scale here + const float METERS_TO_CENTIMETERS = 100.0f; + _maxHipsOffsetLength = METERS_TO_CENTIMETERS * maxLength; +} + void AnimInverseKinematics::clearIKJointLimitHistory() { for (auto& pair : _constraints) { pair.second->clearHistory(); @@ -740,7 +777,7 @@ void AnimInverseKinematics::initConstraints() { stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); std::vector minDots; - const float MAX_SPINE_SWING = PI / 14.0f; + const float MAX_SPINE_SWING = PI / 10.0f; minDots.push_back(cosf(MAX_SPINE_SWING)); stConstraint->setSwingLimits(minDots); if (0 == baseName.compare("Spine1", Qt::CaseSensitive) @@ -776,11 +813,11 @@ void AnimInverseKinematics::initConstraints() { } else if (0 == baseName.compare("Head", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_HEAD_TWIST = PI / 9.0f; + const float MAX_HEAD_TWIST = PI / 6.0f; stConstraint->setTwistLimits(-MAX_HEAD_TWIST, MAX_HEAD_TWIST); std::vector minDots; - const float MAX_HEAD_SWING = PI / 10.0f; + const float MAX_HEAD_SWING = PI / 6.0f; minDots.push_back(cosf(MAX_HEAD_SWING)); stConstraint->setSwingLimits(minDots); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index c9560c7383..892a5616b2 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -39,6 +39,10 @@ public: void clearIKJointLimitHistory(); + void setMaxHipsOffsetLength(float maxLength); + + float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); void solveWithCyclicCoordinateDescent(const std::vector& targets); @@ -83,6 +87,7 @@ protected: // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; + float _maxHipsOffsetLength{ FLT_MAX }; int _headIndex { -1 }; int _hipsIndex { -1 }; int _hipsParentIndex { -1 }; @@ -90,6 +95,8 @@ protected: // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses // during the the cyclic coordinate descent algorithm int _maxTargetIndex { 0 }; + + float _maxErrorOnLastSolve { FLT_MAX }; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 07462e9878..84e34adec7 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -319,6 +319,39 @@ void Rig::clearIKJointLimitHistory() { } } +void Rig::setMaxHipsOffsetLength(float maxLength) { + _maxHipsOffsetLength = maxLength; + + if (_animNode) { + _animNode->traverse([&](AnimNode::Pointer node) { + auto ikNode = std::dynamic_pointer_cast(node); + if (ikNode) { + ikNode->setMaxHipsOffsetLength(_maxHipsOffsetLength); + } + return true; + }); + } +} + +float Rig::getMaxHipsOffsetLength() const { + return _maxHipsOffsetLength; +} + +float Rig::getIKErrorOnLastSolve() const { + float result = 0.0f; + + if (_animNode) { + _animNode->traverse([&](AnimNode::Pointer node) { + auto ikNode = std::dynamic_pointer_cast(node); + if (ikNode) { + result = ikNode->getMaxErrorOnLastSolve(); + } + return true; + }); + } + return result; +} + int Rig::getJointParentIndex(int childIndex) const { if (_animSkeleton && isIndexValid(childIndex)) { return _animSkeleton->getParentIndex(childIndex); @@ -1274,39 +1307,50 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { void Rig::copyJointsFromJointData(const QVector& jointDataVec) { PerformanceTimer perfTimer("copyJoints"); PROFILE_RANGE(simulation_animation_detail, "copyJoints"); - if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._relativePoses.size()) { - // make a vector of rotations in absolute-geometry-frame - const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses(); - std::vector rotations; - rotations.reserve(absoluteDefaultPoses.size()); - const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); - for (int i = 0; i < jointDataVec.size(); i++) { - const JointData& data = jointDataVec.at(i); - if (data.rotationSet) { - // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame - rotations.push_back(rigToGeometryRot * data.rotation); - } else { - rotations.push_back(absoluteDefaultPoses[i].rot()); - } - } + if (!_animSkeleton) { + return; + } + if (jointDataVec.size() != (int)_internalPoseSet._relativePoses.size()) { + // animations haven't fully loaded yet. + _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + } - // convert rotations from absolute to parent relative. - _animSkeleton->convertAbsoluteRotationsToRelative(rotations); - - // store new relative poses - const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses(); - for (int i = 0; i < jointDataVec.size(); i++) { - const JointData& data = jointDataVec.at(i); - _internalPoseSet._relativePoses[i].scale() = Vectors::ONE; - _internalPoseSet._relativePoses[i].rot() = rotations[i]; - if (data.translationSet) { - // JointData translations are in scaled relative-frame so we scale back to regular relative-frame - _internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation; - } else { - _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans(); - } + // make a vector of rotations in absolute-geometry-frame + const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses(); + std::vector rotations; + rotations.reserve(absoluteDefaultPoses.size()); + const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); + for (int i = 0; i < jointDataVec.size(); i++) { + const JointData& data = jointDataVec.at(i); + if (data.rotationSet) { + // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame + rotations.push_back(rigToGeometryRot * data.rotation); + } else { + rotations.push_back(absoluteDefaultPoses[i].rot()); } } + + // convert rotations from absolute to parent relative. + _animSkeleton->convertAbsoluteRotationsToRelative(rotations); + + // store new relative poses + const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses(); + for (int i = 0; i < jointDataVec.size(); i++) { + const JointData& data = jointDataVec.at(i); + _internalPoseSet._relativePoses[i].scale() = Vectors::ONE; + _internalPoseSet._relativePoses[i].rot() = rotations[i]; + if (data.translationSet) { + // JointData translations are in scaled relative-frame so we scale back to regular relative-frame + _internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation; + } else { + _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans(); + } + } + + // build absolute poses and copy to externalPoseSet + buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); + QWriteLocker writeLock(&_externalPoseSetLock); + _externalPoseSet = _internalPoseSet; } void Rig::computeAvatarBoundingCapsule( diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 78a669b249..f1c87d0d3e 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -104,6 +104,10 @@ public: void clearJointAnimationPriority(int index); void clearIKJointLimitHistory(); + void setMaxHipsOffsetLength(float maxLength); + float getMaxHipsOffsetLength() const; + + float getIKErrorOnLastSolve() const; int getJointParentIndex(int childIndex) const; @@ -318,6 +322,8 @@ protected: bool _enabledAnimations { true }; mutable uint32_t _jointNameWarningCount { 0 }; + float _maxHipsOffsetLength { 1.0f }; + float _maxErrorOnLastSolve { 0.0f }; private: QMap _stateHandlers; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c1dd60a3b0..8025c680ca 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2324,61 +2324,57 @@ float AvatarData::_avatarSortCoefficientSize { 0.5f }; float AvatarData::_avatarSortCoefficientCenter { 0.25 }; float AvatarData::_avatarSortCoefficientAge { 1.0f }; -std::priority_queue AvatarData::sortAvatars( - QList avatarList, - const ViewFrustum& cameraView, - std::function getLastUpdated, - std::function getBoundingRadius, - std::function shouldIgnore) { +void AvatarData::sortAvatars( + QList avatarList, + const ViewFrustum& cameraView, + std::priority_queue& sortedAvatarsOut, + std::function getLastUpdated, + std::function getBoundingRadius, + std::function shouldIgnore) { - uint64_t startTime = usecTimestampNow(); + PROFILE_RANGE(simulation, "sort"); + uint64_t now = usecTimestampNow(); glm::vec3 frustumCenter = cameraView.getPosition(); + const glm::vec3& forward = cameraView.getDirection(); + for (int32_t i = 0; i < avatarList.size(); ++i) { + const auto& avatar = avatarList.at(i); - std::priority_queue sortedAvatars; - { - PROFILE_RANGE(simulation, "sort"); - for (int32_t i = 0; i < avatarList.size(); ++i) { - const auto& avatar = avatarList.at(i); - - if (shouldIgnore(avatar)) { - continue; - } - - // priority = weighted linear combination of: - // (a) apparentSize - // (b) proximity to center of view - // (c) time since last update - glm::vec3 avatarPosition = avatar->getPosition(); - glm::vec3 offset = avatarPosition - frustumCenter; - float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero - - // FIXME - AvatarData has something equivolent to this - float radius = getBoundingRadius(avatar); - - const glm::vec3& forward = cameraView.getDirection(); - float apparentSize = 2.0f * radius / distance; - float cosineAngle = glm::length(glm::dot(offset, forward) * forward) / distance; - float age = (float)(startTime - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND); - - // NOTE: we are adding values of different units to get a single measure of "priority". - // Thus we multiply each component by a conversion "weight" that scales its units relative to the others. - // These weights are pure magic tuning and should be hard coded in the relation below, - // but are currently exposed for anyone who would like to explore fine tuning: - float priority = _avatarSortCoefficientSize * apparentSize - + _avatarSortCoefficientCenter * cosineAngle - + _avatarSortCoefficientAge * age; - - // decrement priority of avatars outside keyhole - if (distance > cameraView.getCenterRadius()) { - if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) { - priority += OUT_OF_VIEW_PENALTY; - } - } - sortedAvatars.push(AvatarPriority(avatar, priority)); + if (shouldIgnore(avatar)) { + continue; } + + // priority = weighted linear combination of: + // (a) apparentSize + // (b) proximity to center of view + // (c) time since last update + glm::vec3 avatarPosition = avatar->getPosition(); + glm::vec3 offset = avatarPosition - frustumCenter; + float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero + + // FIXME - AvatarData has something equivolent to this + float radius = getBoundingRadius(avatar); + + float apparentSize = 2.0f * radius / distance; + float cosineAngle = glm::dot(offset, forward) / distance; + float age = (float)(now - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND); + + // NOTE: we are adding values of different units to get a single measure of "priority". + // Thus we multiply each component by a conversion "weight" that scales its units relative to the others. + // These weights are pure magic tuning and should be hard coded in the relation below, + // but are currently exposed for anyone who would like to explore fine tuning: + float priority = _avatarSortCoefficientSize * apparentSize + + _avatarSortCoefficientCenter * cosineAngle + + _avatarSortCoefficientAge * age; + + // decrement priority of avatars outside keyhole + if (distance > cameraView.getCenterRadius()) { + if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) { + priority += OUT_OF_VIEW_PENALTY; + } + } + sortedAvatarsOut.push(AvatarPriority(avatar, priority)); } - return sortedAvatars; } QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 12209d9c31..c2240f400f 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -597,9 +597,10 @@ public: static const float OUT_OF_VIEW_PENALTY; - static std::priority_queue sortAvatars( + static void sortAvatars( QList avatarList, const ViewFrustum& cameraView, + std::priority_queue& sortedAvatarsOut, std::function getLastUpdated, std::function getBoundingRadius, std::function shouldIgnore); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 4f4f3bf67f..55a7221f5d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -944,7 +944,10 @@ void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, cons void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload, const bool unloadFirst) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); - bool shouldLoad = entity && entity->shouldPreloadScript() && _entitiesScriptEngine; + if (!entity) { + return; + } + bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine; QString scriptUrl = entity->getScript(); if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) { _entitiesScriptEngine->unloadEntityScript(entityID); diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7646f0a454..7359a548fc 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -266,6 +266,35 @@ void RenderablePolyVoxEntityItem::forEachVoxelValue(quint16 voxelXSize, quint16 }); } +QByteArray RenderablePolyVoxEntityItem::volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const { + int totalSize = voxelXSize * voxelYSize * voxelZSize; + QByteArray result = QByteArray(totalSize, '\0'); + int index = 0; + int lowX = 0; + int lowY = 0; + int lowZ = 0; + + withReadLock([&] { + if (isEdged(_voxelSurfaceStyle)) { + lowX++; + lowY++; + lowZ++; + voxelXSize++; + voxelYSize++; + voxelZSize++; + } + + for (int z = lowZ; z < voxelZSize; z++) { + for (int y = lowY; y < voxelYSize; y++) { + for (int x = lowX; x < voxelXSize; x++) { + result[index++] = _volData->getVoxelAt(x, y, z); + } + } + } + }); + + return result; +} bool RenderablePolyVoxEntityItem::setAll(uint8_t toValue) { bool result = false; @@ -365,12 +394,28 @@ bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float r } glm::mat4 vtwMatrix = voxelToWorldMatrix(); + glm::mat4 wtvMatrix = glm::inverse(vtwMatrix); - // This three-level for loop iterates over every voxel in the volume + glm::vec3 dimensions = getDimensions(); + glm::vec3 voxelSize = dimensions / _voxelVolumeSize; + float smallestDimensionSize = voxelSize.x; + smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y); + smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z); + + glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize); + glm::vec3 centerInVoxelCoords = wtvMatrix * glm::vec4(centerWorldCoords, 1.0f); + + glm::vec3 low = glm::floor(centerInVoxelCoords - maxRadiusInVoxelCoords); + glm::vec3 high = glm::ceil(centerInVoxelCoords + maxRadiusInVoxelCoords); + + glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize); + glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize); + + // This three-level for loop iterates over every voxel in the volume that might be in the sphere withWriteLock([&] { - for (int z = 0; z < _voxelVolumeSize.z; z++) { - for (int y = 0; y < _voxelVolumeSize.y; y++) { - for (int x = 0; x < _voxelVolumeSize.x; x++) { + for (int z = lowI.z; z < highI.z; z++) { + for (int y = lowI.y; y < highI.y; y++) { + for (int x = lowI.x; x < highI.x; x++) { // Store our current position as a vector... glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates // convert to world coordinates @@ -392,6 +437,59 @@ bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float r return result; } +bool RenderablePolyVoxEntityItem::setCapsule(glm::vec3 startWorldCoords, glm::vec3 endWorldCoords, + float radiusWorldCoords, uint8_t toValue) { + bool result = false; + if (_locked) { + return result; + } + + glm::mat4 vtwMatrix = voxelToWorldMatrix(); + glm::mat4 wtvMatrix = glm::inverse(vtwMatrix); + + glm::vec3 dimensions = getDimensions(); + glm::vec3 voxelSize = dimensions / _voxelVolumeSize; + float smallestDimensionSize = voxelSize.x; + smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y); + smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z); + + glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize); + + glm::vec3 startInVoxelCoords = wtvMatrix * glm::vec4(startWorldCoords, 1.0f); + glm::vec3 endInVoxelCoords = wtvMatrix * glm::vec4(endWorldCoords, 1.0f); + + glm::vec3 low = glm::min(glm::floor(startInVoxelCoords - maxRadiusInVoxelCoords), + glm::floor(endInVoxelCoords - maxRadiusInVoxelCoords)); + glm::vec3 high = glm::max(glm::ceil(startInVoxelCoords + maxRadiusInVoxelCoords), + glm::ceil(endInVoxelCoords + maxRadiusInVoxelCoords)); + + glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize); + glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize); + + // This three-level for loop iterates over every voxel in the volume that might be in the capsule + withWriteLock([&] { + for (int z = lowI.z; z < highI.z; z++) { + for (int y = lowI.y; y < highI.y; y++) { + for (int x = lowI.x; x < highI.x; x++) { + // Store our current position as a vector... + glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates + // convert to world coordinates + glm::vec3 worldPos = glm::vec3(vtwMatrix * pos); + if (pointInCapsule(worldPos, startWorldCoords, endWorldCoords, radiusWorldCoords)) { + result |= setVoxelInternal(x, y, z, toValue); + } + } + } + } + }); + + if (result) { + compressVolumeDataAndSendEditPacket(); + } + return result; +} + + class RaycastFunctor { public: @@ -501,6 +599,9 @@ PolyVox::RaycastResult RenderablePolyVoxEntityItem::doRayCast(glm::vec4 originIn // virtual ShapeType RenderablePolyVoxEntityItem::getShapeType() const { + if (_collisionless) { + return SHAPE_TYPE_NONE; + } return SHAPE_TYPE_COMPOUND; } @@ -512,6 +613,11 @@ void RenderablePolyVoxEntityItem::updateRegistrationPoint(const glm::vec3& value } bool RenderablePolyVoxEntityItem::isReadyToComputeShape() { + ShapeType shapeType = getShapeType(); + if (shapeType == SHAPE_TYPE_NONE) { + return true; + } + // we determine if we are ready to compute the physics shape by actually doing so. // if _voxelDataDirty or _volDataDirty is set, don't do this yet -- wait for their // threads to finish before creating the collision shape. @@ -524,6 +630,12 @@ bool RenderablePolyVoxEntityItem::isReadyToComputeShape() { } void RenderablePolyVoxEntityItem::computeShapeInfo(ShapeInfo& info) { + ShapeType shapeType = getShapeType(); + if (shapeType == SHAPE_TYPE_NONE) { + info.setParams(getShapeType(), 0.5f * getDimensions()); + return; + } + // the shape was actually computed in isReadyToComputeShape. Just hand it off, here. withWriteLock([&] { info = _shapeInfo; @@ -736,7 +848,7 @@ glm::vec3 RenderablePolyVoxEntityItem::localCoordsToVoxelCoords(glm::vec3& local void RenderablePolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) { // This controls how many individual voxels are in the entity. This is unrelated to - // the dimentions of the entity -- it defines the size of the arrays that hold voxel values. + // the dimentions of the entity -- it defines the sizes of the arrays that hold voxel values. // In addition to setting the number of voxels, this is used in a few places for its // side-effect of allocating _volData to be the correct size. withWriteLock([&] { @@ -807,7 +919,7 @@ uint8_t RenderablePolyVoxEntityItem::getVoxel(int x, int y, int z) { } -uint8_t RenderablePolyVoxEntityItem::getVoxelInternal(int x, int y, int z) { +uint8_t RenderablePolyVoxEntityItem::getVoxelInternal(int x, int y, int z) const { if (!inUserBounds(_volData, _voxelSurfaceStyle, x, y, z)) { return 0; } @@ -949,17 +1061,8 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() { EntityTreePointer tree = element ? element->getTree() : nullptr; QtConcurrent::run([voxelXSize, voxelYSize, voxelZSize, entity, tree] { - int rawSize = voxelXSize * voxelYSize * voxelZSize; - QByteArray uncompressedData = QByteArray(rawSize, '\0'); - auto polyVoxEntity = std::static_pointer_cast(entity); - polyVoxEntity->forEachVoxelValue(voxelXSize, voxelYSize, voxelZSize, [&] (int x, int y, int z, uint8_t uVoxelValue) { - int uncompressedIndex = - z * voxelYSize * voxelXSize + - y * voxelXSize + - x; - uncompressedData[uncompressedIndex] = uVoxelValue; - }); + QByteArray uncompressedData = polyVoxEntity->volDataToArray(voxelXSize, voxelYSize, voxelZSize); QByteArray newVoxelData; QDataStream writer(&newVoxelData, QIODevice::WriteOnly | QIODevice::Truncate); @@ -1174,7 +1277,9 @@ void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { // this catches the payload from getMesh bool neighborsNeedUpdate; withWriteLock([&] { - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + if (!_collisionless) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } _mesh = mesh; _meshDirty = true; _meshInitialized = true; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index ee4c3b318f..45842c2fb9 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -94,6 +94,8 @@ public: // coords are in world-space virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) override; + virtual bool setCapsule(glm::vec3 startWorldCoords, glm::vec3 endWorldCoords, + float radiusWorldCoords, uint8_t toValue) override; virtual bool setAll(uint8_t toValue) override; virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) override; @@ -128,12 +130,13 @@ public: void setVoxelsFromData(QByteArray uncompressedData, quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize); void forEachVoxelValue(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize, std::function thunk); + QByteArray volDataToArray(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize) const; void setMesh(model::MeshPointer mesh); void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } - uint8_t getVoxelInternal(int x, int y, int z); + uint8_t getVoxelInternal(int x, int y, int z) const; bool setVoxelInternal(int x, int y, int z, uint8_t toValue); void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; }); } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index a5bd0135e4..540eba4511 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -285,7 +285,7 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit desiredProperties = entity->getEntityProperties(params); desiredProperties.setHasProperty(PROP_LOCAL_POSITION); desiredProperties.setHasProperty(PROP_LOCAL_ROTATION); - } + } results = entity->getProperties(desiredProperties); @@ -825,7 +825,7 @@ bool EntityScriptingInterface::setVoxels(QUuid entityID, EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { - qCDebug(entities) << "EntityScriptingInterface::setVoxelSphere no entity with ID" << entityID; + qCDebug(entities) << "EntityScriptingInterface::setVoxels no entity with ID" << entityID; return false; } @@ -887,24 +887,34 @@ bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& c PROFILE_RANGE(script_entities, __FUNCTION__); return setVoxels(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) { - return polyVoxEntity.setSphere(center, radius, value); - }); + return polyVoxEntity.setSphere(center, radius, value); + }); +} + +bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID, + const glm::vec3& start, const glm::vec3& end, + float radius, int value) { + PROFILE_RANGE(script_entities, __FUNCTION__); + + return setVoxels(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) { + return polyVoxEntity.setCapsule(start, end, radius, value); + }); } bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& position, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return setVoxels(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) { - return polyVoxEntity.setVoxelInVolume(position, value); - }); + return polyVoxEntity.setVoxelInVolume(position, value); + }); } bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return setVoxels(entityID, [value](PolyVoxEntityItem& polyVoxEntity) { - return polyVoxEntity.setAll(value); - }); + return polyVoxEntity.setAll(value); + }); } bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, @@ -912,8 +922,8 @@ bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3 PROFILE_RANGE(script_entities, __FUNCTION__); return setVoxels(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) { - return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value); - }); + return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value); + }); } bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector& points) { @@ -1020,25 +1030,25 @@ QUuid EntityScriptingInterface::addAction(const QString& actionTypeString, auto actionFactory = DependencyManager::get(); bool success = false; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { - // create this action even if the entity doesn't have physics info. it will often be the - // case that a script adds an action immediately after an object is created, and the physicsInfo - // is computed asynchronously. - // if (!entity->getPhysicsInfo()) { - // return false; - // } - EntityActionType actionType = EntityActionInterface::actionTypeFromString(actionTypeString); - if (actionType == ACTION_TYPE_NONE) { - return false; - } - EntityActionPointer action = actionFactory->factory(actionType, actionID, entity, arguments); - if (!action) { - return false; - } - action->setIsMine(true); - success = entity->addAction(simulation, action); - entity->grabSimulationOwnership(); - return false; // Physics will cause a packet to be sent, so don't send from here. - }); + // create this action even if the entity doesn't have physics info. it will often be the + // case that a script adds an action immediately after an object is created, and the physicsInfo + // is computed asynchronously. + // if (!entity->getPhysicsInfo()) { + // return false; + // } + EntityActionType actionType = EntityActionInterface::actionTypeFromString(actionTypeString); + if (actionType == ACTION_TYPE_NONE) { + return false; + } + EntityActionPointer action = actionFactory->factory(actionType, actionID, entity, arguments); + if (!action) { + return false; + } + action->setIsMine(true); + success = entity->addAction(simulation, action); + entity->grabSimulationOwnership(); + return false; // Physics will cause a packet to be sent, so don't send from here. + }); if (success) { return actionID; } @@ -1050,12 +1060,12 @@ bool EntityScriptingInterface::updateAction(const QUuid& entityID, const QUuid& PROFILE_RANGE(script_entities, __FUNCTION__); return actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { - bool success = entity->updateAction(simulation, actionID, arguments); - if (success) { - entity->grabSimulationOwnership(); - } - return success; - }); + bool success = entity->updateAction(simulation, actionID, arguments); + if (success) { + entity->grabSimulationOwnership(); + } + return success; + }); } bool EntityScriptingInterface::deleteAction(const QUuid& entityID, const QUuid& actionID) { @@ -1063,13 +1073,13 @@ bool EntityScriptingInterface::deleteAction(const QUuid& entityID, const QUuid& bool success = false; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { - success = entity->removeAction(simulation, actionID); - if (success) { - // reduce from grab to poke - entity->pokeSimulationOwnership(); - } - return false; // Physics will cause a packet to be sent, so don't send from here. - }); + success = entity->removeAction(simulation, actionID); + if (success) { + // reduce from grab to poke + entity->pokeSimulationOwnership(); + } + return false; // Physics will cause a packet to be sent, so don't send from here. + }); return success; } @@ -1078,10 +1088,10 @@ QVector EntityScriptingInterface::getActionIDs(const QUuid& entityID) { QVector result; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { - QList actionIDs = entity->getActionIDs(); - result = QVector::fromList(actionIDs); - return false; // don't send an edit packet - }); + QList actionIDs = entity->getActionIDs(); + result = QVector::fromList(actionIDs); + return false; // don't send an edit packet + }); return result; } @@ -1090,9 +1100,9 @@ QVariantMap EntityScriptingInterface::getActionArguments(const QUuid& entityID, QVariantMap result; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { - result = entity->getActionArguments(actionID); - return false; // don't send an edit packet - }); + result = entity->getActionArguments(actionID); + return false; // don't send an edit packet + }); return result; } @@ -1523,3 +1533,11 @@ QObject* EntityScriptingInterface::getWebViewRoot(const QUuid& entityID) { return nullptr; } } + +// TODO move this someplace that makes more sense... +bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions, + const glm::vec3& start, const glm::vec3& end, float radius) { + glm::vec3 penetration; + AABox aaBox(low, dimensions); + return aaBox.findCapsulePenetration(start, end, radius, penetration); +} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 0353fa08a8..e9f0637830 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -223,6 +223,8 @@ public slots: Q_INVOKABLE bool getDrawZoneBoundaries() const; Q_INVOKABLE bool setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value); + Q_INVOKABLE bool setVoxelCapsule(QUuid entityID, const glm::vec3& start, const glm::vec3& end, float radius, int value); + Q_INVOKABLE bool setVoxel(QUuid entityID, const glm::vec3& position, int value); Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, @@ -287,6 +289,10 @@ public slots: Q_INVOKABLE QObject* getWebViewRoot(const QUuid& entityID); + Q_INVOKABLE bool AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions, + const glm::vec3& start, const glm::vec3& end, float radius); + + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 4f478c8bf7..910d8eff88 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -86,6 +86,8 @@ class PolyVoxEntityItem : public EntityItem { // coords are in world-space virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) { return false; } + virtual bool setCapsule(glm::vec3 startWorldCoords, glm::vec3 endWorldCoords, + float radiusWorldCoords, uint8_t toValue) { return false; } virtual bool setAll(uint8_t toValue) { return false; } virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value) { return false; } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index f4a02ad805..7147682d48 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -49,7 +49,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) setCustomDeleter([](Dependency* dependency){ static_cast(dependency)->deleteLater(); }); - + auto addressManager = DependencyManager::get(); // handle domain change signals from AddressManager @@ -85,8 +85,8 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); auto accountManager = DependencyManager::get(); - - // assume that we may need to send a new DS check in anytime a new keypair is generated + + // assume that we may need to send a new DS check in anytime a new keypair is generated connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn); // clear out NodeList when login is finished @@ -101,7 +101,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) // anytime we get a new node we may need to re-send our set of ignored node IDs to it connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode); - + // setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect) _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable connect(&_keepAlivePingTimer, &QTimer::timeout, this, &NodeList::sendKeepAlivePings); @@ -161,11 +161,11 @@ qint64 NodeList::sendStatsToDomainServer(QJsonObject statsObject) { void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode) { PingType_t pingType; - + quint64 ourOriginalTime, othersReplyTime; - + message.seek(0); - + message.readPrimitive(&pingType); message.readPrimitive(&ourOriginalTime); message.readPrimitive(&othersReplyTime); @@ -199,7 +199,7 @@ void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer& } void NodeList::processPingPacket(QSharedPointer message, SharedNodePointer sendingNode) { - + // send back a reply auto replyPacket = constructPingReplyPacket(*message); const HifiSockAddr& senderSockAddr = message->getSenderSockAddr(); @@ -252,6 +252,11 @@ void NodeList::reset() { _personalMutedNodeIDs.clear(); _personalMutedSetLock.unlock(); + // lock and clear out set of avatarGains + _avatarGainMapLock.lockForWrite(); + _avatarGainMap.clear(); + _avatarGainMapLock.unlock(); + // refresh the owner UUID to the NULL UUID setSessionUUID(QUuid()); @@ -329,7 +334,7 @@ void NodeList::sendDomainServerCheckIn() { } auto domainPacket = NLPacket::create(domainPacketType); - + QDataStream packetStream(domainPacket.get()); if (domainPacketType == PacketType::DomainConnectRequest) { @@ -488,7 +493,7 @@ void NodeList::processDomainServerPathResponse(QSharedPointer m qCDebug(networking) << "Could not read query path from DomainServerPathQueryResponse. Bailing."; return; } - + QString pathQuery = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numPathBytes); message->seek(message->getPosition() + numPathBytes); @@ -500,10 +505,10 @@ void NodeList::processDomainServerPathResponse(QSharedPointer m qCDebug(networking) << "Could not read resulting viewpoint from DomainServerPathQueryReponse. Bailing"; return; } - + // pull the viewpoint from the packet QString viewpoint = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numViewpointBytes); - + // Hand it off to the AddressManager so it can handle it as a relative viewpoint if (DependencyManager::get()->goToViewpointForPath(viewpoint, pathQuery)) { qCDebug(networking) << "Going to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery; @@ -664,16 +669,16 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { } void NodeList::sendAssignment(Assignment& assignment) { - + PacketType assignmentPacketType = assignment.getCommand() == Assignment::CreateCommand ? PacketType::CreateAssignment : PacketType::RequestAssignment; auto assignmentPacket = NLPacket::create(assignmentPacketType); - + QDataStream packetStream(assignmentPacket.get()); packetStream << assignment; - + sendPacket(std::move(assignmentPacket), _assignmentServerSocket); } @@ -833,7 +838,7 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { _ignoredNodeIDs.insert(nodeID); } { - QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert + QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert // add this nodeID to our set of personal muted IDs _personalMutedNodeIDs.insert(nodeID); } @@ -896,7 +901,7 @@ void NodeList::personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled if (muteEnabled) { - QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert + QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert // add this nodeID to our set of personal muted IDs _personalMutedNodeIDs.insert(nodeID); } else { @@ -981,7 +986,7 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { if (audioMixer) { // setup the packet auto setAvatarGainPacket = NLPacket::create(PacketType::PerAvatarGainSet, NUM_BYTES_RFC4122_UUID + sizeof(float), true); - + // write the node ID to the packet setAvatarGainPacket->write(nodeID.toRfc4122()); // We need to convert the gain in dB (from the script) to an amplitude before packing it. @@ -990,6 +995,9 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { qCDebug(networking) << "Sending Set Avatar Gain packet UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; sendPacket(std::move(setAvatarGainPacket), *audioMixer); + QWriteLocker{ &_avatarGainMapLock }; + _avatarGainMap[nodeID] = gain; + } else { qWarning() << "Couldn't find audio mixer to send set gain request"; } @@ -998,6 +1006,15 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { } } +float NodeList::getAvatarGain(const QUuid& nodeID) { + QReadLocker{ &_avatarGainMapLock }; + auto it = _avatarGainMap.find(nodeID); + if (it != _avatarGainMap.cend()) { + return it->second; + } + return 0.0f; +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) @@ -1036,7 +1053,7 @@ void NodeList::muteNodeBySessionID(const QUuid& nodeID) { mutePacket->write(nodeID.toRfc4122()); qCDebug(networking) << "Sending packet to mute node" << uuidStringWithoutCurlyBraces(nodeID); - + sendPacket(std::move(mutePacket), *audioMixer); } else { qWarning() << "Couldn't find audio mixer to send node mute request"; diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 0e0a2fd6c8..293b0942d6 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -68,7 +68,7 @@ public: void setAssignmentServerSocket(const HifiSockAddr& serverSocket) { _assignmentServerSocket = serverSocket; } void sendAssignment(Assignment& assignment); - + void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } void ignoreNodesInRadius(bool enabled = true); @@ -83,6 +83,7 @@ public: void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); bool isPersonalMutingNode(const QUuid& nodeID) const; void setAvatarGain(const QUuid& nodeID, float gain); + float getAvatarGain(const QUuid& nodeID); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -103,7 +104,7 @@ public slots: void processDomainServerPathResponse(QSharedPointer message); void processDomainServerConnectionTokenPacket(QSharedPointer message); - + void processPingPacket(QSharedPointer message, SharedNodePointer sendingNode); void processPingReplyPacket(QSharedPointer message, SharedNodePointer sendingNode); @@ -131,11 +132,11 @@ private slots: void handleNodePingTimeout(); void pingPunchForDomainServer(); - + void sendKeepAlivePings(); void maybeSendIgnoreSetToNode(SharedNodePointer node); - + private: NodeList() : LimitedNodeList(INVALID_PORT, INVALID_PORT) { assert(false); } // Not implemented, needed for DependencyManager templates compile NodeList(char ownerType, int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT); @@ -148,7 +149,7 @@ private: void timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode); void sendDSPathQuery(const QString& newPath); - + void parseNodeFromPacketStream(QDataStream& packetStream); void pingPunchForInactiveNode(const SharedNodePointer& node); @@ -170,6 +171,8 @@ private: tbb::concurrent_unordered_set _ignoredNodeIDs; mutable QReadWriteLock _personalMutedSetLock; tbb::concurrent_unordered_set _personalMutedNodeIDs; + mutable QReadWriteLock _avatarGainMapLock; + tbb::concurrent_unordered_map _avatarGainMap; void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true }; diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 3a3225ec75..6dc3188b3f 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -47,6 +47,10 @@ void UsersScriptingInterface::setAvatarGain(const QUuid& nodeID, float gain) { DependencyManager::get()->setAvatarGain(nodeID, gain); } +float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { + return DependencyManager::get()->getAvatarGain(nodeID); +} + void UsersScriptingInterface::kick(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID DependencyManager::get()->kickNodeBySessionID(nodeID); @@ -88,4 +92,4 @@ bool UsersScriptingInterface::getRequestsDomainListData() { } void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) { DependencyManager::get()->setRequestsDomainListData(isRequesting); -} \ No newline at end of file +} diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 608fa937c8..acaa92d9c8 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -70,6 +70,14 @@ public slots: */ void setAvatarGain(const QUuid& nodeID, float gain); + /**jsdoc + * Gets an avatar's gain for you and you only. + * @function Users.getAvatarGain + * @param {nodeID} nodeID The node or session ID of the user whose gain you want to get. + * @return {float} gain (in dB) + */ + float getAvatarGain(const QUuid& nodeID); + /**jsdoc * Kick another user. * @function Users.kick diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index 92fe138021..c137ebd438 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -205,6 +205,33 @@ bool findRaySphereIntersection(const glm::vec3& origin, const glm::vec3& directi return true; } +bool pointInSphere(const glm::vec3& origin, const glm::vec3& center, float radius) { + glm::vec3 relativeOrigin = origin - center; + float c = glm::dot(relativeOrigin, relativeOrigin) - radius * radius; + return c <= 0.0f; +} + + +bool pointInCapsule(const glm::vec3& origin, const glm::vec3& start, const glm::vec3& end, float radius) { + glm::vec3 relativeOrigin = origin - start; + glm::vec3 relativeEnd = end - start; + float capsuleLength = glm::length(relativeEnd); + relativeEnd /= capsuleLength; + float originProjection = glm::dot(relativeEnd, relativeOrigin); + glm::vec3 constant = relativeOrigin - relativeEnd * originProjection; + float c = glm::dot(constant, constant) - radius * radius; + if (c < 0.0f) { // starts inside cylinder + if (originProjection < 0.0f) { // below start + return pointInSphere(origin, start, radius); + } else if (originProjection > capsuleLength) { // above end + return pointInSphere(origin, end, radius); + } else { // between start and end + return true; + } + } + return false; +} + bool findRayCapsuleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& start, const glm::vec3& end, float radius, float& distance) { if (start == end) { diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index 1c951ca09a..2fdc1aa25f 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -73,6 +73,9 @@ glm::vec3 addPenetrations(const glm::vec3& currentPenetration, const glm::vec3& bool findRaySphereIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& center, float radius, float& distance); +bool pointInSphere(const glm::vec3& origin, const glm::vec3& center, float radius); +bool pointInCapsule(const glm::vec3& origin, const glm::vec3& start, const glm::vec3& end, float radius); + bool findRayCapsuleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& start, const glm::vec3& end, float radius, float& distance); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 42f3ece9cd..6d503a208a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -545,7 +545,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { // HACK: when interface is launched and steam vr is NOT running, openvr will return bad HMD poses for a few frames // To workaround this, filter out any hmd poses that are obviously bad, i.e. beneath the floor. if (isBadPose(&nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking)) { - qDebug() << "WARNING: ignoring bad hmd pose from openvr"; + // qDebug() << "WARNING: ignoring bad hmd pose from openvr"; // use the last known good HMD pose nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking = _lastGoodHMDPose; diff --git a/scripts/system/assets/models/teleport-seat.fbx b/scripts/system/assets/models/teleport-seat.fbx new file mode 100644 index 0000000000..cd7a9abc7e Binary files /dev/null and b/scripts/system/assets/models/teleport-seat.fbx differ diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index d3284352bf..c058f046db 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -17,12 +17,20 @@ var NUMBER_OF_STEPS = 6; var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx"); var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); +var SEAT_MODEL_URL = Script.resolvePath("../assets/models/teleport-seat.fbx"); + var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, z: 1.15 }; +var COLORS_TELEPORT_SEAT = { + red: 255, + green: 0, + blue: 170 +} + var COLORS_TELEPORT_CAN_TELEPORT = { red: 97, green: 247, @@ -35,29 +43,30 @@ var COLORS_TELEPORT_CANNOT_TELEPORT = { blue: 141 }; -var COLORS_TELEPORT_TOO_CLOSE = { +var COLORS_TELEPORT_CANCEL = { red: 255, green: 184, blue: 73 }; var TELEPORT_CANCEL_RANGE = 1; -var USE_COOL_IN = true; var COOL_IN_DURATION = 500; +const handInfo = { + right: { + controllerInput: Controller.Standard.RightHand + }, + left: { + controllerInput: Controller.Standard.LeftHand + } +}; + function ThumbPad(hand) { this.hand = hand; var _thisPad = this; this.buttonPress = function(value) { _thisPad.buttonValue = value; - if (value === 0) { - if (activationTimeout !== null) { - Script.clearTimeout(activationTimeout); - activationTimeout = null; - } - - } }; } @@ -67,7 +76,6 @@ function Trigger(hand) { this.buttonPress = function(value) { _this.buttonValue = value; - }; this.down = function() { @@ -78,347 +86,224 @@ function Trigger(hand) { var coolInTimeout = null; +var TELEPORTER_STATES = { + IDLE: 'idle', + COOL_IN: 'cool_in', + TARGETTING_INVALID: 'targetting_invalid', +} + +var TARGET = { + NONE: 'none', // Not currently targetting anything + INVISIBLE: 'invisible', // The current target is an invvsible surface + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat', // The current target is a seat +} + function Teleporter() { var _this = this; - this.intersection = null; - this.rightOverlayLine = null; - this.leftOverlayLine = null; - this.targetOverlay = null; - this.cancelOverlay = null; - this.updateConnected = null; - this.smoothArrivalInterval = null; - this.teleportHand = null; - this.tooClose = false; - this.inCoolIn = false; + this.active = false; + this.state = TELEPORTER_STATES.IDLE; + this.currentTarget = TARGET.INVALID; - this.initialize = function() { - this.createMappings(); + this.overlayLines = { + left: null, + right: null, }; + this.updateConnected = null; + this.activeHand = null; - this.createMappings = function() { - teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); - teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); + this.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); + this.teleportMappingInternal = Controller.newMapping(this.telporterMappingInternalName); - Controller.enableMapping(teleporter.telporterMappingInternalName); + // Setup overlays + this.cancelOverlay = Overlays.addOverlay("model", { + url: TOO_CLOSE_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: false + }); + this.targetOverlay = Overlays.addOverlay("model", { + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: false + }); + this.seatOverlay = Overlays.addOverlay("model", { + url: SEAT_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: false + }); + + this.enableMappings = function() { + Controller.enableMapping(this.telporterMappingInternalName); }; this.disableMappings = function() { Controller.disableMapping(teleporter.telporterMappingInternalName); }; - this.enterTeleportMode = function(hand) { + this.cleanup = function() { + this.disableMappings(); + Overlays.deleteOverlay(this.targetOverlay); + this.targetOverlay = null; + + Overlays.deleteOverlay(this.cancelOverlay); + this.cancelOverlay = null; + + Overlays.deleteOverlay(this.seatOverlay); + this.seatOverlay = null; + + this.deleteOverlayBeams(); + if (this.updateConnected === true) { + Script.update.disconnect(this, this.update); + } + }; + + this.enterTeleportMode = function(hand) { if (inTeleportMode === true) { return; } - if (isDisabled === 'both') { + if (isDisabled === 'both' || isDisabled === hand) { return; } inTeleportMode = true; - this.inCoolIn = true; + if (coolInTimeout !== null) { Script.clearTimeout(coolInTimeout); - } + + this.state = TELEPORTER_STATES.COOL_IN; coolInTimeout = Script.setTimeout(function() { - _this.inCoolIn = false; + if (_this.state === TELEPORTER_STATES.COOL_IN) { + _this.state = TELEPORTER_STATES.TARGETTING; + } }, COOL_IN_DURATION) - if (this.smoothArrivalInterval !== null) { - Script.clearInterval(this.smoothArrivalInterval); - } - if (activationTimeout !== null) { - Script.clearInterval(activationTimeout); - } - - this.teleportHand = hand; - this.initialize(); - Script.update.connect(this.update); + this.activeHand = hand; + this.enableMappings(); + Script.update.connect(this, this.update); this.updateConnected = true; - - - }; - this.createTargetOverlay = function(visible) { - if (visible == undefined) { - visible = true; - } - - if (_this.targetOverlay !== null) { - return; - } - var targetOverlayProps = { - url: TARGET_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - visible: visible - }; - - _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); - - }; - - this.createCancelOverlay = function(visible) { - if (visible == undefined) { - visible = true; - } - - if (_this.cancelOverlay !== null) { - return; - } - - var cancelOverlayProps = { - url: TOO_CLOSE_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - visible: visible - }; - - _this.cancelOverlay = Overlays.addOverlay("model", cancelOverlayProps); - }; - - this.deleteCancelOverlay = function() { - if (this.cancelOverlay === null) { - return; - } - - Overlays.deleteOverlay(this.cancelOverlay); - this.cancelOverlay = null; - } - - this.hideCancelOverlay = function() { - if (this.cancelOverlay === null) { - return; - } - - this.intersection = null; - Overlays.editOverlay(this.cancelOverlay, { visible: false }); - } - - this.showCancelOverlay = function() { - if (this.cancelOverlay === null) { - return this.createCancelOverlay(); - } - Overlays.editOverlay(this.cancelOverlay, { visible: true }); - } - - - this.deleteTargetOverlay = function() { - if (this.targetOverlay === null) { - return; - } - - Overlays.deleteOverlay(this.targetOverlay); - this.intersection = null; - this.targetOverlay = null; - } - - this.hideTargetOverlay = function() { - if (this.targetOverlay === null) { - return; - } - - this.intersection = null; - Overlays.editOverlay(this.targetOverlay, { visible: false }); - } - - this.showTargetOverlay = function() { - if (this.targetOverlay === null) { - return this.createTargetOverlay(); - } - Overlays.editOverlay(this.targetOverlay, { visible: true }); - } - - this.turnOffOverlayBeams = function() { - this.rightOverlayOff(); - this.leftOverlayOff(); - } - this.exitTeleportMode = function(value) { - if (activationTimeout !== null) { - Script.clearTimeout(activationTimeout); - activationTimeout = null; - } if (this.updateConnected === true) { - Script.update.disconnect(this.update); + Script.update.disconnect(this, this.update); } this.disableMappings(); - this.turnOffOverlayBeams(); + this.deleteOverlayBeams(); + this.hideTargetOverlay(); + this.hideCancelOverlay(); this.updateConnected = null; - this.inCoolIn = false; + this.state = TELEPORTER_STATES.IDLE; inTeleportMode = false; }; - this.update = function() { - if (isDisabled === 'both') { - return; - } - - if (teleporter.teleportHand === 'left') { - if (isDisabled === 'left') { - return; + this.deleteOverlayBeams = function() { + for (key in this.overlayLines) { + if (this.overlayLines[key] !== null) { + Overlays.deleteOverlay(this.overlayLines[key]); + this.overlayLines[key] = null; } - teleporter.leftRay(); - if ((leftPad.buttonValue === 0) && inTeleportMode === true) { - if (_this.inCoolIn === true) { - _this.exitTeleportMode(); - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - } else { - _this.teleport(); - } - return; - } - - } else { - if (isDisabled === 'right') { - return; - } - teleporter.rightRay(); - if ((rightPad.buttonValue === 0) && inTeleportMode === true) { - if (_this.inCoolIn === true) { - _this.exitTeleportMode(); - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - } else { - _this.teleport(); - } - return; - } - } - - }; - - this.rightRay = function() { - var pose = Controller.getPoseValue(Controller.Standard.RightHand); - var rightPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); - var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : - Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { - x: 1, - y: 0, - z: 0 - })); - - var rightPickRay = { - origin: rightPosition, - direction: Quat.getUp(rightRotation), - }; - - this.rightPickRay = rightPickRay; - - var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 50)); - - - var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity], true, true); - - if (rightIntersection.intersects) { - if (this.tooClose === true) { - this.hideTargetOverlay(); - - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(rightIntersection); - } else { - this.createCancelOverlay(); - } - } else { - if (this.inCoolIn === true) { - this.hideTargetOverlay(); - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(rightIntersection); - } else { - this.createCancelOverlay(); - } - } else { - this.hideCancelOverlay(); - - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); - if (this.targetOverlay !== null) { - this.updateTargetOverlay(rightIntersection); - } else { - this.createTargetOverlay(); - } - } - - - } - - } else { - - this.hideTargetOverlay(); - this.rightLineOn(rightPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT); } } + this.update = function() { + if (_this.state === TELEPORTER_STATES.IDLE) { + return; + } - this.leftRay = function() { - var pose = Controller.getPoseValue(Controller.Standard.LeftHand); - var leftPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); - var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : + // Get current hand pose information so that we can get the direction of the teleport beam + var pose = Controller.getPoseValue(handInfo[_this.activeHand].controllerInput); + var handPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); + var handRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 })); - var leftPickRay = { - origin: leftPosition, - direction: Quat.getUp(leftRotation), + var pickRay = { + origin: handPosition, + direction: Quat.getUp(handRotation), }; - this.leftPickRay = leftPickRay; + // We do up to 2 ray picks to find a teleport location. + // There are 2 types of teleport locations we are interested in: + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // 2. A seat. The seat can be visible or invisible. + // + // * In the first pass we pick against visible and invisible entities so that we can find invisible seats. + // We might hit an invisible entity that is not a seat, so we need to do a second pass. + // * In the second pass we pick against visible entities only. + // + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); - var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 50)); - - - var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity], true, true); - - if (leftIntersection.intersects) { - - if (this.tooClose === true) { - this.hideTargetOverlay(); - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(leftIntersection); - } else { - this.createCancelOverlay(); - } - } else { - if (this.inCoolIn === true) { - this.hideTargetOverlay(); - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(leftIntersection); - } else { - this.createCancelOverlay(); - } - } else { - this.hideCancelOverlay(); - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); - - if (this.targetOverlay !== null) { - this.updateTargetOverlay(leftIntersection); - } else { - this.createTargetOverlay(); - } - } - - - } - - - } else { + var teleportLocationType = getTeleportTargetType(intersection); + if (teleportLocationType === TARGET.INVISIBLE) { + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); + teleportLocationType = getTeleportTargetType(intersection); + } + if (teleportLocationType === TARGET.NONE) { this.hideTargetOverlay(); - this.leftLineOn(leftPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT); + this.hideCancelOverlay(); + this.hideSeatOverlay(); + + var farPosition = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, 50)); + this.updateLineOverlay(_this.activeHand, pickRay.origin, farPosition, COLORS_TELEPORT_CANNOT_TELEPORT); + } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { + this.hideTargetOverlay(); + this.hideSeatOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL); + this.updateDestinationOverlay(this.cancelOverlay, intersection); + } else if (teleportLocationType === TARGET.SURFACE) { + if (this.state === TELEPORTER_STATES.COOL_IN) { + this.hideTargetOverlay(); + this.hideSeatOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL); + this.updateDestinationOverlay(this.cancelOverlay, intersection); + } else { + this.hideCancelOverlay(); + this.hideSeatOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + this.updateDestinationOverlay(this.targetOverlay, intersection); + } + } else if (teleportLocationType === TARGET.SEAT) { + this.hideCancelOverlay(); + this.hideTargetOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_SEAT); + this.updateDestinationOverlay(this.seatOverlay, intersection); + } + + + if (((_this.activeHand == 'left' ? leftPad : rightPad).buttonValue === 0) && inTeleportMode === true) { + this.exitTeleportMode(); + this.hideCancelOverlay(); + this.hideTargetOverlay(); + this.hideSeatOverlay(); + + if (teleportLocationType === TARGET.NONE || teleportLocationType === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) { + // Do nothing + } else if (teleportLocationType === TARGET.SEAT) { + Entities.callEntityMethod(intersection.entityID, 'sit'); + } else if (teleportLocationType === TARGET.SURFACE) { + var offset = getAvatarFootOffset(); + intersection.intersection.y += offset; + MyAvatar.position = intersection.intersection; + HMD.centerUI(); + } } }; - this.rightLineOn = function(closePoint, farPoint, color) { - if (this.rightOverlayLine === null) { + this.updateLineOverlay = function(hand, closePoint, farPoint, color) { + if (this.overlayLines[hand] === null) { var lineProperties = { start: closePoint, end: farPoint, @@ -431,10 +316,10 @@ function Teleporter() { glow: 1.0 }; - this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); + this.overlayLines[hand] = Overlays.addOverlay("line3d", lineProperties); } else { - var success = Overlays.editOverlay(this.rightOverlayLine, { + var success = Overlays.editOverlay(this.overlayLines[hand], { start: closePoint, end: farPoint, color: color @@ -442,47 +327,19 @@ function Teleporter() { } }; - this.leftLineOn = function(closePoint, farPoint, color) { - if (this.leftOverlayLine === null) { - var lineProperties = { - ignoreRayIntersection: true, - start: closePoint, - end: farPoint, - color: color, - visible: true, - alpha: 1, - solid: true, - glow: 1.0, - drawInFront: true - }; - - this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - var success = Overlays.editOverlay(this.leftOverlayLine, { - start: closePoint, - end: farPoint, - color: color - }); - } - }; - this.rightOverlayOff = function() { - if (this.rightOverlayLine !== null) { - Overlays.deleteOverlay(this.rightOverlayLine); - this.rightOverlayLine = null; - } + this.hideCancelOverlay = function() { + Overlays.editOverlay(this.cancelOverlay, { visible: false }); }; - this.leftOverlayOff = function() { - if (this.leftOverlayLine !== null) { - Overlays.deleteOverlay(this.leftOverlayLine); - this.leftOverlayLine = null; - } + this.hideTargetOverlay = function() { + Overlays.editOverlay(this.targetOverlay, { visible: false }); }; - this.updateTargetOverlay = function(intersection) { - _this.intersection = intersection; + this.hideSeatOverlay = function() { + Overlays.editOverlay(this.seatOverlay, { visible: false }); + }; + this.updateDestinationOverlay = function(overlayID, intersection) { var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP); var euler = Quat.safeEulerAngles(rotation); var position = { @@ -491,115 +348,15 @@ function Teleporter() { z: intersection.intersection.z }; - this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal); var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); - Overlays.editOverlay(this.targetOverlay, { + Overlays.editOverlay(overlayID, { visible: true, position: position, rotation: towardUs }); }; - - this.updateCancelOverlay = function(intersection) { - _this.intersection = intersection; - - var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP); - var euler = Quat.safeEulerAngles(rotation); - var position = { - x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, - z: intersection.intersection.z - }; - - this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal); - var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); - - Overlays.editOverlay(this.cancelOverlay, { - visible: true, - position: position, - rotation: towardUs - }); - }; - - this.triggerHaptics = function() { - var hand = this.teleportHand === 'left' ? 0 : 1; - var haptic = Controller.triggerShortHapticPulse(0.2, hand); - }; - - this.teleport = function(value) { - - if (value === undefined) { - this.exitTeleportMode(); - } - - if (this.intersection !== null) { - if (this.tooClose === true) { - this.exitTeleportMode(); - this.hideCancelOverlay(); - return; - } - var offset = getAvatarFootOffset(); - this.intersection.intersection.y += offset; - this.exitTeleportMode(); - // Disable smooth arrival, possibly temporarily - //this.smoothArrival(); - MyAvatar.position = _this.intersection.intersection; - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - HMD.centerUI(); - } - }; - - this.findMidpoint = function(start, end) { - var xy = Vec3.sum(start, end); - var midpoint = Vec3.multiply(0.5, xy); - return midpoint - }; - - this.getArrivalPoints = function(startPoint, endPoint) { - var arrivalPoints = []; - var i; - var lastPoint; - - for (i = 0; i < NUMBER_OF_STEPS; i++) { - if (i === 0) { - lastPoint = startPoint; - } - var newPoint = _this.findMidpoint(lastPoint, endPoint); - lastPoint = newPoint; - arrivalPoints.push(newPoint); - } - - arrivalPoints.push(endPoint); - - return arrivalPoints; - }; - - this.smoothArrival = function() { - - _this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection); - _this.smoothArrivalInterval = Script.setInterval(function() { - if (_this.arrivalPoints.length === 0) { - Script.clearInterval(_this.smoothArrivalInterval); - HMD.centerUI(); - return; - } - var landingPoint = _this.arrivalPoints.shift(); - MyAvatar.position = landingPoint; - - if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - } - - }, SMOOTH_ARRIVAL_SPACING); - } - - this.createTargetOverlay(false); - this.createCancelOverlay(false); - } //related to repositioning the avatar after you teleport @@ -611,20 +368,16 @@ function getAvatarFootOffset() { var jointName = d.joint; if (jointName === "RightUpLeg") { upperLeg = d.translation.y; - } - if (jointName === "RightLeg") { + } else if (jointName === "RightLeg") { lowerLeg = d.translation.y; - } - if (jointName === "RightFoot") { + } else if (jointName === "RightFoot") { foot = d.translation.y; - } - if (jointName === "RightToeBase") { + } else if (jointName === "RightToeBase") { toe = d.translation.y; - } - if (jointName === "RightToe_End") { + } else if (jointName === "RightToe_End") { toeTop = d.translation.y; } - }) + }); var offset = upperLeg + lowerLeg + foot + toe + toeTop; offset = offset / 100; @@ -655,7 +408,6 @@ var rightTrigger = new Trigger('right'); var mappingName, teleportMapping; -var activationTimeout = null; var TELEPORT_DELAY = 0; function isMoving() { @@ -668,17 +420,44 @@ function isMoving() { } }; +function parseJSON(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } +} // When determininig whether you can teleport to a location, the normal of the // point that is being intersected with is looked at. If this normal is more // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then // you can't teleport there. -var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; -function isValidTeleportLocation(position, surfaceNormal) { +const MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; +function getTeleportTargetType(intersection) { + if (!intersection.intersects) { + return TARGET.NONE; + } + + var props = Entities.getEntityProperties(intersection.entityID, ['userData', 'visible']); + var data = parseJSON(props.userData); + if (data !== undefined && data.seat !== undefined) { + return TARGET.SEAT; + } + + if (!props.visible) { + return TARGET.INVISIBLE; + } + + var surfaceNormal = intersection.surfaceNormal; var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z); var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI); - return angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || - angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || - Vec3.distance(MyAvatar.position, position) <= TELEPORT_CANCEL_RANGE; + + if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || + angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || + Vec3.distance(MyAvatar.position, intersection.intersection) <= TELEPORT_CANCEL_RANGE) { + return TARGET.INVALID; + } else { + return TARGET.SURFACE; + } }; function registerMappings() { @@ -695,20 +474,13 @@ function registerMappings() { if (isDisabled === 'left' || isDisabled === 'both') { return; } - if (activationTimeout !== null) { - return - } if (leftTrigger.down()) { return; } if (isMoving() === true) { return; } - activationTimeout = Script.setTimeout(function() { - Script.clearTimeout(activationTimeout); - activationTimeout = null; - teleporter.enterTeleportMode('left') - }, TELEPORT_DELAY) + teleporter.enterTeleportMode('left') return; }); teleportMapping.from(Controller.Standard.RightPrimaryThumb) @@ -716,9 +488,6 @@ function registerMappings() { if (isDisabled === 'right' || isDisabled === 'both') { return; } - if (activationTimeout !== null) { - return - } if (rightTrigger.down()) { return; } @@ -726,11 +495,7 @@ function registerMappings() { return; } - activationTimeout = Script.setTimeout(function() { - teleporter.enterTeleportMode('right') - Script.clearTimeout(activationTimeout); - activationTimeout = null; - }, TELEPORT_DELAY) + teleporter.enterTeleportMode('right') return; }); }; @@ -741,18 +506,11 @@ var teleporter = new Teleporter(); Controller.enableMapping(mappingName); -Script.scriptEnding.connect(cleanup); - function cleanup() { teleportMapping.disable(); - teleporter.disableMappings(); - teleporter.deleteTargetOverlay(); - teleporter.deleteCancelOverlay(); - teleporter.turnOffOverlayBeams(); - if (teleporter.updateConnected !== null) { - Script.update.disconnect(teleporter.update); - } + teleporter.cleanup(); } +Script.scriptEnding.connect(cleanup); var isDisabled = false; var handleHandMessages = function(channel, message, sender) { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 9df4b2df92..106f226a33 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -245,18 +245,6 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See populateUserList(message.params.selected); UserActivityLogger.palAction("refresh", ""); break; - case 'updateGain': - data = message.params; - if (data['isReleased']) { - // isReleased=true happens once at the end of a cycle of dragging - // the slider about, but with same gain as last isReleased=false so - // we don't set the gain in that case, and only here do we want to - // send an analytic event. - UserActivityLogger.palAction("avatar_gain_changed", data['sessionId']); - } else { - Users.setAvatarGain(data['sessionId'], data['gain']); - } - break; case 'displayNameUpdate': if (MyAvatar.displayName !== message.params) { MyAvatar.displayName = message.params; diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js new file mode 100644 index 0000000000..2ba19231e0 --- /dev/null +++ b/scripts/tutorials/entity_scripts/sit.js @@ -0,0 +1,257 @@ +(function() { + Script.include("/~/system/libraries/utils.js"); + + var SETTING_KEY = "com.highfidelity.avatar.isSitting"; + var ROLE = "fly"; + var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; + var ANIMATION_FPS = 30; + var ANIMATION_FIRST_FRAME = 1; + var ANIMATION_LAST_FRAME = 10; + var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; + var RELEASE_TIME = 500; // ms + var RELEASE_DISTANCE = 0.2; // meters + var MAX_IK_ERROR = 20; + var DESKTOP_UI_CHECK_INTERVAL = 250; + var DESKTOP_MAX_DISTANCE = 5; + var SIT_DELAY = 25 + + this.entityID = null; + this.timers = {}; + this.animStateHandlerID = null; + + this.preload = function(entityID) { + this.entityID = entityID; + } + this.unload = function() { + if (MyAvatar.sessionUUID === this.getSeatUser()) { + this.sitUp(this.entityID); + } + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = null; + } + this.cleanupOverlay(); + } + + this.setSeatUser = function(user) { + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); + + if (user) { + userData.seat.user = user; + } else { + delete userData.seat.user; + } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); + } + this.getSeatUser = function() { + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); + + if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { + return null; + } + } + return userData.seat.user; + } + + this.checkSeatForAvatar = function() { + var seatUser = this.getSeatUser(); + var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); + for (var i in avatarIdentifiers) { + var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); + if (avatar && avatar.sessionUUID === seatUser) { + return true; + } + } + return false; + } + + this.sitDown = function() { + if (this.checkSeatForAvatar()) { + print("Someone is already sitting in that chair."); + return; + } + + this.setSeatUser(MyAvatar.sessionUUID); + + var previousValue = Settings.getValue(SETTING_KEY); + Settings.setValue(SETTING_KEY, this.entityID); + if (previousValue === "") { + MyAvatar.characterControllerEnabled = false; + MyAvatar.hmdLeanRecenterEnabled = false; + MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + MyAvatar.resetSensorsAndBody(); + } + + var that = this; + Script.setTimeout(function() { + var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); + + that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(that, that.update); + Controller.keyPressEvent.connect(that, that.keyPressed); + Controller.keyReleaseEvent.connect(that, that.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); + } + }, SIT_DELAY); + } + + this.sitUp = function() { + this.setSeatUser(null); + + if (Settings.getValue(SETTING_KEY) === this.entityID) { + MyAvatar.restoreRoleAnimation(ROLE); + MyAvatar.characterControllerEnabled = true; + MyAvatar.hmdLeanRecenterEnabled = true; + + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.clearPinOnJoint(index); + + MyAvatar.resetSensorsAndBody(); + + Script.setTimeout(function() { + MyAvatar.bodyPitch = 0.0; + MyAvatar.bodyRoll = 0.0; + }, SIT_DELAY); + + Settings.setValue(SETTING_KEY, ""); + } + + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + Controller.keyPressEvent.disconnect(this, this.keyPressed); + Controller.keyReleaseEvent.disconnect(this, this.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); + } + } + + this.sit = function () { + this.sitDown(); + } + + this.createOverlay = function() { + var text = "Click to sit"; + var textMargin = 0.05; + var lineHeight = 0.15; + + this.overlay = Overlays.addOverlay("text3d", { + position: { x: 0.0, y: 0.0, z: 0.0}, + dimensions: { x: 0.1, y: 0.1 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 255, green: 255, blue: 255 }, + topMargin: textMargin, + leftMargin: textMargin, + bottomMargin: textMargin, + rightMargin: textMargin, + text: text, + lineHeight: lineHeight, + alpha: 0.9, + backgroundAlpha: 0.9, + ignoreRayIntersection: true, + visible: true, + isFacingAvatar: true + }); + var textSize = Overlays.textSize(this.overlay, text); + var overlayDimensions = { + x: textSize.width + 2 * textMargin, + y: textSize.height + 2 * textMargin + } + var properties = Entities.getEntityProperties(this.entityID, ["position", "registrationPoint", "dimensions"]); + var yOffset = (1.0 - properties.registrationPoint.y) * properties.dimensions.y + (overlayDimensions.y / 2.0); + var overlayPosition = Vec3.sum(properties.position, { x: 0, y: yOffset, z: 0 }); + Overlays.editOverlay(this.overlay, { + position: overlayPosition, + dimensions: overlayDimensions + }); + } + this.cleanupOverlay = function() { + if (this.overlay !== null) { + Overlays.deleteOverlay(this.overlay); + this.overlay = null; + } + } + + + this.update = function(dt) { + if (MyAvatar.sessionUUID === this.getSeatUser()) { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); + var ikError = MyAvatar.getIKErrorOnLastSolve(); + if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { + print("IK error: " + ikError + ", distance from chair: " + avatarDistance); + this.sitUp(this.entityID); + } + } + } + this.keyPressed = function(event) { + if (isInEditMode()) { + return; + } + + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + var that = this; + this.timers[event.text] = Script.setTimeout(function() { + that.sitUp(); + }, RELEASE_TIME); + } + } + this.keyReleased = function(event) { + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + if (this.timers[event.text]) { + Script.clearTimeout(this.timers[event.text]); + delete this.timers[event.text]; + } + } + } + + this.canSitDesktop = function() { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); + return distanceFromSeat < DESKTOP_MAX_DISTANCE && !this.checkSeatForAvatar(); + } + + this.hoverEnterEntity = function(event) { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + return; + } + + var that = this; + this.interval = Script.setInterval(function() { + if (that.overlay === null) { + if (that.canSitDesktop()) { + that.createOverlay(); + } + } else if (!that.canSitDesktop()) { + that.cleanupOverlay(); + } + }, DESKTOP_UI_CHECK_INTERVAL); + } + this.hoverLeaveEntity = function(event) { + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = null; + } + this.cleanupOverlay(); + } + + this.clickDownOnEntity = function () { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + return; + } + if (this.canSitDesktop()) { + this.sitDown(); + } + } +});