diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index c8af792d8f..c47cfdb383 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -113,6 +113,7 @@ MyAvatar::MyAvatar(QThread* thread) : _recentModeReadings(MODE_READINGS_RING_BUFFER_SIZE), _bodySensorMatrix(), _goToPending(false), + _goToSafe(true), _goToPosition(), _goToOrientation(), _prevShouldDrawHead(true), @@ -148,7 +149,8 @@ MyAvatar::MyAvatar(QThread* thread) : }); connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); - + connect(&_skeletonModel->getRig(), &Rig::onLoadComplete, this, &MyAvatar::updateCollisionCapsuleCache); + connect(this, &MyAvatar::sensorToWorldScaleChanged, this, &MyAvatar::updateCollisionCapsuleCache); using namespace recording; _skeletonModel->flagAsCauterized(); @@ -254,6 +256,7 @@ MyAvatar::MyAvatar(QThread* thread) : }); connect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SIGNAL(onLoadComplete())); + _characterController.setDensity(_density); } @@ -509,7 +512,9 @@ void MyAvatar::update(float deltaTime) { if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) { // When needed and ready, arrange to check and fix. _physicsSafetyPending = false; - safeLanding(_goToPosition); // no-op if already safe + if (_goToSafe) { + safeLanding(_goToPosition); // no-op if already safe + } } Head* head = getHead(); @@ -3011,7 +3016,7 @@ void MyAvatar::goToFeetLocation(const glm::vec3& newPosition, void MyAvatar::goToLocation(const glm::vec3& newPosition, bool hasOrientation, const glm::quat& newOrientation, - bool shouldFaceLocation) { + bool shouldFaceLocation, bool withSafeLanding) { // Most cases of going to a place or user go through this now. Some possible improvements to think about in the future: // - It would be nice if this used the same teleport steps and smoothing as in the teleport.js script, as long as it @@ -3031,6 +3036,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, _goToPending = true; _goToPosition = newPosition; + _goToSafe = withSafeLanding; _goToOrientation = getWorldOrientation(); if (hasOrientation) { qCDebug(interfaceapp).nospace() << "MyAvatar goToLocation - new orientation is " @@ -3303,6 +3309,22 @@ bool MyAvatar::getCollisionsEnabled() { return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS; } +void MyAvatar::updateCollisionCapsuleCache() { + glm::vec3 start, end; + float radius; + getCapsule(start, end, radius); + QVariantMap capsule; + capsule["start"] = vec3toVariant(start); + capsule["end"] = vec3toVariant(end); + capsule["radius"] = QVariant(radius); + _collisionCapsuleCache.set(capsule); +} + +// thread safe +QVariantMap MyAvatar::getCollisionCapsule() const { + return _collisionCapsuleCache.get(); +} + void MyAvatar::setCharacterControllerEnabled(bool enabled) { qCDebug(interfaceapp) << "MyAvatar.characterControllerEnabled is deprecated. Use MyAvatar.collisionsEnabled instead."; setCollisionsEnabled(enabled); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5008190c33..139f1f6ea2 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1017,6 +1017,12 @@ public: */ Q_INVOKABLE bool getCollisionsEnabled(); + /**jsdoc + * @function MyAvatar.getCollisionCapsule + * @returns {object} + */ + Q_INVOKABLE QVariantMap getCollisionCapsule() const; + /**jsdoc * @function MyAvatar.setCharacterControllerEnabled * @param {boolean} enabled @@ -1180,11 +1186,12 @@ public slots: * @param {boolean} [hasOrientation=false] - Set to true to set the orientation of the avatar. * @param {Quat} [orientation=Quat.IDENTITY] - The new orientation for the avatar. * @param {boolean} [shouldFaceLocation=false] - Set to true to position the avatar a short distance away from + * @param {boolean} [withSafeLanding=true] - Set to false MyAvatar::safeLanding will not be called (used when teleporting). * the new position and orientate the avatar to face the position. */ void goToLocation(const glm::vec3& newPosition, bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), - bool shouldFaceLocation = false); + bool shouldFaceLocation = false, bool withSafeLanding = true); /**jsdoc * @function MyAvatar.goToLocation * @param {object} properties @@ -1498,6 +1505,7 @@ signals: private slots: void leaveDomain(); + void updateCollisionCapsuleCache(); protected: virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override; @@ -1722,6 +1730,7 @@ private: bool _goToPending { false }; bool _physicsSafetyPending { false }; + bool _goToSafe { true }; glm::vec3 _goToPosition; glm::quat _goToOrientation; diff --git a/interface/src/raypick/CollisionPick.cpp b/interface/src/raypick/CollisionPick.cpp index c21ee69b74..9aee76a3da 100644 --- a/interface/src/raypick/CollisionPick.cpp +++ b/interface/src/raypick/CollisionPick.cpp @@ -51,6 +51,7 @@ void buildObjectIntersectionsMap(IntersectionType intersectionType, const std::v QVariantMap collisionPointPair; collisionPointPair["pointOnPick"] = vec3toVariant(objectIntersection.testCollisionPoint); collisionPointPair["pointOnObject"] = vec3toVariant(objectIntersection.foundCollisionPoint); + collisionPointPair["normalOnPick"] = vec3toVariant(objectIntersection.collisionNormal); collisionPointPairs[objectIntersection.foundID].append(collisionPointPair); } @@ -397,7 +398,7 @@ PickResultPointer CollisionPick::getEntityIntersection(const CollisionRegion& pi } getShapeInfoReady(pick); - auto entityIntersections = _physicsEngine->contactTest(USER_COLLISION_MASK_ENTITIES, *_mathPick.shapeInfo, pick.transform, USER_COLLISION_GROUP_DYNAMIC, pick.threshold); + auto entityIntersections = _physicsEngine->contactTest(USER_COLLISION_MASK_ENTITIES, *_mathPick.shapeInfo, pick.transform, pick.collisionGroup, pick.threshold); filterIntersections(entityIntersections); return std::make_shared(pick, entityIntersections, std::vector()); } @@ -413,13 +414,13 @@ PickResultPointer CollisionPick::getAvatarIntersection(const CollisionRegion& pi } getShapeInfoReady(pick); - auto avatarIntersections = _physicsEngine->contactTest(USER_COLLISION_MASK_AVATARS, *_mathPick.shapeInfo, pick.transform, USER_COLLISION_GROUP_DYNAMIC, pick.threshold); + auto avatarIntersections = _physicsEngine->contactTest(USER_COLLISION_MASK_AVATARS, *_mathPick.shapeInfo, pick.transform, pick.collisionGroup, pick.threshold); filterIntersections(avatarIntersections); return std::make_shared(pick, std::vector(), avatarIntersections); } PickResultPointer CollisionPick::getHUDIntersection(const CollisionRegion& pick) { - return std::make_shared(pick.toVariantMap(), std::vector(), std::vector()); + return std::make_shared(pick, std::vector(), std::vector()); } Transform CollisionPick::getResultTransform() const { diff --git a/interface/src/raypick/CollisionPick.h b/interface/src/raypick/CollisionPick.h index fe0e5a6337..0662ab6c19 100644 --- a/interface/src/raypick/CollisionPick.h +++ b/interface/src/raypick/CollisionPick.h @@ -70,6 +70,9 @@ protected: CollisionRegion _mathPick; PhysicsEnginePointer _physicsEngine; QSharedPointer _cachedResource; + + // Options for what information to get from collision results + bool _includeNormals; }; #endif // hifi_CollisionPick_h \ No newline at end of file diff --git a/interface/src/raypick/PickScriptingInterface.cpp b/interface/src/raypick/PickScriptingInterface.cpp index 0273b084b2..b9693f6782 100644 --- a/interface/src/raypick/PickScriptingInterface.cpp +++ b/interface/src/raypick/PickScriptingInterface.cpp @@ -270,6 +270,8 @@ unsigned int PickScriptingInterface::createParabolaPick(const QVariant& properti * @property {Quat} orientation - The orientation of the collision region, relative to a parent if defined. * @property {float} threshold - The approximate minimum penetration depth for a test object to be considered in contact with the collision region. * The depth is measured in world space, but will scale with the parent if defined. +* @property {CollisionMask} [collisionGroup=8] - The type of object this collision pick collides as. Objects whose collision masks overlap with the pick's collision group +* will be considered colliding with the pick. * @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, or an overlay. * @property {number} parentJointIndex - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint) * @property {string} joint - If "Mouse," parents the pick to the mouse. If "Avatar," parents the pick to MyAvatar's head. Otherwise, parents to the joint of the given name on MyAvatar. diff --git a/interface/src/raypick/PickScriptingInterface.h b/interface/src/raypick/PickScriptingInterface.h index 4d99309618..36079cec2b 100644 --- a/interface/src/raypick/PickScriptingInterface.h +++ b/interface/src/raypick/PickScriptingInterface.h @@ -167,6 +167,7 @@ public: * @typedef {object} CollisionContact * @property {Vec3} pointOnPick A point representing a penetration of the object's surface into the volume of the pick, in world space. * @property {Vec3} pointOnObject A point representing a penetration of the pick's surface into the volume of the found object, in world space. + * @property {Vec3} normalOnPick The normalized vector pointing away from the pick, representing the direction of collision. */ /**jsdoc diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 056e745370..39f0ea34f6 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1438,6 +1438,8 @@ protected: ThreadSafeValueCache _farGrabLeftMatrixCache { glm::mat4() }; ThreadSafeValueCache _farGrabMouseMatrixCache { glm::mat4() }; + ThreadSafeValueCache _collisionCapsuleCache{ QVariantMap() }; + int getFauxJointIndex(const QString& name) const; float _audioLoudness { 0.0f }; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 871958f883..8343919ae1 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -936,19 +936,22 @@ struct AllContactsCallback : public btCollisionWorld::ContactResultCallback { const btCollisionObject* otherBody; btVector3 penetrationPoint; btVector3 otherPenetrationPoint; + btVector3 normal; if (colObj0->m_collisionObject == &collisionObject) { otherBody = colObj1->m_collisionObject; penetrationPoint = getWorldPoint(cp.m_localPointB, colObj1->getWorldTransform()); otherPenetrationPoint = getWorldPoint(cp.m_localPointA, colObj0->getWorldTransform()); + normal = -cp.m_normalWorldOnB; } else { otherBody = colObj0->m_collisionObject; penetrationPoint = getWorldPoint(cp.m_localPointA, colObj0->getWorldTransform()); otherPenetrationPoint = getWorldPoint(cp.m_localPointB, colObj1->getWorldTransform()); + normal = cp.m_normalWorldOnB; } // TODO: Give MyAvatar a motion state so we don't have to do this if ((m_collisionFilterMask & BULLET_COLLISION_GROUP_MY_AVATAR) && myAvatarCollisionObject && myAvatarCollisionObject == otherBody) { - contacts.emplace_back(Physics::getSessionUUID(), bulletToGLM(penetrationPoint), bulletToGLM(otherPenetrationPoint)); + contacts.emplace_back(Physics::getSessionUUID(), bulletToGLM(penetrationPoint), bulletToGLM(otherPenetrationPoint), bulletToGLM(normal)); return 0; } @@ -964,7 +967,7 @@ struct AllContactsCallback : public btCollisionWorld::ContactResultCallback { } // This is the correct object type. Add it to the list. - contacts.emplace_back(candidate->getObjectID(), bulletToGLM(penetrationPoint), bulletToGLM(otherPenetrationPoint)); + contacts.emplace_back(candidate->getObjectID(), bulletToGLM(penetrationPoint), bulletToGLM(otherPenetrationPoint), bulletToGLM(normal)); return 0; } diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 09b68d9a8b..d10be018b8 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -49,13 +49,15 @@ struct ContactTestResult { ContactTestResult(const ContactTestResult& contactTestResult) : foundID(contactTestResult.foundID), testCollisionPoint(contactTestResult.testCollisionPoint), - foundCollisionPoint(contactTestResult.foundCollisionPoint) { + foundCollisionPoint(contactTestResult.foundCollisionPoint), + collisionNormal(contactTestResult.collisionNormal) { } - ContactTestResult(QUuid foundID, glm::vec3 testCollisionPoint, glm::vec3 otherCollisionPoint) : + ContactTestResult(const QUuid& foundID, const glm::vec3& testCollisionPoint, const glm::vec3& otherCollisionPoint, const glm::vec3& collisionNormal) : foundID(foundID), testCollisionPoint(testCollisionPoint), - foundCollisionPoint(otherCollisionPoint) { + foundCollisionPoint(otherCollisionPoint), + collisionNormal(collisionNormal) { } QUuid foundID; @@ -63,6 +65,8 @@ struct ContactTestResult { glm::vec3 testCollisionPoint; // The deepest point of an intersection within the volume of the found object, in world space. glm::vec3 foundCollisionPoint; + // The normal vector of this intersection + glm::vec3 collisionNormal; }; using ContactMap = std::map; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index d59c58def8..6ecf9faca7 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -24,6 +24,7 @@ #include "SharedUtil.h" #include "shared/Bilateral.h" #include "Transform.h" +#include "PhysicsCollisionGroups.h" class QColor; class QUrl; @@ -264,6 +265,8 @@ public: * @property {Quat} orientation - The orientation of the collision region, relative to a parent if defined. * @property {float} threshold - The approximate minimum penetration depth for a test object to be considered in contact with the collision region. * The depth is measured in world space, but will scale with the parent if defined. +* @property {CollisionMask} [collisionGroup=8] - The type of object this collision pick collides as. Objects whose collision masks overlap with the pick's collision group +* will be considered colliding with the pick. * @property {Uuid} parentID - The ID of the parent, either an avatar, an entity, or an overlay. * @property {number} parentJointIndex - The joint of the parent to parent to, for example, the joints on the model of an avatar. (default = 0, no joint) * @property {string} joint - If "Mouse," parents the pick to the mouse. If "Avatar," parents the pick to MyAvatar's head. Otherwise, parents to the joint of the given name on MyAvatar. @@ -277,7 +280,8 @@ public: modelURL(collisionRegion.modelURL), shapeInfo(std::make_shared()), transform(collisionRegion.transform), - threshold(collisionRegion.threshold) + threshold(collisionRegion.threshold), + collisionGroup(collisionRegion.collisionGroup) { shapeInfo->setParams(collisionRegion.shapeInfo->getType(), collisionRegion.shapeInfo->getHalfExtents(), collisionRegion.modelURL.toString()); } @@ -316,6 +320,9 @@ public: if (pickVariant["orientation"].isValid()) { transform.setRotation(quatFromVariant(pickVariant["orientation"])); } + if (pickVariant["collisionGroup"].isValid()) { + collisionGroup = pickVariant["collisionGroup"].toUInt(); + } } QVariantMap toVariantMap() const override { @@ -330,6 +337,7 @@ public: collisionRegion["loaded"] = loaded; collisionRegion["threshold"] = threshold; + collisionRegion["collisionGroup"] = collisionGroup; collisionRegion["position"] = vec3toVariant(transform.getTranslation()); collisionRegion["orientation"] = quatToVariant(transform.getRotation()); @@ -341,12 +349,14 @@ public: return !std::isnan(threshold) && !(glm::any(glm::isnan(transform.getTranslation())) || glm::any(glm::isnan(transform.getRotation())) || - shapeInfo->getType() == SHAPE_TYPE_NONE); + shapeInfo->getType() == SHAPE_TYPE_NONE || + collisionGroup == 0); } bool operator==(const CollisionRegion& other) const { return loaded == other.loaded && threshold == other.threshold && + collisionGroup == other.collisionGroup && glm::all(glm::equal(transform.getTranslation(), other.transform.getTranslation())) && glm::all(glm::equal(transform.getRotation(), other.transform.getRotation())) && glm::all(glm::equal(transform.getScale(), other.transform.getScale())) && @@ -362,6 +372,10 @@ public: return false; } + if (collisionGroup == 0) { + return false; + } + return !shapeInfo->getPointCollection().size(); } @@ -372,7 +386,8 @@ public: // We can't compute the shapeInfo here without loading the model first, so we delegate that responsibility to the owning CollisionPick std::shared_ptr shapeInfo = std::make_shared(); Transform transform; - float threshold; + float threshold { 0.0f }; + uint16_t collisionGroup { USER_COLLISION_GROUP_MY_AVATAR }; }; namespace std { diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js index e4dd1c43fa..deaa934f99 100644 --- a/scripts/system/controllers/controllerModules/teleport.js +++ b/scripts/system/controllers/controllerModules/teleport.js @@ -62,40 +62,51 @@ Script.include("/~/system/libraries/controllers.js"); alpha: 1, width: 0.025 }; + var teleportPath = { color: COLORS_TELEPORT_CAN_TELEPORT, alpha: 1, width: 0.025 }; + var seatPath = { color: COLORS_TELEPORT_SEAT, alpha: 1, width: 0.025 }; + var teleportEnd = { type: "model", url: TARGET_MODEL_URL, dimensions: TARGET_MODEL_DIMENSIONS, ignorePickIntersection: true }; + var seatEnd = { type: "model", url: SEAT_MODEL_URL, dimensions: TARGET_MODEL_DIMENSIONS, ignorePickIntersection: true }; - - + + var collisionEnd = { + type: "shape", + shape: "box", + dimensions: { x: 1.0, y: 0.001, z: 1.0 }, + alpha: 0.0, + ignorePickIntersection: true + }; + var teleportRenderStates = [{name: "cancel", path: cancelPath}, {name: "teleport", path: teleportPath, end: teleportEnd}, - {name: "seat", path: seatPath, end: seatEnd}]; + {name: "seat", path: seatPath, end: seatEnd}, + {name: "collision", end: collisionEnd}]; var DEFAULT_DISTANCE = 8.0; var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; var ignoredEntities = []; - var TELEPORTER_STATES = { IDLE: 'idle', TARGETTING: 'targetting', @@ -104,8 +115,9 @@ Script.include("/~/system/libraries/controllers.js"); 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.) + COLLIDES: 'collides', // Insufficient space to accommodate the avatar capsule + DISCREPANCY: 'discrepancy', // We are not 100% sure the avatar will fit so we trigger safe landing SURFACE: 'surface', // The current target is a valid surface SEAT: 'seat' // The current target is a seat }; @@ -115,6 +127,7 @@ Script.include("/~/system/libraries/controllers.js"); function Teleporter(hand) { var _this = this; + this.init = false; this.hand = hand; this.buttonValue = 0; this.disabled = false; // used by the 'Hifi-Teleport-Disabler' message handler @@ -122,74 +135,138 @@ Script.include("/~/system/libraries/controllers.js"); this.state = TELEPORTER_STATES.IDLE; this.currentTarget = TARGET.INVALID; this.currentResult = null; + this.capsuleThreshold = 0.05; + this.pickHeightOffset = 0.05; this.getOtherModule = function() { var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter; return otherModule; }; - this.teleportParabolaHandVisible = Pointers.createPointer(PickType.Parabola, { - joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", - dirOffset: { x: 0, y: 1, z: 0.1 }, - posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, - filter: Picks.PICK_ENTITIES, - faceAvatar: true, - scaleWithAvatar: true, - centerEndY: false, - speed: speed, - accelerationAxis: accelerationAxis, - rotateAccelerationWithAvatar: true, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates, - maxDistance: 8.0 - }); - this.teleportParabolaHandInvisible = Pointers.createPointer(PickType.Parabola, { - joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", - dirOffset: { x: 0, y: 1, z: 0.1 }, - posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, - filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - scaleWithAvatar: true, - centerEndY: false, - speed: speed, - accelerationAxis: accelerationAxis, - rotateAccelerationWithAvatar: true, - renderStates: teleportRenderStates, - maxDistance: 8.0 - }); - this.teleportParabolaHeadVisible = Pointers.createPointer(PickType.Parabola, { - joint: "Avatar", - filter: Picks.PICK_ENTITIES, - faceAvatar: true, - scaleWithAvatar: true, - centerEndY: false, - speed: speed, - accelerationAxis: accelerationAxis, - rotateAccelerationWithAvatar: true, - renderStates: teleportRenderStates, - defaultRenderStates: teleportDefaultRenderStates, - maxDistance: 8.0 - }); - this.teleportParabolaHeadInvisible = Pointers.createPointer(PickType.Parabola, { - joint: "Avatar", - filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, - faceAvatar: true, - scaleWithAvatar: true, - centerEndY: false, - speed: speed, - accelerationAxis: accelerationAxis, - rotateAccelerationWithAvatar: true, - renderStates: teleportRenderStates, - maxDistance: 8.0 - }); + this.teleportHeadCollisionPick; + this.teleportHandCollisionPick; + this.teleportParabolaHandVisuals; + this.teleportParabolaHandCollisions; + this.teleportParabolaHeadVisuals; + this.teleportParabolaHeadCollisions; this.cleanup = function() { - Pointers.removePointer(this.teleportParabolaHandVisible); - Pointers.removePointer(this.teleportParabolaHandInvisible); - Pointers.removePointer(this.teleportParabolaHeadVisible); - Pointers.removePointer(this.teleportParabolaHeadInvisible); + Pointers.removePointer(_this.teleportParabolaHandVisuals); + Pointers.removePointer(_this.teleportParabolaHandCollisions); + Pointers.removePointer(_this.teleportParabolaHeadVisuals); + Pointers.removePointer(_this.teleportParabolaHeadCollisions); + Picks.removePick(_this.teleportHandCollisionPick); + Picks.removePick(_this.teleportHeadCollisionPick); }; + this.initPointers = function () { + if (_this.init) { + _this.cleanup(); + } + _this.teleportParabolaHandVisuals = Pointers.createPointer(PickType.Parabola, { + joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + dirOffset: { x: 0, y: 1, z: 0.1 }, + posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithAvatar: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHandCollisions = Pointers.createPointer(PickType.Parabola, { + joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + dirOffset: { x: 0, y: 1, z: 0.1 }, + posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithAvatar: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHeadVisuals = Pointers.createPointer(PickType.Parabola, { + joint: "Avatar", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithAvatar: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHeadCollisions = Pointers.createPointer(PickType.Parabola, { + joint: "Avatar", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithAvatar: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + maxDistance: 8.0 + }); + + + var capsuleData = MyAvatar.getCollisionCapsule(); + + var sensorToWorldScale = MyAvatar.getSensorToWorldScale(); + + var radius = capsuleData.radius / sensorToWorldScale; + var height = (Vec3.distance(capsuleData.start, capsuleData.end) + (capsuleData.radius * 2.0)) / sensorToWorldScale; + var capsuleRatio = 10.0 * radius / height; + var offset = _this.pickHeightOffset * capsuleRatio; + + _this.teleportHandCollisionPick = Picks.createPick(PickType.Collision, { + enabled: true, + parentID: Pointers.getPointerProperties(_this.teleportParabolaHandCollisions).renderStates["collision"].end, + filter: Picks.PICK_ENTITIES | Picks.PICK_AVATARS, + shape: { + shapeType: "capsule-y", + dimensions: { + x: radius * 2.0, + y: height - (radius * 2.0), + z: radius * 2.0 + } + }, + position: { x: 0, y: offset + height * 0.5, z: 0 }, + threshold: _this.capsuleThreshold + }); + + _this.teleportHeadCollisionPick = Picks.createPick(PickType.Collision, { + enabled: true, + parentID: Pointers.getPointerProperties(_this.teleportParabolaHeadCollisions).renderStates["collision"].end, + filter: Picks.PICK_ENTITIES | Picks.PICK_AVATARS, + shape: { + shapeType: "capsule-y", + dimensions: { + x: radius * 2.0, + y: height - (radius * 2.0), + z: radius * 2.0 + } + }, + position: { x: 0, y: offset + height * 0.5, z: 0 }, + threshold: _this.capsuleThreshold + }); + _this.init = true; + } + + _this.initPointers(); + this.axisButtonStateX = 0; // Left/right axis button pressed. this.axisButtonStateY = 0; // Up/down axis button pressed. this.BUTTON_TRANSITION_DELAY = 100; // Allow time for transition from direction buttons to touch-pad. @@ -254,52 +331,56 @@ Script.include("/~/system/libraries/controllers.js"); var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput); var mode = pose.valid ? _this.hand : 'head'; if (!pose.valid) { - Pointers.disablePointer(_this.teleportParabolaHandVisible); - Pointers.disablePointer(_this.teleportParabolaHandInvisible); - Pointers.enablePointer(_this.teleportParabolaHeadVisible); - Pointers.enablePointer(_this.teleportParabolaHeadInvisible); + Pointers.disablePointer(_this.teleportParabolaHandVisuals); + Pointers.disablePointer(_this.teleportParabolaHandCollisions); + Picks.disablePick(_this.teleportHandCollisionPick); + Pointers.enablePointer(_this.teleportParabolaHeadVisuals); + Pointers.enablePointer(_this.teleportParabolaHeadCollisions); + Picks.enablePick(_this.teleportHeadCollisionPick); } else { - Pointers.enablePointer(_this.teleportParabolaHandVisible); - Pointers.enablePointer(_this.teleportParabolaHandInvisible); - Pointers.disablePointer(_this.teleportParabolaHeadVisible); - Pointers.disablePointer(_this.teleportParabolaHeadInvisible); + Pointers.enablePointer(_this.teleportParabolaHandVisuals); + Pointers.enablePointer(_this.teleportParabolaHandCollisions); + Picks.enablePick(_this.teleportHandCollisionPick); + Pointers.disablePointer(_this.teleportParabolaHeadVisuals); + Pointers.disablePointer(_this.teleportParabolaHeadCollisions); + Picks.disablePick(_this.teleportHeadCollisionPick); } // We do up to 2 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" + // + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // and where the avatar capsule can be positioned without colliding + // // 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. + // The Collision Pick is currently parented to the end overlay on teleportParabolaXXXXCollisions // - var result; + // TODO + // Parent the collision Pick directly to the teleportParabolaXXXXVisuals and get rid of teleportParabolaXXXXCollisions + // + var result, collisionResult; if (mode === 'head') { - result = Pointers.getPrevPickResult(_this.teleportParabolaHeadInvisible); + result = Pointers.getPrevPickResult(_this.teleportParabolaHeadCollisions); + collisionResult = Picks.getPrevPickResult(_this.teleportHeadCollisionPick); } else { - result = Pointers.getPrevPickResult(_this.teleportParabolaHandInvisible); - } - - var teleportLocationType = getTeleportTargetType(result); - if (teleportLocationType === TARGET.INVISIBLE) { - if (mode === 'head') { - result = Pointers.getPrevPickResult(_this.teleportParabolaHeadVisible); - } else { - result = Pointers.getPrevPickResult(_this.teleportParabolaHandVisible); - } - teleportLocationType = getTeleportTargetType(result); + result = Pointers.getPrevPickResult(_this.teleportParabolaHandCollisions); + collisionResult = Picks.getPrevPickResult(_this.teleportHandCollisionPick); } + + var teleportLocationType = getTeleportTargetType(result, collisionResult); if (teleportLocationType === TARGET.NONE) { // Use the cancel default state this.setTeleportState(mode, "cancel", ""); - } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { + } else if (teleportLocationType === TARGET.INVALID) { this.setTeleportState(mode, "", "cancel"); - } else if (teleportLocationType === TARGET.SURFACE) { - this.setTeleportState(mode, "teleport", ""); + } else if (teleportLocationType === TARGET.COLLIDES) { + this.setTeleportState(mode, "cancel", "collision"); + } else if (teleportLocationType === TARGET.SURFACE || teleportLocationType === TARGET.DISCREPANCY) { + this.setTeleportState(mode, "teleport", "collision"); } else if (teleportLocationType === TARGET.SEAT) { - this.setTeleportState(mode, "", "seat"); + this.setTeleportState(mode, "collision", "seat"); } return this.teleport(result, teleportLocationType); }; @@ -314,10 +395,11 @@ Script.include("/~/system/libraries/controllers.js"); // Do nothing } else if (target === TARGET.SEAT) { Entities.callEntityMethod(result.objectID, 'sit'); - } else if (target === TARGET.SURFACE) { + } else if (target === TARGET.SURFACE || target === TARGET.DISCREPANCY) { var offset = getAvatarFootOffset(); result.intersection.y += offset; - MyAvatar.goToLocation(result.intersection, true, HMD.orientation, false); + var shouldLandSafe = target === TARGET.DISCREPANCY; + MyAvatar.goToLocation(result.intersection, true, HMD.orientation, false, shouldLandSafe); HMD.centerUI(); MyAvatar.centerBody(); } @@ -328,33 +410,38 @@ Script.include("/~/system/libraries/controllers.js"); }; this.disableLasers = function() { - Pointers.disablePointer(_this.teleportParabolaHandVisible); - Pointers.disablePointer(_this.teleportParabolaHandInvisible); - Pointers.disablePointer(_this.teleportParabolaHeadVisible); - Pointers.disablePointer(_this.teleportParabolaHeadInvisible); + Pointers.disablePointer(_this.teleportParabolaHandVisuals); + Pointers.disablePointer(_this.teleportParabolaHandCollisions); + Pointers.disablePointer(_this.teleportParabolaHeadVisuals); + Pointers.disablePointer(_this.teleportParabolaHeadCollisions); + Picks.disablePick(_this.teleportHeadCollisionPick); + Picks.disablePick(_this.teleportHandCollisionPick); }; this.setTeleportState = function(mode, visibleState, invisibleState) { if (mode === 'head') { - Pointers.setRenderState(_this.teleportParabolaHeadVisible, visibleState); - Pointers.setRenderState(_this.teleportParabolaHeadInvisible, invisibleState); + Pointers.setRenderState(_this.teleportParabolaHeadVisuals, visibleState); + Pointers.setRenderState(_this.teleportParabolaHeadCollisions, invisibleState); } else { - Pointers.setRenderState(_this.teleportParabolaHandVisible, visibleState); - Pointers.setRenderState(_this.teleportParabolaHandInvisible, invisibleState); + Pointers.setRenderState(_this.teleportParabolaHandVisuals, visibleState); + Pointers.setRenderState(_this.teleportParabolaHandCollisions, invisibleState); } }; this.setIgnoreEntities = function(entitiesToIgnore) { - Pointers.setIgnoreItems(this.teleportParabolaHandVisible, entitiesToIgnore); - Pointers.setIgnoreItems(this.teleportParabolaHandInvisible, entitiesToIgnore); - Pointers.setIgnoreItems(this.teleportParabolaHeadVisible, entitiesToIgnore); - Pointers.setIgnoreItems(this.teleportParabolaHeadInvisible, entitiesToIgnore); + Pointers.setIgnoreItems(this.teleportParabolaHandVisuals, entitiesToIgnore); + Pointers.setIgnoreItems(this.teleportParabolaHandCollisions, entitiesToIgnore); + Pointers.setIgnoreItems(this.teleportParabolaHeadVisuals, entitiesToIgnore); + Pointers.setIgnoreItems(this.teleportParabolaHeadCollisions, entitiesToIgnore); + Picks.setIgnoreItems(_this.teleportHeadCollisionPick, entitiesToIgnore); + Picks.setIgnoreItems(_this.teleportHandCollisionPick, entitiesToIgnore); }; } // related to repositioning the avatar after you teleport var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; + function getAvatarFootOffset() { // find a valid foot jointIndex @@ -395,7 +482,29 @@ Script.include("/~/system/libraries/controllers.js"); // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from your avatar's up, then // you can't teleport there. var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; - function getTeleportTargetType(result) { + var MAX_DISCREPANCY_DISTANCE = 1.0; + var MAX_DOT_SIGN = -0.6; + + function checkForMeshDiscrepancy(result, collisionResult) { + var intersectingObjects = collisionResult.intersectingObjects; + if (intersectingObjects.length > 0 && intersectingObjects.length < 3) { + for (var j = 0; j < collisionResult.intersectingObjects.length; j++) { + var intersectingObject = collisionResult.intersectingObjects[j]; + for (var i = 0; i < intersectingObject.collisionContacts.length; i++) { + var normal = intersectingObject.collisionContacts[i].normalOnPick; + var distanceToPick = Vec3.distance(intersectingObject.collisionContacts[i].pointOnPick, result.intersection); + var normalSign = Vec3.dot(normal, Quat.getUp(MyAvatar.orientation)); + if ((distanceToPick > MAX_DISCREPANCY_DISTANCE) || (normalSign > MAX_DOT_SIGN)) { + return false; + } + } + } + return true; + } + return false; + } + + function getTeleportTargetType(result, collisionResult) { if (result.type === Picks.INTERSECTED_NONE) { return TARGET.NONE; } @@ -410,9 +519,14 @@ Script.include("/~/system/libraries/controllers.js"); return TARGET.INVALID; } } - - if (!props.visible) { - return TARGET.INVISIBLE; + var isDiscrepancy = false; + if (collisionResult.collisionRegion != undefined) { + if (collisionResult.intersects) { + isDiscrepancy = checkForMeshDiscrepancy(result, collisionResult); + if (!isDiscrepancy) { + return TARGET.COLLIDES; + } + } } var surfaceNormal = result.surfaceNormal; @@ -420,6 +534,8 @@ Script.include("/~/system/libraries/controllers.js"); if (angle > MAX_ANGLE_FROM_UP_TO_TELEPORT) { return TARGET.INVALID; + } else if (isDiscrepancy) { + return TARGET.DISCREPANCY; } else { return TARGET.SURFACE; } @@ -513,7 +629,14 @@ Script.include("/~/system/libraries/controllers.js"); } } }; - + + MyAvatar.onLoadComplete.connect(function () { + Script.setTimeout(function () { + leftTeleporter.initPointers(); + rightTeleporter.initPointers(); + }, 500); + }); + Messages.subscribe('Hifi-Teleport-Disabler'); Messages.subscribe('Hifi-Teleport-Ignore-Add'); Messages.subscribe('Hifi-Teleport-Ignore-Remove');