From 4e70e8ed42084e480e8be6cd95220d479a55560a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 09:55:49 -0700 Subject: [PATCH 01/78] js call to ray-pick against avatars --- interface/src/avatar/Avatar.cpp | 9 ++++ interface/src/avatar/Avatar.h | 1 + interface/src/avatar/AvatarManager.cpp | 48 ++++++++++++++++++++ interface/src/avatar/AvatarManager.h | 4 ++ libraries/avatars/src/AvatarData.cpp | 23 ++++++++++ libraries/avatars/src/AvatarData.h | 15 ++++++ libraries/script-engine/src/ScriptEngine.cpp | 1 + 7 files changed, 101 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 39bb7eac17..8fd330db19 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1085,6 +1085,15 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } +void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { + ShapeInfo shapeInfo; + computeShapeInfo(shapeInfo); + glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight + start = getPosition() - glm::vec3(0, halfExtents.y, 0); + end = getPosition() + glm::vec3(0, halfExtents.y, 0); + radius = halfExtents.x; +} + void Avatar::setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 064f0a9533..b9f44613c7 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -154,6 +154,7 @@ public: virtual void rebuildCollisionShape(); virtual void computeShapeInfo(ShapeInfo& shapeInfo); + void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); AvatarMotionState* getMotionState() { return _motionState; } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 68d2eca2c0..f08a75ffda 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -398,3 +398,51 @@ AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) return findAvatar(sessionID); } + +RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& ray, + const QScriptValue& avatarIdsToInclude, + const QScriptValue& avatarIdsToDiscard) { + if (QThread::currentThread() != thread()) { + RayToAvatarIntersectionResult result; + QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + Q_ARG(const QScriptValue&, avatarIdsToInclude), + Q_ARG(const QScriptValue&, avatarIdsToDiscard), + Q_RETURN_ARG(RayToAvatarIntersectionResult, result)); + return result; + } + + RayToAvatarIntersectionResult result; + + QVector avatarsToInclude = qVectorEntityItemIDFromScriptValue(avatarIdsToInclude); + QVector avatarsToDiscard = qVectorEntityItemIDFromScriptValue(avatarIdsToDiscard); + + glm::vec3 normDirection = glm::normalize(ray.direction); + + for (auto avatarData : _avatarHash) { + auto avatar = std::static_pointer_cast(avatarData); + if ((avatarsToInclude.size() > 0 && !avatarsToInclude.contains(avatar->getID())) || + (avatarsToDiscard.size() > 0 && avatarsToDiscard.contains(avatar->getID()))) { + continue; + } + + float distance; + + glm::vec3 start; + glm::vec3 end; + float radius; + avatar->getCapsule(start, end, radius); + + bool intersects = findRayCapsuleIntersection(ray.origin, normDirection, start, end, radius, distance); + if (intersects && (!result.intersects || distance < result.distance)) { + result.intersects = true; + result.avatarID = avatar->getID(); + result.distance = distance; + } + } + + if (result.intersects) { + result.intersection = ray.origin + normDirection * result.distance; + } + + return result; +} diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 9be186301d..c49d566aa8 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,6 +70,10 @@ public: void addAvatarToSimulation(Avatar* avatar); + Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray, + const QScriptValue& avatarIdsToInclude = QScriptValue(), + const QScriptValue& avatarIdsToDiscard = QScriptValue()); + public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } void updateAvatarRenderStatus(bool shouldRenderAvatars); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 709cc76d01..e73702cd95 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "AvatarLogging.h" @@ -1681,3 +1682,25 @@ AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() { }); return result; } + +QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& value) { + QScriptValue obj = engine->newObject(); + obj.setProperty("intersects", value.intersects); + QScriptValue avatarIDValue = quuidToScriptValue(engine, value.avatarID); + obj.setProperty("avatarID", avatarIDValue); + obj.setProperty("distance", value.distance); + QScriptValue intersection = vec3toScriptValue(engine, value.intersection); + obj.setProperty("intersection", intersection); + return obj; +} + +void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, RayToAvatarIntersectionResult& value) { + value.intersects = object.property("intersects").toVariant().toBool(); + QScriptValue avatarIDValue = object.property("avatarID"); + quuidFromScriptValue(avatarIDValue, value.avatarID); + value.distance = object.property("distance").toVariant().toFloat(); + QScriptValue intersection = object.property("intersection"); + if (intersection.isValid()) { + vec3FromScriptValue(intersection, value.intersection); + } +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2dd1079b49..14b4f07471 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -495,4 +495,19 @@ public: void registerAvatarTypes(QScriptEngine* engine); +class RayToAvatarIntersectionResult { +public: +RayToAvatarIntersectionResult() : intersects(false), avatarID(), distance(0) {} + bool intersects; + QUuid avatarID; + float distance; + glm::vec3 intersection; +}; + +Q_DECLARE_METATYPE(RayToAvatarIntersectionResult) + +QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& results); +void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, RayToAvatarIntersectionResult& results); + + #endif // hifi_AvatarData_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index a5e3be8a43..9642aaf588 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -465,6 +465,7 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly); qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue); qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue); + qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); From 8a0d58a0c2410f0fc43f69b4842591ae7143bc87 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 11:15:01 -0700 Subject: [PATCH 02/78] fix invokeMethod in AvatarManager::findRayIntersection --- interface/src/avatar/AvatarManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index f08a75ffda..c5a9efeecc 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -405,6 +405,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& if (QThread::currentThread() != thread()) { RayToAvatarIntersectionResult result; QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + Q_ARG(const PickRay&, ray), Q_ARG(const QScriptValue&, avatarIdsToInclude), Q_ARG(const QScriptValue&, avatarIdsToDiscard), Q_RETURN_ARG(RayToAvatarIntersectionResult, result)); From 0f9f4749e7b849e85246639245e8271679dd7de5 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 11:18:46 -0700 Subject: [PATCH 03/78] fix invokeMethod in AvatarManager::findRayIntersection --- interface/src/avatar/AvatarManager.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c5a9efeecc..0eba27ee78 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -402,18 +402,16 @@ AvatarSharedPointer AvatarManager::getAvatarBySessionID(const QUuid& sessionID) RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& ray, const QScriptValue& avatarIdsToInclude, const QScriptValue& avatarIdsToDiscard) { + RayToAvatarIntersectionResult result; if (QThread::currentThread() != thread()) { - RayToAvatarIntersectionResult result; QMetaObject::invokeMethod(const_cast(this), "findRayIntersection", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(RayToAvatarIntersectionResult, result), Q_ARG(const PickRay&, ray), Q_ARG(const QScriptValue&, avatarIdsToInclude), - Q_ARG(const QScriptValue&, avatarIdsToDiscard), - Q_RETURN_ARG(RayToAvatarIntersectionResult, result)); + Q_ARG(const QScriptValue&, avatarIdsToDiscard)); return result; } - RayToAvatarIntersectionResult result; - QVector avatarsToInclude = qVectorEntityItemIDFromScriptValue(avatarIdsToInclude); QVector avatarsToDiscard = qVectorEntityItemIDFromScriptValue(avatarIdsToDiscard); From 630d5cfc825eef8026dd94145ae51623f1a6579e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 15:47:17 -0700 Subject: [PATCH 04/78] make trigger more sensitive, print out position and rotation when releasing something --- scripts/system/controllers/handControllerGrab.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f86ff158f6..2164340314 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -27,7 +27,7 @@ var WANT_DEBUG_SEARCH_NAME = null; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.75; // Squeezed far enough to complete distant grab +var TRIGGER_GRAB_VALUE = 0.50; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; @@ -1994,6 +1994,12 @@ function MyController(hand) { joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); + + grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + print("adjusted position: " + vec3toStr(grabbedProperties.localPosition)); + print("adjusted rotation: " + quatToStr(grabbedProperties.localRotation)); + + this.grabbedEntity = null; if (this.triggerSmoothedGrab()) { From b782ae667cc34c1495e28a091a6de775728aa6a6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 16:36:37 -0700 Subject: [PATCH 05/78] when one hand adjusts something equippd in the other, print out the new local position and rotation in case a content-creator wants to update userData --- .../system/controllers/handControllerGrab.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2164340314..8857d6c70c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1970,10 +1970,24 @@ function MyController(hand) { var noVelocity = false; if (this.grabbedEntity !== null) { + + // If this looks like the release after adjusting something still held in the other hand, print the position + // and rotation of the held thing to help content creators set the userData. + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); + if (grabData.refCount > 1) { + grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + if (grabbedProperties && grabbedProperties.localPosition && grabbedProperties.localRotation) { + print((this.hand === RIGHT_HAND ? '"LeftHand"' : '"RightHand"') + ":" + + '[{"x":' + grabbedProperties.localPosition.x + ', "y":' + grabbedProperties.localPosition.y + + ', "z":' + grabbedProperties.localPosition.z + '}, {"x":' + grabbedProperties.localRotation.x + + ', "y":' + grabbedProperties.localRotation.y + ', "z":' + grabbedProperties.localRotation.z + + ', "w":' + grabbedProperties.localRotation.w + '}]'); + } + } + if (this.actionID !== null) { Entities.deleteAction(this.grabbedEntity, this.actionID); // sometimes we want things to stay right where they are when we let go. - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); var releaseVelocityData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); if (releaseVelocityData.disableReleaseVelocity === true || // this next line allowed both: @@ -1994,12 +2008,6 @@ function MyController(hand) { joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); - - grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); - print("adjusted position: " + vec3toStr(grabbedProperties.localPosition)); - print("adjusted rotation: " + quatToStr(grabbedProperties.localRotation)); - - this.grabbedEntity = null; if (this.triggerSmoothedGrab()) { From 52d017ac743a6f3eeba49e6d8c9c904a19814494 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 28 Jun 2016 19:22:19 -0700 Subject: [PATCH 06/78] undo previous change --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 8857d6c70c..c92fb88f54 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -27,7 +27,7 @@ var WANT_DEBUG_SEARCH_NAME = null; var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing var TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab -var TRIGGER_GRAB_VALUE = 0.50; // Squeezed far enough to complete distant grab +var TRIGGER_GRAB_VALUE = 0.75; // Squeezed far enough to complete distant grab var TRIGGER_OFF_VALUE = 0.15; var BUMPER_ON_VALUE = 0.5; From 03ed36cf3303b41c63d94955b2532da28bb477f4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 09:13:52 -0700 Subject: [PATCH 07/78] take shape offset into account when getting avatar's capsule --- interface/src/avatar/Avatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 8fd330db19..4d9481f002 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1089,8 +1089,8 @@ void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { ShapeInfo shapeInfo; computeShapeInfo(shapeInfo); glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight - start = getPosition() - glm::vec3(0, halfExtents.y, 0); - end = getPosition() + glm::vec3(0, halfExtents.y, 0); + start = getPosition() - glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); + end = getPosition() + glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); radius = halfExtents.x; } From c3dbe5d9c43e23a84392442de5a036a4c0030b8a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 09:45:09 -0700 Subject: [PATCH 08/78] ray pick against avatar meshes rather than capsules --- interface/src/avatar/AvatarManager.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 0eba27ee78..e1fe03ae2e 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -425,13 +425,20 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& } float distance; + BoxFace face; + glm::vec3 surfaceNormal; - glm::vec3 start; - glm::vec3 end; - float radius; - avatar->getCapsule(start, end, radius); + SkeletonModelPointer avatarModel = avatar->getSkeletonModel(); + AABox avatarBounds = avatarModel->getRenderableMeshBound(); + if (!avatarBounds.findRayIntersection(ray.origin, normDirection, distance, face, surfaceNormal)) { + // ray doesn't intersect avatar's bounding-box + continue; + } + + QString extraInfo; + bool intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, + distance, face, surfaceNormal, extraInfo, true); - bool intersects = findRayCapsuleIntersection(ray.origin, normDirection, start, end, radius, distance); if (intersects && (!result.intersects || distance < result.distance)) { result.intersects = true; result.avatarID = avatar->getID(); From 9114ebb548aa2384fb16584c197704a272483511 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 09:53:26 -0700 Subject: [PATCH 09/78] remove unused code --- interface/src/avatar/Avatar.cpp | 9 --------- interface/src/avatar/Avatar.h | 1 - 2 files changed, 10 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 4d9481f002..39bb7eac17 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1085,15 +1085,6 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } -void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { - ShapeInfo shapeInfo; - computeShapeInfo(shapeInfo); - glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight - start = getPosition() - glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); - end = getPosition() + glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); - radius = halfExtents.x; -} - void Avatar::setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index b9f44613c7..064f0a9533 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -154,7 +154,6 @@ public: virtual void rebuildCollisionShape(); virtual void computeShapeInfo(ShapeInfo& shapeInfo); - void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); AvatarMotionState* getMotionState() { return _motionState; } From d53c7ae5d8021ffc11145fceb684539f2f789257 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 12:39:04 -0700 Subject: [PATCH 10/78] experimenting --- libraries/render-utils/src/Model.cpp | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 0470a238fc..f947474aac 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -456,6 +456,15 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { if (pickAgainstTriangles) { QVector thisMeshTriangles; + + glm::mat4 meshTransform; + if (mesh.clusters.size() > 0) { + int jointIndex = mesh.clusters[0].jointIndex; + meshTransform = _rig->getJointTransform(jointIndex); + } else { + meshTransform = mesh.modelTransform; + } + for (int j = 0; j < mesh.parts.size(); j++) { const FBXMeshPart& part = mesh.parts.at(j); @@ -474,10 +483,10 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 mv3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); // track the mesh parts in model space if (!atLeastOnePointInBounds) { @@ -517,9 +526,9 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); // track the mesh parts in model space if (!atLeastOnePointInBounds) { From fd062d09dc46a9c09ccd149ed6997fbeaef6f363 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 15:59:51 -0700 Subject: [PATCH 11/78] initial notes and research --- scripts/system/controllers/teleport.js | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 scripts/system/controllers/teleport.js diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js new file mode 100644 index 0000000000..fe7ab67ad9 --- /dev/null +++ b/scripts/system/controllers/teleport.js @@ -0,0 +1,63 @@ +//check if trigger is down +//if trigger is down, check if thumb is down +//if both are down, enter 'teleport mode' +//aim controller to change landing spot +//visualize aim + spot (line + circle) +//release trigger to teleport then exit teleport mode +//if thumb is release, exit teleport mode + + +//v2: show room boundaries when choosing a place to teleport +//v2: smooth fade screen in/out? +//v2: haptic feedback +//v2: particles for parabolic arc to landing spot + +//parabola: point count, point spacing, graphic thickness + + + +//Standard.LeftPrimaryThumb +//Standard.LT + + +//create a controller mapping and make sure to disable it when the script is stopped +var teleportMapping = Controller.newMapping('Hifi-Teleporter-Dev-' + Math.random()); +Script.scriptEnding.connect(teleportMapping.disable); + +// Gather the trigger data for smoothing. +clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); +clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); + + + +function thumbPad() { + +} + +thumbPad.prototype = { + down: function() { + return this.padValue === 0 ? true : false; + } +} + +function trigger() { + +} + +trigger.prototype = { + triggerPress: function(value) { + this.triggerValue = value; + }, + down: function() { + return this.triggerValue === 0 ? true : false; + } +} + + +teleportMapping.from(thumbPad.down).when(trigger.down).to(teleport); + +function teleport() { + +} + +function() {} \ No newline at end of file From 5f9c7b6ea5cb5df7392777a9976a53b0e9e2480a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 29 Jun 2016 17:02:49 -0700 Subject: [PATCH 12/78] trying to take joint information into account when raypicking against models --- interface/src/avatar/AvatarManager.cpp | 3 + libraries/render-utils/src/Model.cpp | 161 +++++++++++++++++++------ libraries/render-utils/src/Model.h | 5 + 3 files changed, 135 insertions(+), 34 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index e1fe03ae2e..a5a02bdaff 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -435,6 +435,9 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& continue; } + avatarModel->invalidCalculatedMeshBoxes(); + avatarModel->recalculateMeshBoxes(true); + QString extraInfo; bool intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, distance, face, surfaceNormal, extraInfo, true); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index f947474aac..4969585af4 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "AbstractViewStateInterface.h" #include "MeshPartPayload.h" @@ -441,32 +442,31 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(__FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; - if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { + if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || + (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { const FBXGeometry& geometry = getFBXGeometry(); int numberOfMeshes = geometry.meshes.size(); _calculatedMeshBoxes.resize(numberOfMeshes); _calculatedMeshTriangles.clear(); _calculatedMeshTriangles.resize(numberOfMeshes); _calculatedMeshPartBoxes.clear(); - for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); + + + + int okCount = 0; + int notOkCount = 0; + + + for (int meshIndex = 0; meshIndex < numberOfMeshes; meshIndex++) { + const FBXMesh& mesh = geometry.meshes.at(meshIndex); Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation); - _calculatedMeshBoxes[i] = AABox(scaledMeshExtents); + _calculatedMeshBoxes[meshIndex] = AABox(scaledMeshExtents); if (pickAgainstTriangles) { QVector thisMeshTriangles; - - glm::mat4 meshTransform; - if (mesh.clusters.size() > 0) { - int jointIndex = mesh.clusters[0].jointIndex; - meshTransform = _rig->getJointTransform(jointIndex); - } else { - meshTransform = mesh.modelTransform; - } - - for (int j = 0; j < mesh.parts.size(); j++) { - const FBXMeshPart& part = mesh.parts.at(j); + for (int partIndex = 0; partIndex < mesh.parts.size(); partIndex++) { + const FBXMeshPart& part = mesh.parts.at(partIndex); bool atLeastOnePointInBounds = false; AABox thisPartBounds; @@ -483,10 +483,62 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 mv3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + glm::vec3 v[3]; + int ok = 0; + if (meshIndex < _meshStates.size()) { + int quadPointIndexes[ 4 ] = {i0, i1, i2, i3}; + for (int pointInQuadIndex = 0; pointInQuadIndex < 4; pointInQuadIndex++) { + int vertexIndex = quadPointIndexes[pointInQuadIndex]; + glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; + glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; + + bool vSet = false; + for (int ci = 0; ci < 4; ci++) { + int clusterIndex = (int) clusterIndices[ci]; + float clusterWeight = clusterWeights[ci]; + if (clusterIndex < 0 || + clusterIndex >= mesh.clusters.size() || + clusterWeight == 0.0f) { + continue; + } + const FBXCluster& cluster = mesh.clusters.at(clusterIndex); + auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; + glm::vec3 meshVertex = mesh.vertices[vertexIndex]; + glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); + glm::vec3 tpoint = clusterWeight * (_translation + fuh); + v[pointInQuadIndex] += tpoint; + vSet = true; + } + if (vSet) { + ok++; + } + } + } + + + glm::vec3 v0; + glm::vec3 v1; + glm::vec3 v2; + glm::vec3 v3; + + glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + + if (ok == 4) { + okCount++; + v0 = v[0]; + v1 = v[1]; + v2 = v[2]; + v3 = v[3]; + } else { + notOkCount++; + v0 = calculateScaledOffsetPoint(mv0); + v1 = calculateScaledOffsetPoint(mv1); + v2 = calculateScaledOffsetPoint(mv2); + v3 = calculateScaledOffsetPoint(mv3); + } // track the mesh parts in model space if (!atLeastOnePointInBounds) { @@ -499,11 +551,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv2; thisPartBounds += mv3; - glm::vec3 v0 = calculateScaledOffsetPoint(mv0); - glm::vec3 v1 = calculateScaledOffsetPoint(mv1); - glm::vec3 v2 = calculateScaledOffsetPoint(mv2); - glm::vec3 v3 = calculateScaledOffsetPoint(mv3); - // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; @@ -514,7 +561,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); - } } @@ -526,9 +572,57 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; - glm::vec3 mv0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 v[3]; + int ok = 0; + if (meshIndex < _meshStates.size()) { + int trianglePointIndexes[ 3 ] = {i0, i1, i2}; + for (int pointInTriIndex = 0; pointInTriIndex < 3; pointInTriIndex++) { + int vertexIndex = trianglePointIndexes[pointInTriIndex]; + glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; + glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; + + bool vSet = false; + for (int ci = 0; ci < 4; ci++) { + int clusterIndex = (int) clusterIndices[ci]; + float clusterWeight = clusterWeights[ci]; + if (clusterIndex < 0 || + clusterIndex >= mesh.clusters.size() || + clusterWeight == 0.0f) { + continue; + } + const FBXCluster& cluster = mesh.clusters.at(clusterIndex); + auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; + glm::vec3 meshVertex = mesh.vertices[vertexIndex]; + glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); + glm::vec3 tpoint = clusterWeight * (_translation + fuh); + v[pointInTriIndex] += tpoint; + vSet = true; + } + if (vSet) { + ok++; + } + } + } + + glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + + glm::vec3 v0; + glm::vec3 v1; + glm::vec3 v2; + + if (ok == 3) { + okCount++; + v0 = v[0]; + v1 = v[1]; + v2 = v[2]; + } else { + notOkCount++; + v0 = calculateScaledOffsetPoint(mv0); + v1 = calculateScaledOffsetPoint(mv1); + v2 = calculateScaledOffsetPoint(mv2); + } // track the mesh parts in model space if (!atLeastOnePointInBounds) { @@ -540,21 +634,20 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv1; thisPartBounds += mv2; - glm::vec3 v0 = calculateScaledOffsetPoint(mv0); - glm::vec3 v1 = calculateScaledOffsetPoint(mv1); - glm::vec3 v2 = calculateScaledOffsetPoint(mv2); - Triangle tri = { v0, v1, v2 }; thisMeshTriangles.push_back(tri); } } - _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; + _calculatedMeshPartBoxes[QPair(meshIndex, partIndex)] = thisPartBounds; } - _calculatedMeshTriangles[i] = thisMeshTriangles; + _calculatedMeshTriangles[meshIndex] = thisMeshTriangles; _calculatedMeshPartBoxesValid = true; } } + + qDebug() << "ok = " << okCount << " not-ok =" << notOkCount; + _calculatedMeshBoxesValid = true; _calculatedMeshTrianglesValid = pickAgainstTriangles; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 6a7c9ec560..66c5eb019e 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -316,11 +316,14 @@ protected: float getLimbLength(int jointIndex) const; /// Allow sub classes to force invalidating the bboxes +public: void invalidCalculatedMeshBoxes() { _calculatedMeshBoxesValid = false; _calculatedMeshPartBoxesValid = false; _calculatedMeshTrianglesValid = false; } +protected: + // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; @@ -357,7 +360,9 @@ protected: bool _calculatedMeshTrianglesValid; QMutex _mutex; +public: void recalculateMeshBoxes(bool pickAgainstTriangles = false); +protected: void segregateMeshGroups(); // used to calculate our list of translucent vs opaque meshes static model::MaterialPointer _collisionHullMaterial; From 4b4e38a57cc8e9773965701610b44de4b4f35ab3 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 29 Jun 2016 18:28:39 -0700 Subject: [PATCH 13/78] end of day --- scripts/system/controllers/teleport.js | 129 ++++++++++++++++++++----- 1 file changed, 103 insertions(+), 26 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index fe7ab67ad9..e2d0dcff8d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -14,8 +14,6 @@ //parabola: point count, point spacing, graphic thickness - - //Standard.LeftPrimaryThumb //Standard.LT @@ -24,40 +22,119 @@ var teleportMapping = Controller.newMapping('Hifi-Teleporter-Dev-' + Math.random()); Script.scriptEnding.connect(teleportMapping.disable); -// Gather the trigger data for smoothing. -clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); -clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +// peek at the trigger and thumbs to store their values +teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); +teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); +teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); +teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - -function thumbPad() { +function ThumbPad() { } -thumbPad.prototype = { - down: function() { - return this.padValue === 0 ? true : false; - } -} - -function trigger() { - -} - -trigger.prototype = { - triggerPress: function(value) { - this.triggerValue = value; +ThumbPad.prototype = { + buttonPress: function(value) { + print('pad press: ' + value) + this.buttonValue = value; }, down: function() { - return this.triggerValue === 0 ? true : false; + return this.buttonValue === 0 ? true : false; } } - -teleportMapping.from(thumbPad.down).when(trigger.down).to(teleport); - -function teleport() { +function Trigger() { } -function() {} \ No newline at end of file +Trigger.prototype = { + buttonPress: function(value) { + print('trigger press: ' + value) + this.buttonValue = value; + }, + down: function() { + return this.buttonValue === 0 ? true : false; + } +} + +teleportMapping.from(leftPad.down).when(leftTrigger.down).to(teleporter.enterTeleportMode); +teleportMapping.from(rightPad.down).when(rightTrigger.down).to(teleporter.enterTeleportMode); + + +function Teleporter() { + +} + +Teleporter.prototype = { + enterTeleportMode: function(value) { + print('value on enter: ' + value); + }, + exitTeleportMode: function(value) { + print('value on exit: ' + value); + }, + teleport: function(value) { + //todo + //get the y position of the teleport landing spot + print('value on teleport: ' + value) + var properties = Entities.getEntityProperties(_this.entityID); + var offset = getAvatarFootOffset(); + properties.position.y += offset; + + print('OFFSET IS::: ' + JSON.stringify(offset)) + print('TELEPORT POSITION IS:: ' + JSON.stringify(properties.position)); + MyAvatar.position = properties.position; + } +} + +function getAvatarFootOffset() { + var data = getJointData(); + var upperLeg, lowerLeg, foot, toe, toeTop; + data.forEach(function(d) { + + var jointName = d.joint; + if (jointName === "RightUpLeg") { + upperLeg = d.translation.y; + } + if (jointName === "RightLeg") { + lowerLeg = d.translation.y; + } + if (jointName === "RightFoot") { + foot = d.translation.y; + } + if (jointName === "RightToeBase") { + toe = d.translation.y; + } + if (jointName === "RightToe_End") { + toeTop = d.translation.y + } + }) + + var myPosition = MyAvatar.position; + var offset = upperLeg + lowerLeg + foot + toe + toeTop; + offset = offset / 100; + return offset +}; + +function getJointData() { + var allJointData = []; + var jointNames = MyAvatar.jointNames; + jointNames.forEach(function(joint, index) { + var translation = MyAvatar.getJointTranslation(index); + var rotation = MyAvatar.getJointRotation(index) + allJointData.push({ + joint: joint, + index: index, + translation: translation, + rotation: rotation + }); + }); + + return allJointData; +} + + +var leftPad = new ThumbPad(); +var rightPad = new ThumbPad(); +var leftTrigger = new Trigger(); +var rightTrigger = new Trigger(); +var teleporter = new Teleporter(); \ No newline at end of file From 82e074de0fe1c1dc08eb55170ab19b3a23b2508d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 30 Jun 2016 16:05:44 -0700 Subject: [PATCH 14/78] working teleporter --- scripts/system/controllers/teleport.js | 359 +++++++++++++++++++++---- 1 file changed, 300 insertions(+), 59 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index e2d0dcff8d..5ad09d53a6 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -6,86 +6,295 @@ //release trigger to teleport then exit teleport mode //if thumb is release, exit teleport mode - //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? -//v2: haptic feedback -//v2: particles for parabolic arc to landing spot +//v2: haptic feedbackasd -//parabola: point count, point spacing, graphic thickness +var inTeleportMode = false; -//Standard.LeftPrimaryThumb -//Standard.LT +function ThumbPad(hand) { + this.hand = hand; + var _this = this; + this.buttonPress = function(value) { + print('jbp pad press: ' + value + " on: " + _this.hand) + _this.buttonValue = value; + }; -//create a controller mapping and make sure to disable it when the script is stopped -var teleportMapping = Controller.newMapping('Hifi-Teleporter-Dev-' + Math.random()); -Script.scriptEnding.connect(teleportMapping.disable); - -// peek at the trigger and thumbs to store their values -teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); -teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); -teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); -teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - -function ThumbPad() { + this.down = function() { + return _this.buttonValue === 1 ? 1.0 : 0.0; + }; } -ThumbPad.prototype = { - buttonPress: function(value) { - print('pad press: ' + value) - this.buttonValue = value; - }, - down: function() { - return this.buttonValue === 0 ? true : false; - } +function Trigger(hand) { + this.hand = hand; + var _this = this; + + this.buttonPress = function(value) { + print('jbp trigger press: ' + value + " on: " + _this.hand) + _this.buttonValue = value; + + }; + + this.down = function() { + return _this.buttonValue === 1 ? 1.0 : 0.0; + }; } -function Trigger() { - -} - -Trigger.prototype = { - buttonPress: function(value) { - print('trigger press: ' + value) - this.buttonValue = value; - }, - down: function() { - return this.buttonValue === 0 ? true : false; - } -} - -teleportMapping.from(leftPad.down).when(leftTrigger.down).to(teleporter.enterTeleportMode); -teleportMapping.from(rightPad.down).when(rightTrigger.down).to(teleporter.enterTeleportMode); - function Teleporter() { + var _this = this; + this.targetProps = null; + this.rightOverlayLine = null; + this.leftOverlayLine = null; + this.initialize = function() { + print('jbp initialize') + this.createMappings(); + this.disableGrab(); + _this.targetEntity = Entities.addEntity({ + name: 'Hifi-Teleporter-Target-Entity', + position: MyAvatar.position, + type: 'Sphere', + dimensions: { + x: 0.2, + y: 0.2, + z: 0.2 + }, + color: { + red: 255, + green: 255, + blue: 255 + }, + collisionless: true, + collidesWith: '', + visible: true + }); + }; -} -Teleporter.prototype = { - enterTeleportMode: function(value) { - print('value on enter: ' + value); - }, - exitTeleportMode: function(value) { - print('value on exit: ' + value); - }, - teleport: function(value) { + this.createMappings = function() { + print('jbp create mappings internal'); + // peek at the trigger and thumbs to store their values + teleporter.telporterMappingInternalName='Hifi-Teleporter-Internal-Dev-' + Math.random(); + teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); + + Controller.enableMapping(teleporter.telporterMappingInternalName); + }; + + this.disableMappings = function() { + print('jbp disable mappings internal') + Controller.disableMapping(teleporter.telporterMappingInternalName); + }; + + this.enterTeleportMode = function(hand) { + if (inTeleportMode === true) { + return + } + print('jbp hand on entering teleport mode: ' + hand); + inTeleportMode = true; + this.teleportHand = hand; + this.initialize(); + this.updateConnected = true; + Script.update.connect(this.update); + }; + + this.exitTeleportMode = function(value) { + print('jbp value on exit: ' + value); + this.disableMappings(); + this.rightOverlayOff(); + this.leftOverlayOff(); + Entities.deleteEntity(_this.targetEntity); + this.enableGrab(); + Script.update.disconnect(this.update); + this.updateConnected = false; + inTeleportMode = false; + + }; + + this.update = function() { + //print('in teleporter update') + if (rightPad.buttonValue === 0 || leftPad.buttonValue === 0) { + print('JBP THUMB RELEASED SHOULD EXIT') + _this.exitTeleportMode(); + return; + } + + if (teleporter.teleportHand === 'left') { + teleporter.leftRay(); + if (leftTrigger.buttonValue === 0) { + _this.teleport(); + } + } else { + teleporter.rightRay(); + if (rightTrigger.buttonValue === 0) { + _this.teleport(); + } + } + + + }; + + this.rightRay = function() { + + var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); + + var rightRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).rotation) + + var rightPickRay = { + origin: rightPosition, + direction: Quat.getUp(rightRotation), + }; + + this.rightPickRay = rightPickRay; + + var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); + this.rightLineOn(rightPickRay.origin, location, { + red: 255, + green: 0, + blue: 0 + }); + var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); + + if (rightIntersection.intersects) { + this.updateTargetEntity(rightIntersection); + }; + }; + + this.leftRay = function() { + var leftPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).translation), MyAvatar.position); + + + var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) + var leftPickRay = { + origin: leftPosition, + direction: Quat.getUp(leftRotation), + }; + + this.leftPickRay = leftPickRay; + + var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); + this.leftLineOn(leftPickRay.origin, location, { + red: 0, + green: 255, + blue: 0 + }); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); + + if (leftIntersection.intersects) { + this.updateTargetEntity(leftIntersection); + }; + }; + + this.rightLineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.rightOverlayLine === null) { + var lineProperties = { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, // always ignore this + visible: true, + alpha: 1 + }; + + this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.rightOverlayLine, { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + visible: true, + ignoreRayIntersection: true, // always ignore this + alpha: 1 + }); + } + }; + + this.leftLineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.leftOverlayLine === null) { + var lineProperties = { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, // always ignore this + visible: true, + alpha: 1 + }; + + this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.leftOverlayLine, { + lineWidth: 5, + start: closePoint, + end: farPoint, + color: color, + visible: true, + ignoreRayIntersection: true, // always ignore this + alpha: 1 + }); + } + }; + + this.rightOverlayOff = function() { + if (this.rightOverlayLine !== null) { + Overlays.deleteOverlay(this.rightOverlayLine); + this.rightOverlayLine=null; + } + }; + + this.leftOverlayOff = function() { + if (this.leftOverlayLine !== null) { + Overlays.deleteOverlay(this.leftOverlayLine); + this.leftOverlayLine=null; + } + }; + + this.updateTargetEntity = function(intersection) { + var targetProps = Entities.getEntityProperties(this.targetEntity); + var position = { + x: intersection.intersection.x, + y: intersection.intersection.y + targetProps.dimensions.y / 2, + z: intersection.intersection.z + } + Entities.editEntity(this.targetEntity, { + position: position + }); + + + }; + + this.disableGrab = function() { + Messages.sendLocalMessage('Hifi-Hand-Disabler', 'both'); + }; + + this.enableGrab = function() { + Messages.sendLocalMessage('Hifi-Hand-Disabler', 'none'); + }; + + this.teleport = function(value) { //todo //get the y position of the teleport landing spot print('value on teleport: ' + value) - var properties = Entities.getEntityProperties(_this.entityID); + var properties = Entities.getEntityProperties(teleporter.targetEntity); var offset = getAvatarFootOffset(); properties.position.y += offset; print('OFFSET IS::: ' + JSON.stringify(offset)) print('TELEPORT POSITION IS:: ' + JSON.stringify(properties.position)); MyAvatar.position = properties.position; - } + + this.exitTeleportMode(); + }; } +//related to repositioning the avatar after you teleport function getAvatarFootOffset() { var data = getJointData(); var upperLeg, lowerLeg, foot, toe, toeTop; @@ -133,8 +342,40 @@ function getJointData() { } -var leftPad = new ThumbPad(); -var rightPad = new ThumbPad(); -var leftTrigger = new Trigger(); -var rightTrigger = new Trigger(); -var teleporter = new Teleporter(); \ No newline at end of file +var leftPad = new ThumbPad('left'); +var rightPad = new ThumbPad('right'); +var leftTrigger = new Trigger('left'); +var rightTrigger = new Trigger('right'); + +//create a controller mapping and make sure to disable it when the script is stopped + +var mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); +var teleportMapping = Controller.newMapping(mappingName); +teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); +teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); +teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); +teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + +teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { + teleporter.enterTeleportMode('left') +}); +teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { + teleporter.enterTeleportMode('right') +}); + +var teleporter = new Teleporter(); + +Controller.enableMapping(mappingName); + +Script.scriptEnding.connect(cleanup); + +function cleanup() { + teleportMapping.disable(); + teleporter.disableMappings(); + teleporter.rightOverlayOff(); + teleporter.leftOverlayOff(); + Entities.deleteEntity(teleporter.targetEntity); + if (teleporter.updateConnected !== null) { + Script.update.disconnect(teleporter.update); + } +} \ No newline at end of file From 104ee63a772f4907da558215162610754219acc6 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 30 Jun 2016 17:05:16 -0700 Subject: [PATCH 15/78] notes --- scripts/system/controllers/teleport.js | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5ad09d53a6..728f34faea 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,14 +1,25 @@ -//check if trigger is down -//if trigger is down, check if thumb is down -//if both are down, enter 'teleport mode' -//aim controller to change landing spot -//visualize aim + spot (line + circle) -//release trigger to teleport then exit teleport mode -//if thumb is release, exit teleport mode +//v1 +//check if trigger is down xxx +//if trigger is down, check if thumb is down xxx +//if both are down, enter 'teleport mode' xxx +//aim controller to change landing spot xxx +//visualize aim + spot (line + circle) xxx +//release trigger to teleport then exit teleport mode xxx +//if thumb is release, exit teleport mode xxx //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? -//v2: haptic feedbackasd +//v2: haptic feedback + + +// alternate notes for philip: +// try just thumb to teleport +// cancel if destination is within ~1m of current location + + +//try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) + + var inTeleportMode = false; From b2e407a8e186b093d3721fada0f098d25eabafc1 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 00:48:29 -0700 Subject: [PATCH 16/78] fix vive bug --- scripts/system/controllers/teleport.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 728f34faea..42ffd2dac9 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -87,9 +87,9 @@ function Teleporter() { this.createMappings = function() { print('jbp create mappings internal'); // peek at the trigger and thumbs to store their values - teleporter.telporterMappingInternalName='Hifi-Teleporter-Internal-Dev-' + Math.random(); - teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); - + teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); + teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); + Controller.enableMapping(teleporter.telporterMappingInternalName); }; @@ -124,20 +124,24 @@ function Teleporter() { }; this.update = function() { - //print('in teleporter update') - if (rightPad.buttonValue === 0 || leftPad.buttonValue === 0) { - print('JBP THUMB RELEASED SHOULD EXIT') - _this.exitTeleportMode(); - return; - } + //print('in teleporter update') + if (teleporter.teleportHand === 'left') { teleporter.leftRay(); + if (leftPad.buttonValue === 0) { + _this.exitTeleportMode(); + return; + } if (leftTrigger.buttonValue === 0) { _this.teleport(); } } else { teleporter.rightRay(); + if (rightPad.buttonValue === 0) { + _this.exitTeleportMode(); + return; + } if (rightTrigger.buttonValue === 0) { _this.teleport(); } @@ -256,14 +260,14 @@ function Teleporter() { this.rightOverlayOff = function() { if (this.rightOverlayLine !== null) { Overlays.deleteOverlay(this.rightOverlayLine); - this.rightOverlayLine=null; + this.rightOverlayLine = null; } }; this.leftOverlayOff = function() { if (this.leftOverlayLine !== null) { Overlays.deleteOverlay(this.leftOverlayLine); - this.leftOverlayLine=null; + this.leftOverlayLine = null; } }; From 0c60d87d3be8bfd5b5298e7db6f5f3e2b0f43405 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 10:32:37 -0700 Subject: [PATCH 17/78] fix some touch mappings --- interface/resources/controllers/oculus_touch.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index b59bf54e5b..3def439221 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -1,10 +1,10 @@ { "name": "Oculus Touch to Standard", "channels": [ - { "from": "OculusTouch.A", "to": "Standard.A" }, - { "from": "OculusTouch.B", "to": "Standard.B" }, - { "from": "OculusTouch.X", "to": "Standard.X" }, - { "from": "OculusTouch.Y", "to": "Standard.Y" }, + { "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb" }, + { "from": "OculusTouch.B", "to": "Standard.RightSecondaryThumb" }, + { "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb" }, + { "from": "OculusTouch.Y", "to": "Standard.LeftSecondaryThumb" }, { "from": "OculusTouch.LY", "filters": "invert", "to": "Standard.LY" }, { "from": "OculusTouch.LX", "to": "Standard.LX" }, From 218b26b5213cd1bc8af041064b4d5e82644e7334 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Jul 2016 10:58:55 -0700 Subject: [PATCH 18/78] removed snapshot share dialog, ctrl s shouldn't leave you moving --- .../qml/hifi/dialogs/SnapshotShareDialog.qml | 117 ------------------ interface/src/Application.cpp | 12 +- interface/src/ui/Snapshot.cpp | 115 ----------------- interface/src/ui/Snapshot.h | 8 -- interface/src/ui/SnapshotShareDialog.cpp | 47 ------- .../src/input-plugins/KeyboardMouseDevice.cpp | 8 +- 6 files changed, 6 insertions(+), 301 deletions(-) delete mode 100644 interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml delete mode 100644 interface/src/ui/SnapshotShareDialog.cpp diff --git a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml deleted file mode 100644 index f99b770a78..0000000000 --- a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml +++ /dev/null @@ -1,117 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtQuick.XmlListModel 2.0 - -import "../../windows" -import "../../js/Utils.js" as Utils -import "../models" - -Window { - id: root - resizable: true - width: 516 - height: 616 - minSize: Qt.vector2d(500, 600); - maxSize: Qt.vector2d(1000, 800); - - property alias source: image.source - - Rectangle { - anchors.fill: parent - color: "white" - - Item { - anchors { fill: parent; margins: 8 } - - Image { - id: image - anchors { top: parent.top; left: parent.left; right: parent.right; bottom: notesLabel.top; bottomMargin: 8 } - fillMode: Image.PreserveAspectFit - } - - Text { - id: notesLabel - anchors { left: parent.left; bottom: notes.top; bottomMargin: 8; } - text: "Notes about this image" - font.pointSize: 14 - font.bold: true - color: "#666" - } - - TextArea { - id: notes - anchors { left: parent.left; bottom: parent.bottom; right: shareButton.left; rightMargin: 8 } - height: 60 - } - - Button { - id: shareButton - anchors { verticalCenter: notes.verticalCenter; right: parent.right; } - width: 120; height: 50 - text: "Share" - - style: ButtonStyle { - background: Rectangle { - implicitWidth: 120 - implicitHeight: 50 - border.width: control.activeFocus ? 2 : 1 - color: "#333" - radius: 9 - } - label: Text { - color: shareButton.enabled ? "white" : "gray" - font.pixelSize: 18 - font.bold: true - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - anchors.fill: parent - text: shareButton.text - } - } - - onClicked: { - enabled = false; - uploadTimer.start(); - } - - Timer { - id: uploadTimer - running: false - interval: 5 - repeat: false - onTriggered: { - var uploaded = SnapshotUploader.uploadSnapshot(root.source.toString()) - console.log("Uploaded result " + uploaded) - if (!uploaded) { - console.log("Upload failed "); - } - } - } - } - } - - Action { - id: shareAction - text: qsTr("OK") - enabled: root.result ? true : false - shortcut: Qt.Key_Return - onTriggered: { - root.destroy(); - } - } - - Action { - id: cancelAction - text: qsTr("Cancel") - shortcut: Qt.Key_Escape - onTriggered: { - root.destroy(); - } - } - } -} - - - - diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1c9ec94dc4..9d177d724d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1517,7 +1517,6 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" - rootContext->setContextProperty("SnapshotUploader", new SnapshotUploader()); rootContext->setContextProperty("ApplicationInterface", this); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); @@ -4989,16 +4988,7 @@ void Application::takeSnapshot() { player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - QString fileName = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); - - auto accountManager = DependencyManager::get(); - if (!accountManager->isLoggedIn()) { - return; - } - - DependencyManager::get()->load("hifi/dialogs/SnapshotShareDialog.qml", [=](QQmlContext*, QObject* dialog) { - dialog->setProperty("source", QUrl::fromLocalFile(fileName)); - }); + Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); } float Application::getRenderResolutionScale() const { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index b8be2bb8c4..a3af742f92 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -133,118 +133,3 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { return imageTempFile; } } - -const QString FORUM_URL = "https://alphas.highfidelity.io"; -const QString FORUM_UPLOADS_URL = FORUM_URL + "/uploads"; -const QString FORUM_POST_URL = FORUM_URL + "/posts"; -const QString FORUM_REPLY_TO_TOPIC = "244"; -const QString FORUM_POST_TEMPLATE = "

%2

"; -const QString SHARE_DEFAULT_ERROR = "The server isn't responding. Please try again in a few minutes."; -const QString SUCCESS_LABEL_TEMPLATE = "Success!!! Go check out your image ...
%1"; - - -QString SnapshotUploader::uploadSnapshot(const QUrl& fileUrl) { - auto accountManager = DependencyManager::get(); - if (accountManager->getAccountInfo().getDiscourseApiKey().isEmpty()) { - OffscreenUi::warning(nullptr, "", "Your Discourse API key is missing, you cannot share snapshots. Please try to relog."); - return QString(); - } - - QHttpPart apiKeyPart; - apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"api_key\"")); - apiKeyPart.setBody(accountManager->getAccountInfo().getDiscourseApiKey().toLatin1()); - - QString filename = fileUrl.toLocalFile(); - qDebug() << filename; - QFile* file = new QFile(filename); - Q_ASSERT(file->exists()); - file->open(QIODevice::ReadOnly); - - QHttpPart imagePart; - imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); - imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"file\"; filename=\"" + file->fileName() + "\"")); - imagePart.setBodyDevice(file); - - QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - file->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart - multiPart->append(apiKeyPart); - multiPart->append(imagePart); - - QUrl url(FORUM_UPLOADS_URL); - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - - QString result; - QEventLoop loop; - - QSharedPointer reply(NetworkAccessManager::getInstance().post(request, multiPart)); - QObject::connect(reply.data(), &QNetworkReply::finished, [&] { - loop.quit(); - - qDebug() << reply->errorString(); - for (const auto& header : reply->rawHeaderList()) { - qDebug() << "Header " << QString(header); - } - auto replyResult = reply->readAll(); - qDebug() << QString(replyResult); - QJsonDocument jsonResponse = QJsonDocument::fromJson(replyResult); - const QJsonObject& responseObject = jsonResponse.object(); - if (!responseObject.contains("url")) { - OffscreenUi::warning(this, "", SHARE_DEFAULT_ERROR); - return; - } - result = responseObject["url"].toString(); - }); - loop.exec(); - return result; -} - -QString SnapshotUploader::sendForumPost(const QString& snapshotPath, const QString& notes) { - // post to Discourse forum - QNetworkRequest request; - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QUrl forumUrl(FORUM_POST_URL); - - QUrlQuery query; - query.addQueryItem("api_key", DependencyManager::get()->getAccountInfo().getDiscourseApiKey()); - query.addQueryItem("topic_id", FORUM_REPLY_TO_TOPIC); - query.addQueryItem("raw", FORUM_POST_TEMPLATE.arg(snapshotPath, notes)); - forumUrl.setQuery(query); - - QByteArray postData = forumUrl.toEncoded(QUrl::RemoveFragment); - request.setUrl(forumUrl); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - QNetworkReply* requestReply = NetworkAccessManager::getInstance().post(request, postData); - - QEventLoop loop; - QString result; - connect(requestReply, &QNetworkReply::finished, [&] { - loop.quit(); - QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); - requestReply->deleteLater(); - const QJsonObject& responseObject = jsonResponse.object(); - - if (!responseObject.contains("id")) { - QString errorMessage(SHARE_DEFAULT_ERROR); - if (responseObject.contains("errors")) { - QJsonArray errorArray = responseObject["errors"].toArray(); - if (!errorArray.first().toString().isEmpty()) { - errorMessage = errorArray.first().toString(); - } - } - OffscreenUi::warning(this, "", errorMessage); - return; - } - - const QString urlTemplate = "%1/t/%2/%3/%4"; - result = urlTemplate.arg(FORUM_URL, - responseObject["topic_slug"].toString(), - QString::number(responseObject["topic_id"].toDouble()), - QString::number(responseObject["post_number"].toDouble())); - }); - loop.exec(); - return result; -} - diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index d87a70255f..5856743141 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -43,12 +43,4 @@ private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; -class SnapshotUploader : public QObject{ - Q_OBJECT -public: - SnapshotUploader(QObject* parent = nullptr) : QObject(parent) {} - Q_INVOKABLE QString uploadSnapshot(const QUrl& fileUrl); - Q_INVOKABLE QString sendForumPost(const QString& snapshotPath, const QString& notes); -}; - #endif // hifi_Snapshot_h diff --git a/interface/src/ui/SnapshotShareDialog.cpp b/interface/src/ui/SnapshotShareDialog.cpp deleted file mode 100644 index 94f89641e2..0000000000 --- a/interface/src/ui/SnapshotShareDialog.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// -// SnapshotShareDialog.cpp -// interface/src/ui -// -// Created by Stojce Slavkovski on 2/16/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#if 0 - - -#include - -const int NARROW_SNAPSHOT_DIALOG_SIZE = 500; -const int WIDE_SNAPSHOT_DIALOG_WIDTH = 650; -const int SUCCESS_LABEL_HEIGHT = 140; - -const QString SHARE_BUTTON_STYLE = "border-width:0;border-radius:9px;border-radius:9px;font-family:Arial;font-size:18px;" - "font-weight:100;color:#FFFFFF;width: 120px;height: 50px;"; -const QString SHARE_BUTTON_ENABLED_STYLE = "background-color: #333;"; -const QString SHARE_BUTTON_DISABLED_STYLE = "background-color: #999;"; - -Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) - -SnapshotShareDialog::SnapshotShareDialog(QString fileName, QWidget* parent) : - QDialog(parent), - _fileName(fileName) -{ - - - _ui.snapshotWidget->setPixmap(snaphsotPixmap); - _ui.snapshotWidget->adjustSize(); -} - -void SnapshotShareDialog::accept() { - // prevent multiple clicks on share button - _ui.shareButton->setEnabled(false); - // gray out share button - _ui.shareButton->setStyleSheet(SHARE_BUTTON_STYLE + SHARE_BUTTON_DISABLED_STYLE); - uploadSnapshot(); -} - - -#endif diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 915ec1db87..ebe80f12cf 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -49,9 +49,11 @@ void KeyboardMouseDevice::InputDevice::focusOutEvent() { void KeyboardMouseDevice::keyPressEvent(QKeyEvent* event) { auto input = _inputDevice->makeInput((Qt::Key) event->key()); - auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); - if (!result.second) { - // key pressed again ? without catching the release event ? + if (!(event->modifiers() & Qt::KeyboardModifier::ControlModifier)) { + auto result = _inputDevice->_buttonPressedMap.insert(input.getChannel()); + if (result.second) { + // key pressed again ? without catching the release event ? + } } } From 4762091aaf60c9c2b478952e60bd28b825de3642 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 11:12:20 -0700 Subject: [PATCH 19/78] add easy mode --- scripts/system/controllers/teleport.js | 85 ++++++++++++++++++++------ 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 42ffd2dac9..f26261d0ff 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -19,10 +19,10 @@ //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) - - var inTeleportMode = false; +var easyMode = true; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -53,7 +53,6 @@ function Trigger(hand) { }; } - function Teleporter() { var _this = this; this.targetProps = null; @@ -107,7 +106,13 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - Script.update.connect(this.update); + if (easyMode !== true) { + Script.update.connect(this.update); + + } else { + Script.update.connect(this.updateEasy); + + } }; this.exitTeleportMode = function(value) { @@ -117,7 +122,11 @@ function Teleporter() { this.leftOverlayOff(); Entities.deleteEntity(_this.targetEntity); this.enableGrab(); - Script.update.disconnect(this.update); + if (easyMode !== true) { + Script.update.disconnect(this.update); + } else { + Script.update.disconnect(this.updateEasy); + } this.updateConnected = false; inTeleportMode = false; @@ -126,7 +135,6 @@ function Teleporter() { this.update = function() { //print('in teleporter update') - if (teleporter.teleportHand === 'left') { teleporter.leftRay(); if (leftPad.buttonValue === 0) { @@ -150,6 +158,25 @@ function Teleporter() { }; + this.updateEasy = function() { + + if (teleporter.teleportHand === 'left') { + teleporter.leftRay(); + if (leftPad.buttonValue === 0) { + _this.teleport(); + return; + } + + } else { + teleporter.rightRay(); + if (rightPad.buttonValue === 0) { + _this.teleport(); + return; + } + } + + }; + this.rightRay = function() { var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); @@ -357,6 +384,7 @@ function getJointData() { } + var leftPad = new ThumbPad('left'); var rightPad = new ThumbPad('right'); var leftTrigger = new Trigger('left'); @@ -364,20 +392,41 @@ var rightTrigger = new Trigger('right'); //create a controller mapping and make sure to disable it when the script is stopped -var mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); -var teleportMapping = Controller.newMapping(mappingName); -teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); -teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); -teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); -teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); +var mappingName, teleportMapping; -teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { - teleporter.enterTeleportMode('left') -}); -teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { - teleporter.enterTeleportMode('right') -}); +function registerMappings() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); + teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { + teleporter.enterTeleportMode('right') + }); + +} + +function registerMappingsEasy() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + + teleportMapping.from(leftPad.down).to(function() { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightPad.down).to(function() { + teleporter.enterTeleportMode('right') + }); +} + +registerMappingsEasy(); var teleporter = new Teleporter(); Controller.enableMapping(mappingName); From ffbfc89e99565277a2a1c05475c458df576471db Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 15:19:37 -0700 Subject: [PATCH 20/78] teleportr --- scripts/system/controllers/teleport.js | 129 ++++++++++--------------- 1 file changed, 51 insertions(+), 78 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index f26261d0ff..1486f8390f 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -13,7 +13,7 @@ // alternate notes for philip: -// try just thumb to teleport +// try just thumb to teleport xxx // cancel if destination is within ~1m of current location @@ -23,6 +23,13 @@ var inTeleportMode = false; var easyMode = true; +var TARGET_MODEL_URL = 'http://hifi-production.s3.amazonaws.com/DomainContent/Toybox/potted_plant/potted_plant.fbx'; +var TARGET_MODEL_DIMENSIONS = { + x: 1.1005, + y: 2.1773, + z: 1.0739 +}; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -62,24 +69,22 @@ function Teleporter() { print('jbp initialize') this.createMappings(); this.disableGrab(); - _this.targetEntity = Entities.addEntity({ - name: 'Hifi-Teleporter-Target-Entity', - position: MyAvatar.position, - type: 'Sphere', - dimensions: { - x: 0.2, - y: 0.2, - z: 0.2 - }, - color: { - red: 255, - green: 255, - blue: 255 - }, - collisionless: true, - collidesWith: '', - visible: true + + var cameraEuler = Quat.safeEulerAngles(Camera.orientation); + var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { + x: 0, + y: 1, + z: 0 }); + + var targetOverlayProps = { + url: TARGET_MODEL_URL, + position: MyAvatar.position, + rotation: towardsMe, + dimensions: TARGET_MODEL_DIMENSIONS + }; + + _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; @@ -106,59 +111,28 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - if (easyMode !== true) { - Script.update.connect(this.update); + Script.update.connect(this.update); - } else { - Script.update.connect(this.updateEasy); - - } }; this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); + Script.update.disconnect(this.update); this.disableMappings(); this.rightOverlayOff(); this.leftOverlayOff(); - Entities.deleteEntity(_this.targetEntity); + // Entities.deleteEntity(_this.targetEntity); + Overlays.deleteOverlay(_this.targetOverlay); this.enableGrab(); - if (easyMode !== true) { - Script.update.disconnect(this.update); - } else { - Script.update.disconnect(this.updateEasy); - } + this.updateConnected = false; - inTeleportMode = false; - + Script.setTimeout(function() { + inTeleportMode = false; + }, 100); }; + this.update = function() { - //print('in teleporter update') - - if (teleporter.teleportHand === 'left') { - teleporter.leftRay(); - if (leftPad.buttonValue === 0) { - _this.exitTeleportMode(); - return; - } - if (leftTrigger.buttonValue === 0) { - _this.teleport(); - } - } else { - teleporter.rightRay(); - if (rightPad.buttonValue === 0) { - _this.exitTeleportMode(); - return; - } - if (rightTrigger.buttonValue === 0) { - _this.teleport(); - } - } - - - }; - - this.updateEasy = function() { if (teleporter.teleportHand === 'left') { teleporter.leftRay(); @@ -199,7 +173,8 @@ function Teleporter() { var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.updateTargetEntity(rightIntersection); + this.updateTargetOverlay(rightIntersection); + }; }; @@ -221,10 +196,10 @@ function Teleporter() { green: 255, blue: 0 }); - var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); if (leftIntersection.intersects) { - this.updateTargetEntity(leftIntersection); + this.updateTargetOverlay(leftIntersection); }; }; @@ -298,18 +273,17 @@ function Teleporter() { } }; - this.updateTargetEntity = function(intersection) { - var targetProps = Entities.getEntityProperties(this.targetEntity); + this.updateTargetOverlay = function(intersection) { + this.intersection=intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y + targetProps.dimensions.y / 2, + y: intersection.intersection.y+TARGET_MODEL_DIMENSIONS.y, z: intersection.intersection.z } - Entities.editEntity(this.targetEntity, { + Overlays.editOverlay(this.targetOverlay, { position: position }); - - + }; this.disableGrab = function() { @@ -321,17 +295,16 @@ function Teleporter() { }; this.teleport = function(value) { - //todo - //get the y position of the teleport landing spot - print('value on teleport: ' + value) - var properties = Entities.getEntityProperties(teleporter.targetEntity); + + print('TELEPORT CALLED'); + var offset = getAvatarFootOffset(); - properties.position.y += offset; - - print('OFFSET IS::: ' + JSON.stringify(offset)) - print('TELEPORT POSITION IS:: ' + JSON.stringify(properties.position)); - MyAvatar.position = properties.position; - + + // _this.intersectionPosition.y+=offset; + // print('OFFSET IS::: ' + JSON.stringify(offset)) + // print('TELEPORT POSITION IS:: ' + JSON.stringify(_this.intersectionPosition)); + _this.intersection.intersection.y+=offset; + MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); }; } @@ -438,7 +411,7 @@ function cleanup() { teleporter.disableMappings(); teleporter.rightOverlayOff(); teleporter.leftOverlayOff(); - Entities.deleteEntity(teleporter.targetEntity); + Overlays.deleteOverlay(teleporter.targetOverlay); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 9afc4d0c26fd3982c677678e03cadcc979ea6c95 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 15:19:53 -0700 Subject: [PATCH 21/78] cleanup --- scripts/system/controllers/teleport.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 1486f8390f..65c3274684 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -124,7 +124,7 @@ function Teleporter() { // Entities.deleteEntity(_this.targetEntity); Overlays.deleteOverlay(_this.targetOverlay); this.enableGrab(); - + this.updateConnected = false; Script.setTimeout(function() { inTeleportMode = false; @@ -274,16 +274,16 @@ function Teleporter() { }; this.updateTargetOverlay = function(intersection) { - this.intersection=intersection; + this.intersection = intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y+TARGET_MODEL_DIMENSIONS.y, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { position: position }); - + }; this.disableGrab = function() { @@ -299,11 +299,8 @@ function Teleporter() { print('TELEPORT CALLED'); var offset = getAvatarFootOffset(); - - // _this.intersectionPosition.y+=offset; - // print('OFFSET IS::: ' + JSON.stringify(offset)) - // print('TELEPORT POSITION IS:: ' + JSON.stringify(_this.intersectionPosition)); - _this.intersection.intersection.y+=offset; + + _this.intersection.intersection.y += offset; MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); }; From 00ce75ef485b70fba81c58f3c12d441382a5cc92 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 15:30:21 -0700 Subject: [PATCH 22/78] more notes --- scripts/system/controllers/teleport.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 65c3274684..4b8c31c918 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -7,18 +7,19 @@ //release trigger to teleport then exit teleport mode xxx //if thumb is release, exit teleport mode xxx -//v2: show room boundaries when choosing a place to teleport -//v2: smooth fade screen in/out? -//v2: haptic feedback - -// alternate notes for philip: // try just thumb to teleport xxx -// cancel if destination is within ~1m of current location - //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) + +//terminate the line when there is an intersection +//when there's not an intersection, set a fixed distance? + + +//v2: show room boundaries when choosing a place to teleport +//v2: smooth fade screen in/out? +//v2: haptic feedback var inTeleportMode = false; var easyMode = true; From 9b822f97c03cd62b741a7743531f14ac0603427c Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 1 Jul 2016 17:26:52 -0700 Subject: [PATCH 23/78] end of day --- scripts/system/controllers/teleport.js | 56 ++++++++++++++++---------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4b8c31c918..7a826e2ca7 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,3 +1,5 @@ +// by james b. pollack @imgntn on 7/2/2016 + //v1 //check if trigger is down xxx //if trigger is down, check if thumb is down xxx @@ -16,19 +18,18 @@ //terminate the line when there is an intersection //when there's not an intersection, set a fixed distance? - //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? //v2: haptic feedback + var inTeleportMode = false; -var easyMode = true; +var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; -var TARGET_MODEL_URL = 'http://hifi-production.s3.amazonaws.com/DomainContent/Toybox/potted_plant/potted_plant.fbx'; var TARGET_MODEL_DIMENSIONS = { - x: 1.1005, - y: 2.1773, - z: 1.0739 + x: 1.15, + y: 0.5 + z: 1.15 }; function ThumbPad(hand) { @@ -167,16 +168,17 @@ function Teleporter() { var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); this.rightLineOn(rightPickRay.origin, location, { - red: 255, - green: 0, - blue: 0 + red: 7, + green: 36, + blue: 44 }); var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { this.updateTargetOverlay(rightIntersection); - - }; + } else { + this.noIntersection() + } }; this.leftRay = function() { @@ -193,22 +195,24 @@ function Teleporter() { var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); this.leftLineOn(leftPickRay.origin, location, { - red: 0, - green: 255, - blue: 0 + red: 7, + green: 36, + blue: 44 }); var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); if (leftIntersection.intersects) { this.updateTargetOverlay(leftIntersection); - }; + } else { + this.noIntersection() + } }; this.rightLineOn = function(closePoint, farPoint, color) { // draw a line if (this.rightOverlayLine === null) { var lineProperties = { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -221,7 +225,7 @@ function Teleporter() { } else { var success = Overlays.editOverlay(this.rightOverlayLine, { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -236,7 +240,7 @@ function Teleporter() { // draw a line if (this.leftOverlayLine === null) { var lineProperties = { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -249,7 +253,7 @@ function Teleporter() { } else { var success = Overlays.editOverlay(this.leftOverlayLine, { - lineWidth: 5, + lineWidth: 50, start: closePoint, end: farPoint, color: color, @@ -274,6 +278,13 @@ function Teleporter() { } }; + this.noIntersection = function() { + print('no intersection' + teleporter.targetOverlay); + Overlays.editOverlay(teleporter.targetOverlay, { + visible: false, + }); + }; + this.updateTargetOverlay = function(intersection) { this.intersection = intersection; var position = { @@ -282,7 +293,8 @@ function Teleporter() { z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { - position: position + position: position, + visible: true }); }; @@ -382,7 +394,7 @@ function registerMappings() { } -function registerMappingsEasy() { +function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); @@ -397,7 +409,7 @@ function registerMappingsEasy() { }); } -registerMappingsEasy(); +registerMappings(); var teleporter = new Teleporter(); Controller.enableMapping(mappingName); From f4d409f1e16b56bc08558edca6fa1928647b11c1 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 09:22:19 -0700 Subject: [PATCH 24/78] fix typo --- scripts/system/controllers/teleport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 7a826e2ca7..2988136603 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -28,7 +28,7 @@ var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Te var TARGET_MODEL_DIMENSIONS = { x: 1.15, - y: 0.5 + y: 0.5, z: 1.15 }; From ff745a345e17e3ba44dc5e7925338f252d9538fe Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 09:44:51 -0700 Subject: [PATCH 25/78] remove dupe --- scripts/system/controllers/teleport.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 2988136603..50a673272b 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -377,23 +377,6 @@ var rightTrigger = new Trigger('right'); var mappingName, teleportMapping; -function registerMappings() { - mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); - teleportMapping = Controller.newMapping(mappingName); - teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); - teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); - teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); - teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function() { - teleporter.enterTeleportMode('left') - }); - teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function() { - teleporter.enterTeleportMode('right') - }); - -} - function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); From 056e9e3a720851dd0635e3315df2be861481b3fb Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 10:31:47 -0700 Subject: [PATCH 26/78] add teleport models etc --- scripts/system/assets/models/teleportBeam.fbx | Bin 0 -> 40172 bytes .../assets/models/teleportDestination.fbx | Bin 0 -> 188700 bytes scripts/system/controllers/teleport.js | 92 +++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 scripts/system/assets/models/teleportBeam.fbx create mode 100644 scripts/system/assets/models/teleportDestination.fbx diff --git a/scripts/system/assets/models/teleportBeam.fbx b/scripts/system/assets/models/teleportBeam.fbx new file mode 100644 index 0000000000000000000000000000000000000000..21b63d5843ac14a12ea7601d1bf8475c696cac93 GIT binary patch literal 40172 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 bNQV9Ysr(37w%}RY!{p|7QjO6owI%;QsNJ!J literal 0 HcmV?d00001 diff --git a/scripts/system/assets/models/teleportDestination.fbx b/scripts/system/assets/models/teleportDestination.fbx new file mode 100644 index 0000000000000000000000000000000000000000..c850982f563417e960d9079dbc0da29408ed01c9 GIT binary patch literal 188700 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℞lN$ndk^!g{~r9Z~3GWN2eQLZJ8~+EUl9 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 z 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 RE9bu=_IC)^{WEw8{ue`kmstP+ literal 0 HcmV?d00001 diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 50a673272b..32c88174c9 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -15,8 +15,8 @@ //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) -//terminate the line when there is an intersection -//when there's not an intersection, set a fixed distance? +//terminate the line when there is an intersection (moving away from lines so...) +//when there's not an intersection, set a fixed distance? (no) //v2: show room boundaries when choosing a place to teleport //v2: smooth fade screen in/out? @@ -24,6 +24,13 @@ var inTeleportMode = false; +var currentFadeSphereOpacity = 1; +var fadeSphereInterval = null; +//milliseconds between fading one-tenth -- so this is a one second fade total +var FADE_IN_INTERVAL = 100; +var FADE_OUT_INTERVAL = 100; +var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; + var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -117,6 +124,87 @@ function Teleporter() { }; + this.createStretchyBeam = function() { + + var beamProps = { + url: BEAM_MODEL_URL, + position: MyAvatar.position, + rotation: towardsMe, + dimensions: TARGET_MODEL_DIMENSIONS + }; + + _this.stretchyBeam = Overlays.addOverlay("model", beamProps); + }; + + this.createFadeSphere = function(avatarHead) { + var sphereProps = { + // rotation: props.rotation, + position: avatarHead, + size: 0.25, + color: { + red: 0, + green: 0, + blue: 0, + }, + alpha: 1, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: true + }; + + currentFadeSphereOpacity = 1; + + _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); + }; + + this.fadeSphereOut = function() { + + fadeSphereInterval = Script.setInterval(function() { + if (currentFadeSphereOpacity === 0) { + Script.clearInterval(fadeSphereInterval); + fadeSphereInterval = null; + return; + } + if (currentFadeSphereOpacity > 0) { + currentFadeSphereOpacity -= 0.1; + } + Overlays.editOverlay(_this.fadeSphere, { + opacity: currentFadeSphereOpacity + }) + + }, FADE_OUT_INTERVAL) + }; + + this.fadeSphereIn = function() { + fadeSphereInterval = Script.setInterval(function() { + if (currentFadeSphereOpacity === 1) { + Script.clearInterval(fadeSphereInterval); + fadeSphereInterval = null; + return; + } + if (currentFadeSphereOpacity < 1) { + currentFadeSphereOpacity += 0.1; + } + Overlays.editOverlay(_this.fadeSphere, { + opacity: currentFadeSphereOpacity + }) + + }, FADE_IN_INTERVAL); + }; + + this.deleteFadeSphere = function() { + Overlays.deleteOverlay(_this.fadeSphere); + }; + + this.updateStretchyBeam = function() { + + }; + + this.deleteStretchyBeam = function() { + Overlays.deleteOverlay(_this.stretchyBeam); + }; + this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); From cbd1f8df88d83d8ea3c703b9d50b214b2967d751 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 13:11:14 -0700 Subject: [PATCH 27/78] add stuff for fading in/out and also for a nice stretchy beam --- scripts/system/controllers/teleport.js | 127 ++++++++++++++++++++----- 1 file changed, 102 insertions(+), 25 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 32c88174c9..92e53ed9f8 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -10,8 +10,16 @@ //if thumb is release, exit teleport mode xxx +//v2 // try just thumb to teleport xxx +//v2 +//fade in/out +//stretchy beam instead of GL line + +//v3 +// + //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) @@ -26,10 +34,13 @@ var inTeleportMode = false; var currentFadeSphereOpacity = 1; var fadeSphereInterval = null; -//milliseconds between fading one-tenth -- so this is a one second fade total -var FADE_IN_INTERVAL = 100; -var FADE_OUT_INTERVAL = 100; +//milliseconds between fading one-tenth -- so this is a half second fade total +var FADE_IN_INTERVAL = 50; +var FADE_OUT_INTERVAL = 50; + var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; +var STRETCHY_BEAM_DIMENSIONS_X = 0.07; +var STRETCHY_BEAM_DIMENSIONS_Y = 0.07; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; @@ -59,7 +70,6 @@ function Trigger(hand) { var _this = this; this.buttonPress = function(value) { - print('jbp trigger press: ' + value + " on: " + _this.hand) _this.buttonValue = value; }; @@ -74,6 +84,7 @@ function Teleporter() { this.targetProps = null; this.rightOverlayLine = null; this.leftOverlayLine = null; + this.stretchyBeam = null; this.initialize = function() { print('jbp initialize') this.createMappings(); @@ -113,8 +124,9 @@ function Teleporter() { this.enterTeleportMode = function(hand) { if (inTeleportMode === true) { - return - } + return; + }; + print('jbp hand on entering teleport mode: ' + hand); inTeleportMode = true; this.teleportHand = hand; @@ -124,21 +136,61 @@ function Teleporter() { }; - this.createStretchyBeam = function() { + this.drawStretchyBeamWithoutIntersection = function() { + + }; + + this.findMidpoint = function(handPosition, intersection) { + var xy = Vec3.sum(handPosition, intersection.intersection); + var midpoint = Vec3.multiply(0.5, xy); + print('midpoint point is ' + JSON.stringify(midpoint)); + return midpoint + }; + + + this.createStretchyBeam = function(handPosition, intersection, rotation) { var beamProps = { url: BEAM_MODEL_URL, - position: MyAvatar.position, - rotation: towardsMe, - dimensions: TARGET_MODEL_DIMENSIONS + position: _this.findMidpoint(handPosition, intersection), + dimensions: { + x: STRETCHY_BEAM_DIMENSIONS_X, + y: STRETCHY_BEAM_DIMENSIONS_Y, + z: 0.1 + }, + ignoreRayIntersection: true, }; _this.stretchyBeam = Overlays.addOverlay("model", beamProps); + Script.update.connect(_this.updateStretchyBeam); }; + + this.updateStretchyBeam = function(handPosition, intersection, rotation) { + var dimensions = { + x: STRETCHY_BEAM_DIMENSIONS_X, + y: STRETCHY_BEAM_DIMENSIONS_Y, + z: Vec3.distance(handPosition, intersection.intersection) / 2 + }; + + var position = _this.findMidpoint(handPosition, intersection); + + Overlays.editOverlay(_this.stretchyBeam, { + dimensions: dimensions, + position: position, + + }) + + }; + + this.deleteStretchyBeam = function() { + Overlays.deleteOverlay(_this.stretchyBeam); + Script.update.disconnect(_this.updateStretchyBeam); + }; + + this.createFadeSphere = function(avatarHead) { var sphereProps = { - // rotation: props.rotation, position: avatarHead, size: 0.25, color: { @@ -156,6 +208,8 @@ function Teleporter() { currentFadeSphereOpacity = 1; _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); + + Script.update.connect(_this.updateFadeSphere); }; this.fadeSphereOut = function() { @@ -163,6 +217,7 @@ function Teleporter() { fadeSphereInterval = Script.setInterval(function() { if (currentFadeSphereOpacity === 0) { Script.clearInterval(fadeSphereInterval); + _this.deleteFadeSphere(); fadeSphereInterval = null; return; } @@ -170,16 +225,17 @@ function Teleporter() { currentFadeSphereOpacity -= 0.1; } Overlays.editOverlay(_this.fadeSphere, { - opacity: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity }) - }, FADE_OUT_INTERVAL) + }, FADE_OUT_INTERVAL); }; this.fadeSphereIn = function() { fadeSphereInterval = Script.setInterval(function() { if (currentFadeSphereOpacity === 1) { Script.clearInterval(fadeSphereInterval); + _this.deleteFadeSphere(); fadeSphereInterval = null; return; } @@ -187,34 +243,32 @@ function Teleporter() { currentFadeSphereOpacity += 0.1; } Overlays.editOverlay(_this.fadeSphere, { - opacity: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity }) }, FADE_IN_INTERVAL); }; + this.updateFadeSphere = function() { + var headPosition = MyAvatar.getHeadPosition(); + Overlays.editOverlay(_this.fadeSphere, { + position: headPosition + }) + }; + this.deleteFadeSphere = function() { + Script.update.disconnect(_this.updateFadeSphere); Overlays.deleteOverlay(_this.fadeSphere); }; - this.updateStretchyBeam = function() { - - }; - - this.deleteStretchyBeam = function() { - Overlays.deleteOverlay(_this.stretchyBeam); - }; - this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); this.disableMappings(); this.rightOverlayOff(); this.leftOverlayOff(); - // Entities.deleteEntity(_this.targetEntity); Overlays.deleteOverlay(_this.targetOverlay); this.enableGrab(); - this.updateConnected = false; Script.setTimeout(function() { inTeleportMode = false; @@ -264,8 +318,14 @@ function Teleporter() { if (rightIntersection.intersects) { this.updateTargetOverlay(rightIntersection); + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + } else { + this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + } } else { this.noIntersection() + this.noIntersectionStretchyBeam(); } }; @@ -282,17 +342,27 @@ function Teleporter() { this.leftPickRay = leftPickRay; var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); + this.leftLineOn(leftPickRay.origin, location, { red: 7, green: 36, blue: 44 }); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); if (leftIntersection.intersects) { this.updateTargetOverlay(leftIntersection); + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + } else { + this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + + } + } else { - this.noIntersection() + this.noIntersection(); + this.noIntersectionStretchyBeam(); } }; @@ -373,6 +443,13 @@ function Teleporter() { }); }; + + this.noIntersectionStretchyBeam = function() { + print('no intersection'); + + }; + + this.updateTargetOverlay = function(intersection) { this.intersection = intersection; var position = { From 04184ee1fba636a42e6844334d6b7e11980c5cc4 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 17:21:57 -0700 Subject: [PATCH 28/78] added snapshot notification in desktop mode --- interface/src/Application.cpp | 4 +- .../src/scripting/WindowScriptingInterface.h | 1 + scripts/system/notifications.js | 83 +++++++++++-------- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9d177d724d..d00c966da1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4988,7 +4988,9 @@ void Application::takeSnapshot() { player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot()); + + emit DependencyManager::get()->snapshotTaken(path); } float Application::getRenderResolutionScale() const { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index b92114c1bf..4f26ccd057 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -46,6 +46,7 @@ signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode); + void snapshotTaken(const QString& path); private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 7d97470b8a..667d87271d 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -49,10 +49,10 @@ // 2. Declare a text string. // 3. Call createNotifications(text, NotificationType) parsing the text. // example: -// if (key.text === "s") { +// if (key.text === "o") { // if (ctrlIsPressed === true) { -// noteString = "Snapshot taken."; -// createNotification(noteString, NotificationType.SNAPSHOT); +// noteString = "Open script"; +// createNotification(noteString, NotificationType.OPEN_SCRIPT); // } // } @@ -233,8 +233,9 @@ function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { // Pushes data to each array and sets up data for 2nd dimension array // to handle auxiliary data not carried by the overlay class // specifically notification "heights", "times" of creation, and . -function notify(notice, button, height) { - var noticeWidth, +function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, noticeHeight, positions, last; @@ -269,9 +270,13 @@ function notify(notice, button, height) { height: noticeHeight }); } else { - var notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - buttons.push((Overlays.addOverlay("image", button))); + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push((notificationText)); + } else { + notifications.push(Overlays.addOverlay("image", notice)); + } + buttons.push(Overlays.addOverlay("image", button)); } height = height + 1.0; @@ -281,11 +286,24 @@ function notify(notice, button, height) { last = notifications.length - 1; createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); fadeIn(notifications[last], buttons[last]); + + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + imageURL: imageProperties.path + }; + notify(notice, button, imageHeight, imageProperties, true); + } + return notificationText; } // This function creates and sizes the overlays -function createNotification(text, notificationType) { +function createNotification(text, notificationType, imageProperties) { var count = (text.match(/\n/g) || []).length, breakPoint = 43.0, // length when new line is added extraLine = 0, @@ -308,6 +326,11 @@ function createNotification(text, notificationType) { level = (stack + 20.0); height = height + extraLine; + + if (imageProperties && imageProperties.path) { + imageProperties.path = Script.resolvePath(imageProperties.path); + } + noticeProperties = { x: overlayLocationX, y: level, @@ -336,12 +359,11 @@ function createNotification(text, notificationType) { }; if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) - { + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { randomSounds.playRandom(); } - return notify(noticeProperties, buttonProperties, height); + return notify(noticeProperties, buttonProperties, height, imageProperties); } function deleteNotification(index) { @@ -362,21 +384,12 @@ function deleteNotification(index) { // wraps whole word to newline function stringDivider(str, slotWidth, spaceReplacer) { - var p, - left, - right; + var left, right; - if (str.length > slotWidth) { - p = slotWidth; - while (p > 0 && str[p] !== ' ') { - p -= 1; - } - - if (p > 0) { - left = str.substring(0, p); - right = str.substring(p + 1); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } + if (str.length > slotWidth && slotWidth > 0) { + left = str.substring(0, slotWidth); + right = str.substring(slotWidth + 1); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); } return str; } @@ -504,7 +517,15 @@ function onMuteStateChanged() { } function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED ); + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); +} + +function onSnapshotTaken(path) { + var imageProperties = { + path: path, + aspectRatio: Window.innerWidth / Window.innerHeight + } + createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); } // handles mouse clicks on buttons @@ -541,13 +562,6 @@ function keyPressEvent(key) { if (key.key === 16777249) { ctrlIsPressed = true; } - - if (key.text === "s") { - if (ctrlIsPressed === true) { - noteString = "Snapshot taken."; - createNotification(noteString, NotificationType.SNAPSHOT); - } - } } function setup() { @@ -615,5 +629,6 @@ Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); Menu.menuItemEvent.connect(menuItemEvent); Window.domainConnectionRefused.connect(onDomainConnectionRefused); +Window.snapshotTaken.connect(onSnapshotTaken); setup(); From c78dbe26b65fe19c96644e00221a09c8889dccb2 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 17:25:21 -0700 Subject: [PATCH 29/78] working initial beam, still trying to clear up no intersection beam issues --- scripts/system/controllers/teleport.js | 329 ++++++++++++++----------- 1 file changed, 187 insertions(+), 142 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 92e53ed9f8..8c67811529 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -39,17 +39,30 @@ var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; -var STRETCHY_BEAM_DIMENSIONS_X = 0.07; -var STRETCHY_BEAM_DIMENSIONS_Y = 0.07; +var BEAM_MODEL_URL_NO_INTERSECTION = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam2.fbx"; + + +var STRETCHY_BEAM_DIMENSIONS_X = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 10; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; - var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, z: 1.15 }; + +//swirly thing, not sure we'll get to use it +// var TARGET_MODEL_URL='http://hifi-content.s3.amazonaws.com/alan/dev/Cyclone-4.fbx'; + +// var TARGET_MODEL_DIMENSIONS = { +// x: 0.93, +// y: 0.93, +// z: 1.22 +// }; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -62,7 +75,6 @@ function ThumbPad(hand) { this.down = function() { return _this.buttonValue === 1 ? 1.0 : 0.0; }; - } function Trigger(hand) { @@ -81,15 +93,21 @@ function Trigger(hand) { function Teleporter() { var _this = this; + this.intersection = null; this.targetProps = null; - this.rightOverlayLine = null; - this.leftOverlayLine = null; + this.targetOverlay = null; this.stretchyBeam = null; + this.noIntersectionStretchyBeam = null; + this.updateConnected = null; + this.initialize = function() { print('jbp initialize') this.createMappings(); this.disableGrab(); + }; + this.createTargetOverlay = function() { + print('creating target overlay') var cameraEuler = Quat.safeEulerAngles(Camera.orientation); var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { x: 0, @@ -101,7 +119,8 @@ function Teleporter() { url: TARGET_MODEL_URL, position: MyAvatar.position, rotation: towardsMe, - dimensions: TARGET_MODEL_DIMENSIONS + dimensions: TARGET_MODEL_DIMENSIONS, + visible: true, }; _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); @@ -125,7 +144,7 @@ function Teleporter() { this.enterTeleportMode = function(hand) { if (inTeleportMode === true) { return; - }; + } print('jbp hand on entering teleport mode: ' + hand); inTeleportMode = true; @@ -136,14 +155,9 @@ function Teleporter() { }; - this.drawStretchyBeamWithoutIntersection = function() { - - }; - this.findMidpoint = function(handPosition, intersection) { var xy = Vec3.sum(handPosition, intersection.intersection); var midpoint = Vec3.multiply(0.5, xy); - print('midpoint point is ' + JSON.stringify(midpoint)); return midpoint }; @@ -162,7 +176,6 @@ function Teleporter() { }; _this.stretchyBeam = Overlays.addOverlay("model", beamProps); - Script.update.connect(_this.updateStretchyBeam); }; @@ -170,24 +183,105 @@ function Teleporter() { var dimensions = { x: STRETCHY_BEAM_DIMENSIONS_X, y: STRETCHY_BEAM_DIMENSIONS_Y, - z: Vec3.distance(handPosition, intersection.intersection) / 2 + z: Vec3.distance(handPosition, intersection.intersection) }; + print('dimensions in update:: ' + JSON.stringify(dimensions)); var position = _this.findMidpoint(handPosition, intersection); - Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, - + rotation: Quat.multiply(rotation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })) }) }; this.deleteStretchyBeam = function() { - Overlays.deleteOverlay(_this.stretchyBeam); - Script.update.disconnect(_this.updateStretchyBeam); + if (_this.stretchyBeam !== null) { + Overlays.deleteOverlay(_this.stretchyBeam); + _this.stretchyBeam = null; + } }; + this.createNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { + + var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; + + var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); + + var midpoint = this.findMidpoint(handPosition, { + intersection: ahead + }); + + var dimensions = { + x: 0.04, + y: 0.04, + z: Vec3.distance(handPosition, ahead) + }; + + + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })); + + var beamProps = { + dimensions: dimensions, + url: BEAM_MODEL_URL_NO_INTERSECTION, + position: midpoint, + rotation: finalRotation, + ignoreRayIntersection: true, + }; + + this.noIntersectionStretchyBeam = Overlays.addOverlay("model", beamProps); + + + }; + + this.updateNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { + + var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; + + var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); + + var midpoint = this.findMidpoint(handPosition, { + intersection: ahead + }); + + var dimensions = { + x: STRETCHY_BEAM_DIMENSIONS_X, + y: STRETCHY_BEAM_DIMENSIONS_Y, + z: Vec3.distance(handPosition, ahead) + }; + + print('dimensions in update:: ' + JSON.stringify(dimensions)); + + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })); + + var goodEdit = Overlays.editOverlay(_this.noIntersectionStretchyBeam, { + dimensions: dimensions, + position: midpoint, + rotation: rotation + }) + + }; + + + this.deleteNoIntersectionStretchyBeam = function() { + if (_this.noIntersectionStretchyBeam !== null) { + Overlays.deleteOverlay(_this.noIntersectionStretchyBeam); + _this.noIntersectionStretchyBeam = null; + } + }; this.createFadeSphere = function(avatarHead) { var sphereProps = { @@ -261,15 +355,21 @@ function Teleporter() { Overlays.deleteOverlay(_this.fadeSphere); }; + this.deleteTargetOverlay = function() { + Overlays.deleteOverlay(this.targetOverlay); + this.intersection = null; + this.targetOverlay = null; + } + this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); + this.updateConnected = null; this.disableMappings(); - this.rightOverlayOff(); - this.leftOverlayOff(); - Overlays.deleteOverlay(_this.targetOverlay); + this.deleteStretchyBeam(); + this.deleteNoIntersectionStretchyBeam(); + this.deleteTargetOverlay(); this.enableGrab(); - this.updateConnected = false; Script.setTimeout(function() { inTeleportMode = false; }, 100); @@ -301,160 +401,105 @@ function Teleporter() { var rightRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).rotation) + var rightPickRay = { origin: rightPosition, direction: Quat.getUp(rightRotation), }; + var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(90, { + x: 1, + y: 0, + z: 0 + })); + this.rightPickRay = rightPickRay; var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); - this.rightLineOn(rightPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); + var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.updateTargetOverlay(rightIntersection); - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.deleteNoIntersectionStretchyBeam(); + + if (this.targetOverlay !== null) { + this.updateTargetOverlay(rightIntersection); } else { - this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.createTargetOverlay(); + } + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); + } else { + this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); } } else { - this.noIntersection() - this.noIntersectionStretchyBeam(); + this.deleteTargetOverlay(); + this.deleteStretchyBeam(); + if (this.noIntersectionStretchyBeam !== null) { + this.updateNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); + + } else { + this.createNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); + } } - }; + } + this.leftRay = function() { var leftPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).translation), MyAvatar.position); - var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) + var leftPickRay = { origin: leftPosition, direction: Quat.getUp(leftRotation), }; + + var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { + x: 1, + y: 0, + z: 0 + })); + this.leftPickRay = leftPickRay; var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); - this.leftLineOn(leftPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); - - var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], []); + var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { - this.updateTargetOverlay(leftIntersection); - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.deleteNoIntersectionStretchyBeam(); + if (this.targetOverlay !== null) { + this.updateTargetOverlay(leftIntersection); } else { - this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightPickRay.direction); + this.createTargetOverlay(); + } + + if (this.stretchyBeam !== null) { + this.updateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); + } else { + this.createStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); } } else { - this.noIntersection(); - this.noIntersectionStretchyBeam(); + this.deleteTargetOverlay(); + this.deleteStretchyBeam(); + if (this.noIntersectionStretchyBeam !== null) { + this.updateNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); + } else { + this.createNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); + } } }; - this.rightLineOn = function(closePoint, farPoint, color) { - // draw a line - if (this.rightOverlayLine === null) { - var lineProperties = { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - ignoreRayIntersection: true, // always ignore this - visible: true, - alpha: 1 - }; - - this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - var success = Overlays.editOverlay(this.rightOverlayLine, { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - visible: true, - ignoreRayIntersection: true, // always ignore this - alpha: 1 - }); - } - }; - - this.leftLineOn = function(closePoint, farPoint, color) { - // draw a line - if (this.leftOverlayLine === null) { - var lineProperties = { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - ignoreRayIntersection: true, // always ignore this - visible: true, - alpha: 1 - }; - - this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - var success = Overlays.editOverlay(this.leftOverlayLine, { - lineWidth: 50, - start: closePoint, - end: farPoint, - color: color, - visible: true, - ignoreRayIntersection: true, // always ignore this - alpha: 1 - }); - } - }; - - this.rightOverlayOff = function() { - if (this.rightOverlayLine !== null) { - Overlays.deleteOverlay(this.rightOverlayLine); - this.rightOverlayLine = null; - } - }; - - this.leftOverlayOff = function() { - if (this.leftOverlayLine !== null) { - Overlays.deleteOverlay(this.leftOverlayLine); - this.leftOverlayLine = null; - } - }; - - this.noIntersection = function() { - print('no intersection' + teleporter.targetOverlay); - Overlays.editOverlay(teleporter.targetOverlay, { - visible: false, - }); - }; - - - this.noIntersectionStretchyBeam = function() { - print('no intersection'); - - }; - this.updateTargetOverlay = function(intersection) { this.intersection = intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y + 0.1, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { @@ -477,13 +522,15 @@ function Teleporter() { print('TELEPORT CALLED'); var offset = getAvatarFootOffset(); - - _this.intersection.intersection.y += offset; - MyAvatar.position = _this.intersection.intersection; + if (_this.intersection !== null) { + _this.intersection.intersection.y += offset; + MyAvatar.position = _this.intersection.intersection; + } this.exitTeleportMode(); }; } + //related to repositioning the avatar after you teleport function getAvatarFootOffset() { var data = getJointData(); @@ -529,9 +576,7 @@ function getJointData() { }); return allJointData; -} - - +}; var leftPad = new ThumbPad('left'); var rightPad = new ThumbPad('right'); @@ -567,9 +612,9 @@ Script.scriptEnding.connect(cleanup); function cleanup() { teleportMapping.disable(); teleporter.disableMappings(); - teleporter.rightOverlayOff(); - teleporter.leftOverlayOff(); - Overlays.deleteOverlay(teleporter.targetOverlay); + teleporter.deleteStretchyBeam(); + teleporter.deleteTargetOverlay(); + teleporter.deleteNoIntersectionStretchyBeam(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 26950eaaa6efed9de1a7a9ce4495d943108210fa Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 17:30:17 -0700 Subject: [PATCH 30/78] working second model adjustment with hack but wtf --- scripts/system/controllers/teleport.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 8c67811529..b724ac3cfd 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -44,7 +44,7 @@ var BEAM_MODEL_URL_NO_INTERSECTION = "http://hifi-content.s3.amazonaws.com/james var STRETCHY_BEAM_DIMENSIONS_X = 0.04; var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 10; +var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -185,7 +185,7 @@ function Teleporter() { y: STRETCHY_BEAM_DIMENSIONS_Y, z: Vec3.distance(handPosition, intersection.intersection) }; - print('dimensions in update:: ' + JSON.stringify(dimensions)); + var position = _this.findMidpoint(handPosition, intersection); Overlays.editOverlay(_this.stretchyBeam, { @@ -220,7 +220,7 @@ function Teleporter() { var dimensions = { x: 0.04, y: 0.04, - z: Vec3.distance(handPosition, ahead) + z:0.1 }; @@ -258,8 +258,9 @@ function Teleporter() { y: STRETCHY_BEAM_DIMENSIONS_Y, z: Vec3.distance(handPosition, ahead) }; - - print('dimensions in update:: ' + JSON.stringify(dimensions)); + dimensions=Vec3.multiply(10,dimensions) + print('dimensions in update:: ' + JSON.stringify(dimensions)); + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, From 4eb94f8ccbd48d6b86f2ca23945c34fea2122d8e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 17:40:33 -0700 Subject: [PATCH 31/78] added support for snapshot notification in HMD, although 3d image overlays seem to be broken in master --- scripts/system/notifications.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 667d87271d..988d2998d7 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -251,16 +251,23 @@ function notify(notice, button, height, imageProperties, image) { notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; notice.bottomMargin = 0; notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + if (!image) { + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); + } button.url = button.imageURL; button.scale = button.width * NOTIFICATION_3D_SCALE; button.isFacingAvatar = false; - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notifications.push((Overlays.addOverlay("text3d", notice))); buttons.push((Overlays.addOverlay("image3d", button))); overlay3DDetails.push({ notificationOrientation: positions.notificationOrientation, @@ -294,6 +301,8 @@ function notify(notice, button, height, imageProperties, image) { y: notice.y + height, width: notice.width, height: imageHeight, + topMargin: 0, + leftMargin: 0, imageURL: imageProperties.path }; notify(notice, button, imageHeight, imageProperties, true); From b0494ec9d53d194637b78dcdf172c85507196434 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 5 Jul 2016 17:48:30 -0700 Subject: [PATCH 32/78] bug notes --- scripts/system/controllers/teleport.js | 30 +++++++++++--------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b724ac3cfd..4630b234bc 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,5 +1,8 @@ // by james b. pollack @imgntn on 7/2/2016 + +///BUG: there is currently something going on when you try to adjust the dimensions of a second overlay model, it seems to be off by a factor of 10 or already changed or something weird. + //v1 //check if trigger is down xxx //if trigger is down, check if thumb is down xxx @@ -13,22 +16,15 @@ //v2 // try just thumb to teleport xxx -//v2 -//fade in/out -//stretchy beam instead of GL line - //v3 -// +//fade in/out +//stretchy beam instead of GL line xxx + //try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) - -//terminate the line when there is an intersection (moving away from lines so...) -//when there's not an intersection, set a fixed distance? (no) - -//v2: show room boundaries when choosing a place to teleport -//v2: smooth fade screen in/out? -//v2: haptic feedback +//v?: haptic feedback +//v?: show room boundaries when choosing a place to teleport var inTeleportMode = false; @@ -220,7 +216,7 @@ function Teleporter() { var dimensions = { x: 0.04, y: 0.04, - z:0.1 + z: 0.1 }; @@ -231,7 +227,7 @@ function Teleporter() { })); var beamProps = { - dimensions: dimensions, + // dimensions: dimensions, url: BEAM_MODEL_URL_NO_INTERSECTION, position: midpoint, rotation: finalRotation, @@ -258,9 +254,9 @@ function Teleporter() { y: STRETCHY_BEAM_DIMENSIONS_Y, z: Vec3.distance(handPosition, ahead) }; - dimensions=Vec3.multiply(10,dimensions) - print('dimensions in update:: ' + JSON.stringify(dimensions)); - + dimensions = Vec3.multiply(10, dimensions) + print('dimensions in update:: ' + JSON.stringify(dimensions)); + var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, From b3513b543c50899edc63c9fda74bd386e4916e27 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 18:23:59 -0700 Subject: [PATCH 33/78] fixed snapshot path setting --- .../hifi/dialogs/AvatarPreferencesDialog.qml | 2 +- interface/src/ui/PreferencesDialog.cpp | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml index 86f195612c..45414cfaf8 100644 --- a/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AvatarPreferencesDialog.qml @@ -7,7 +7,7 @@ PreferencesDialog { id: root objectName: "AvatarPreferencesDialog" title: "Avatar Settings" - showCategories: [ "Avatar Basics", "Avatar Tuning", "Avatar Camera" ] + showCategories: [ "Avatar Basics", "Snapshots", "Avatar Tuning", "Avatar Camera" ] property var settings: Settings { category: root.objectName property alias x: root.x diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index c1705da206..146df549dd 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -35,7 +35,7 @@ void setupPreferences() { MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); static const QString AVATAR_BASICS { "Avatar Basics" }; { - auto getter = [=]()->QString {return myAvatar->getDisplayName(); }; + auto getter = [=]()->QString { return myAvatar->getDisplayName(); }; auto setter = [=](const QString& value) { myAvatar->setDisplayName(value); }; auto preference = new EditPreference(AVATAR_BASICS, "Avatar display name (optional)", getter, setter); preference->setPlaceholderText("Not showing a name"); @@ -43,7 +43,7 @@ void setupPreferences() { } { - auto getter = [=]()->QString {return myAvatar->getCollisionSoundURL(); }; + auto getter = [=]()->QString { return myAvatar->getCollisionSoundURL(); }; auto setter = [=](const QString& value) { myAvatar->setCollisionSoundURL(value); }; auto preference = new EditPreference(AVATAR_BASICS, "Avatar collision sound URL (optional)", getter, setter); preference->setPlaceholderText("Enter the URL of a sound to play when you bump into something"); @@ -56,20 +56,24 @@ void setupPreferences() { auto preference = new AvatarPreference(AVATAR_BASICS, "Appearance", getter, setter); preferences->addPreference(preference); } + { - auto getter = [=]()->bool {return myAvatar->getSnapTurn(); }; + auto getter = [=]()->bool { return myAvatar->getSnapTurn(); }; auto setter = [=](bool value) { myAvatar->setSnapTurn(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Snap turn when in HMD", getter, setter)); } { - auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenMoving(); }; + auto getter = [=]()->bool { return myAvatar->getClearOverlayWhenMoving(); }; auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } + + // Snapshots + static const QString SNAPSHOTS { "Snapshots" }; { - auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; - auto setter = [](const QString& value) { Snapshot::snapshotsLocation.set(value); }; - auto preference = new BrowsePreference("Snapshots", "Put my snapshots here", getter, setter); + auto getter = [=]()->QString { return Snapshot::snapshotsLocation.get(); }; + auto setter = [=](const QString& value) { Snapshot::snapshotsLocation.set(value); }; + auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } @@ -85,7 +89,7 @@ void setupPreferences() { })); { - auto getter = []()->bool {return !Menu::getInstance()->isOptionChecked(MenuOption::DisableActivityLogger); }; + auto getter = []()->bool { return !Menu::getInstance()->isOptionChecked(MenuOption::DisableActivityLogger); }; auto setter = [](bool value) { Menu::getInstance()->setIsOptionChecked(MenuOption::DisableActivityLogger, !value); }; preferences->addPreference(new CheckPreference("Privacy", "Send data", getter, setter)); } @@ -184,7 +188,7 @@ void setupPreferences() { static const QString AUDIO("Audio"); { - auto getter = []()->bool {return DependencyManager::get()->getReceivedAudioStream().getDynamicJitterBuffers(); }; + auto getter = []()->bool { return DependencyManager::get()->getReceivedAudioStream().getDynamicJitterBuffers(); }; auto setter = [](bool value) { DependencyManager::get()->getReceivedAudioStream().setDynamicJitterBuffers(value); }; preferences->addPreference(new CheckPreference(AUDIO, "Enable dynamic jitter buffers", getter, setter)); } @@ -207,7 +211,7 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = []()->bool {return DependencyManager::get()->getReceivedAudioStream().getUseStDevForJitterCalc(); }; + auto getter = []()->bool { return DependencyManager::get()->getReceivedAudioStream().getUseStDevForJitterCalc(); }; auto setter = [](bool value) { DependencyManager::get()->getReceivedAudioStream().setUseStDevForJitterCalc(value); }; preferences->addPreference(new CheckPreference(AUDIO, "Use standard deviation for dynamic jitter calc", getter, setter)); } @@ -236,7 +240,7 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = []()->bool {return DependencyManager::get()->getReceivedAudioStream().getRepetitionWithFade(); }; + auto getter = []()->bool { return DependencyManager::get()->getReceivedAudioStream().getRepetitionWithFade(); }; auto setter = [](bool value) { DependencyManager::get()->getReceivedAudioStream().setRepetitionWithFade(value); }; preferences->addPreference(new CheckPreference(AUDIO, "Repetition with fade", getter, setter)); } @@ -250,7 +254,7 @@ void setupPreferences() { preferences->addPreference(preference); } { - auto getter = []()->bool {return DependencyManager::get()->getOutputStarveDetectionEnabled(); }; + auto getter = []()->bool { return DependencyManager::get()->getOutputStarveDetectionEnabled(); }; auto setter = [](bool value) { DependencyManager::get()->setOutputStarveDetectionEnabled(value); }; auto preference = new CheckPreference(AUDIO, "Output starve detection (automatic buffer size increase)", getter, setter); preferences->addPreference(preference); From 79864df08ea91abf042bbf20ffc02068d91c9693 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 5 Jul 2016 18:53:39 -0700 Subject: [PATCH 34/78] didn't mean to do that --- interface/src/ui/PreferencesDialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 146df549dd..4007c940c3 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -71,8 +71,8 @@ void setupPreferences() { // Snapshots static const QString SNAPSHOTS { "Snapshots" }; { - auto getter = [=]()->QString { return Snapshot::snapshotsLocation.get(); }; - auto setter = [=](const QString& value) { Snapshot::snapshotsLocation.set(value); }; + auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; + auto setter = [](const QString& value) { Snapshot::snapshotsLocation.set(value); }; auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } From 6a44ea76e541b22bd9bfdaec392f4a5e193bdf95 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Jul 2016 11:57:29 -0700 Subject: [PATCH 35/78] fixed snapshot preview on windows --- scripts/system/notifications.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 988d2998d7..98a31d708c 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -247,14 +247,14 @@ function notify(notice, button, height, imageProperties, image) { noticeHeight = notice.height * NOTIFICATION_3D_SCALE; notice.size = { x: noticeWidth, y: noticeHeight }; - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; notice.isFacingAvatar = false; @@ -289,8 +289,8 @@ function notify(notice, button, height, imageProperties, image) { height = height + 1.0; heights.push(height); times.push(new Date().getTime() / 1000); - myAlpha.push(0); last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); fadeIn(notifications[last], buttons[last]); @@ -301,9 +301,11 @@ function notify(notice, button, height, imageProperties, image) { y: notice.y + height, width: notice.width, height: imageHeight, - topMargin: 0, - leftMargin: 0, - imageURL: imageProperties.path + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255}, + visible: true, + imageURL: imageProperties.path, + alpha: backgroundAlpha }; notify(notice, button, imageHeight, imageProperties, true); } @@ -336,10 +338,6 @@ function createNotification(text, notificationType, imageProperties) { level = (stack + 20.0); height = height + extraLine; - if (imageProperties && imageProperties.path) { - imageProperties.path = Script.resolvePath(imageProperties.path); - } - noticeProperties = { x: overlayLocationX, y: level, @@ -397,7 +395,7 @@ function stringDivider(str, slotWidth, spaceReplacer) { if (str.length > slotWidth && slotWidth > 0) { left = str.substring(0, slotWidth); - right = str.substring(slotWidth + 1); + right = str.substring(slotWidth); return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); } return str; @@ -531,7 +529,7 @@ function onDomainConnectionRefused(reason) { function onSnapshotTaken(path) { var imageProperties = { - path: path, + path: Script.resolvePath("file:///" + path), aspectRatio: Window.innerWidth / Window.innerHeight } createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); From 890b22426c44b5773257c483d17e00e86809aab9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 6 Jul 2016 12:15:34 -0700 Subject: [PATCH 36/78] added folder selection dialog on first snapshot --- interface/src/ui/Snapshot.cpp | 10 +++++++++- interface/src/ui/Snapshot.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index a3af742f92..aaf11d14a4 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -44,6 +44,7 @@ const QString URL = "highfidelity_url"; Setting::Handle Snapshot::snapshotsLocation("snapshotsLocation", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); +Setting::Handle Snapshot::hasSetSnapshotsLocation("hasSetSnapshotsLocation", false); SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { @@ -103,7 +104,14 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) { const int IMAGE_QUALITY = 100; if (!isTemporary) { - QString snapshotFullPath = snapshotsLocation.get(); + QString snapshotFullPath; + if (!hasSetSnapshotsLocation.get()) { + snapshotFullPath = QFileDialog::getExistingDirectory(nullptr, "Choose Snapshots Directory", snapshotsLocation.get()); + hasSetSnapshotsLocation.set(true); + snapshotsLocation.set(snapshotFullPath); + } else { + snapshotFullPath = snapshotsLocation.get(); + } if (!snapshotFullPath.endsWith(QDir::separator())) { snapshotFullPath.append(QDir::separator()); diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 5856743141..2e7986a5c0 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -39,6 +39,7 @@ public: static SnapshotMetaData* parseSnapshotData(QString snapshotPath); static Setting::Handle snapshotsLocation; + static Setting::Handle hasSetSnapshotsLocation; private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; From 2d073cc99c2b50b042c69af331989efd82bbc774 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 12:31:03 -0700 Subject: [PATCH 37/78] working teleporter in huffmans build --- scripts/system/controllers/teleport.js | 179 +++++++------------------ 1 file changed, 45 insertions(+), 134 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4630b234bc..d4ad82721d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -104,17 +104,10 @@ function Teleporter() { this.createTargetOverlay = function() { print('creating target overlay') - var cameraEuler = Quat.safeEulerAngles(Camera.orientation); - var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { - x: 0, - y: 1, - z: 0 - }); + var targetOverlayProps = { url: TARGET_MODEL_URL, - position: MyAvatar.position, - rotation: towardsMe, dimensions: TARGET_MODEL_DIMENSIONS, visible: true, }; @@ -122,6 +115,15 @@ function Teleporter() { _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; + this.rotateTargetTorwardMe = function() { + var cameraEuler = Quat.safeEulerAngles(Camera.orientation); + var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { + x: 0, + y: 1, + z: 0 + }); + } + this.createMappings = function() { print('jbp create mappings internal'); @@ -157,37 +159,45 @@ function Teleporter() { return midpoint }; + this.updateOrCreateStretchyBeam = function(handPosition, intersection, rotation, direction, noIntersection) { - this.createStretchyBeam = function(handPosition, intersection, rotation) { + if (_this.stretchyBeam === null) { + var beamProps = { + url: BEAM_MODEL_URL, + position: MyAvatar.position, - var beamProps = { - url: BEAM_MODEL_URL, - position: _this.findMidpoint(handPosition, intersection), - dimensions: { - x: STRETCHY_BEAM_DIMENSIONS_X, - y: STRETCHY_BEAM_DIMENSIONS_Y, - z: 0.1 - }, - ignoreRayIntersection: true, - }; + }; - _this.stretchyBeam = Overlays.addOverlay("model", beamProps); - }; + _this.stretchyBeam = Overlays.addOverlay("model", beamProps); + } - - this.updateStretchyBeam = function(handPosition, intersection, rotation) { var dimensions = { x: STRETCHY_BEAM_DIMENSIONS_X, y: STRETCHY_BEAM_DIMENSIONS_Y, - z: Vec3.distance(handPosition, intersection.intersection) + z: intersection !== null ? Vec3.distance(handPosition, intersection.intersection) : STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION }; + var position; + if (noIntersection === true) { + this.deleteTargetOverlay(); + print('no intersection') + position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION/2 , direction)); + } else { + print('intersection, find midpoint') + position = _this.findMidpoint(handPosition, intersection); + } + + + print('rotation: ' + JSON.stringify(rotation)); + print('position: ' + JSON.stringify(position)); + print('dimensions: ' + JSON.stringify(dimensions)); + + - var position = _this.findMidpoint(handPosition, intersection); Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, - rotation: Quat.multiply(rotation, Quat.angleAxis(180, { + rotation: Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, y: 1, z: 0 @@ -203,82 +213,6 @@ function Teleporter() { } }; - this.createNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { - - var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; - - var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); - - var midpoint = this.findMidpoint(handPosition, { - intersection: ahead - }); - - var dimensions = { - x: 0.04, - y: 0.04, - z: 0.1 - }; - - - var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { - x: 0, - y: 1, - z: 0 - })); - - var beamProps = { - // dimensions: dimensions, - url: BEAM_MODEL_URL_NO_INTERSECTION, - position: midpoint, - rotation: finalRotation, - ignoreRayIntersection: true, - }; - - this.noIntersectionStretchyBeam = Overlays.addOverlay("model", beamProps); - - - }; - - this.updateNoIntersectionStretchyBeam = function(handPosition, direction, rotation) { - - var howBig = STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION; - - var ahead = Vec3.sum(handPosition, Vec3.multiply(howBig, direction)); - - var midpoint = this.findMidpoint(handPosition, { - intersection: ahead - }); - - var dimensions = { - x: STRETCHY_BEAM_DIMENSIONS_X, - y: STRETCHY_BEAM_DIMENSIONS_Y, - z: Vec3.distance(handPosition, ahead) - }; - dimensions = Vec3.multiply(10, dimensions) - print('dimensions in update:: ' + JSON.stringify(dimensions)); - - - var finalRotation = Quat.multiply(rotation, Quat.angleAxis(180, { - x: 0, - y: 1, - z: 0 - })); - - var goodEdit = Overlays.editOverlay(_this.noIntersectionStretchyBeam, { - dimensions: dimensions, - position: midpoint, - rotation: rotation - }) - - }; - - - this.deleteNoIntersectionStretchyBeam = function() { - if (_this.noIntersectionStretchyBeam !== null) { - Overlays.deleteOverlay(_this.noIntersectionStretchyBeam); - _this.noIntersectionStretchyBeam = null; - } - }; this.createFadeSphere = function(avatarHead) { var sphereProps = { @@ -364,7 +298,6 @@ function Teleporter() { this.updateConnected = null; this.disableMappings(); this.deleteStretchyBeam(); - this.deleteNoIntersectionStretchyBeam(); this.deleteTargetOverlay(); this.enableGrab(); Script.setTimeout(function() { @@ -417,27 +350,17 @@ function Teleporter() { var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.deleteNoIntersectionStretchyBeam(); if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { this.createTargetOverlay(); } - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); - } else { - this.createStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal); - } + this.updateOrCreateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal, rightPickRay.direction, false); } else { - this.deleteTargetOverlay(); - this.deleteStretchyBeam(); - if (this.noIntersectionStretchyBeam !== null) { - this.updateNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); - } else { - this.createNoIntersectionStretchyBeam(rightPickRay.origin, rightPickRay.direction, rightFinal); - } + this.updateOrCreateStretchyBeam(rightPickRay.origin, null, rightFinal, rightPickRay.direction, true); + } } @@ -452,7 +375,6 @@ function Teleporter() { direction: Quat.getUp(leftRotation), }; - var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { x: 1, y: 0, @@ -466,28 +388,18 @@ function Teleporter() { var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { - this.deleteNoIntersectionStretchyBeam(); if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { this.createTargetOverlay(); } - - if (this.stretchyBeam !== null) { - this.updateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); - } else { - this.createStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal); - - } + this.updateOrCreateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal, leftPickRay.direction, false); } else { - this.deleteTargetOverlay(); - this.deleteStretchyBeam(); - if (this.noIntersectionStretchyBeam !== null) { - this.updateNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); - } else { - this.createNoIntersectionStretchyBeam(leftPickRay.origin, leftPickRay.direction, leftFinal); - } + + this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); + + } }; @@ -496,7 +408,7 @@ function Teleporter() { this.intersection = intersection; var position = { x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y + 0.1, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y/2, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { @@ -611,7 +523,6 @@ function cleanup() { teleporter.disableMappings(); teleporter.deleteStretchyBeam(); teleporter.deleteTargetOverlay(); - teleporter.deleteNoIntersectionStretchyBeam(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From b2ef491b973ff704567180bbfae22ef94805a027 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 13:20:42 -0700 Subject: [PATCH 38/78] rotate target toward you --- scripts/system/controllers/teleport.js | 73 ++++++++++++++------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index d4ad82721d..e45b2f3127 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -26,28 +26,6 @@ //v?: haptic feedback //v?: show room boundaries when choosing a place to teleport -var inTeleportMode = false; - -var currentFadeSphereOpacity = 1; -var fadeSphereInterval = null; -//milliseconds between fading one-tenth -- so this is a half second fade total -var FADE_IN_INTERVAL = 50; -var FADE_OUT_INTERVAL = 50; - -var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; -var BEAM_MODEL_URL_NO_INTERSECTION = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam2.fbx"; - - -var STRETCHY_BEAM_DIMENSIONS_X = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; - -var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; -var TARGET_MODEL_DIMENSIONS = { - x: 1.15, - y: 0.5, - z: 1.15 -}; //swirly thing, not sure we'll get to use it @@ -59,6 +37,29 @@ var TARGET_MODEL_DIMENSIONS = { // z: 1.22 // }; + +var inTeleportMode = false; + +var currentFadeSphereOpacity = 1; +var fadeSphereInterval = null; +//milliseconds between fading one-tenth -- so this is a half second fade total +var FADE_IN_INTERVAL = 50; +var FADE_OUT_INTERVAL = 50; + +var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; + +var STRETCHY_BEAM_DIMENSIONS_X = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; +var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; + +var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; +var TARGET_MODEL_DIMENSIONS = { + x: 1.15, + y: 0.5, + z: 1.15 + +}; + function ThumbPad(hand) { this.hand = hand; var _this = this; @@ -117,11 +118,15 @@ function Teleporter() { this.rotateTargetTorwardMe = function() { var cameraEuler = Quat.safeEulerAngles(Camera.orientation); + print('camera euler' + JSON.stringify(cameraEuler)) var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { x: 0, y: 1, z: 0 }); + + print('towardsMe' + JSON.stringify(towardsMe)) + return towardsMe } @@ -179,25 +184,18 @@ function Teleporter() { var position; if (noIntersection === true) { - this.deleteTargetOverlay(); + this.deleteTargetOverlay(); print('no intersection') - position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION/2 , direction)); + position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION / 2, direction)); } else { print('intersection, find midpoint') position = _this.findMidpoint(handPosition, intersection); } - - print('rotation: ' + JSON.stringify(rotation)); - print('position: ' + JSON.stringify(position)); - print('dimensions: ' + JSON.stringify(dimensions)); - - - Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, - rotation: Quat.multiply(rotation, Quat.angleAxis(180, { + rotation: Quat.multiply(rotation, Quat.angleAxis(180, { x: 0, y: 1, z: 0 @@ -406,14 +404,21 @@ function Teleporter() { this.updateTargetOverlay = function(intersection) { this.intersection = intersection; + print('intersection is: ' + JSON.stringify(intersection)) + + // var rotation = Quat.getUp(intersection.intersection.surfaceNormal); + + 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, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, z: intersection.intersection.z } Overlays.editOverlay(this.targetOverlay, { position: position, - visible: true + rotation:Quat.fromPitchYawRollDegrees(0,euler.y,0), + // rotation: this.rotateTargetTorwardMe() }); }; From e993a695195aca298f1898449d2cd0ed0a16073b Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 14:06:53 -0700 Subject: [PATCH 39/78] last commit for stretched beams -- doesnt work at long distances due to lack of precision --- scripts/system/controllers/teleport.js | 115 +++++++++++-------------- 1 file changed, 48 insertions(+), 67 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index e45b2f3127..0f7403fcc3 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,42 +1,12 @@ -// by james b. pollack @imgntn on 7/2/2016 +// Created by james b. pollack @imgntn on 7/2/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Creates a beam and target and then teleports you there when you let go of the activation button. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -///BUG: there is currently something going on when you try to adjust the dimensions of a second overlay model, it seems to be off by a factor of 10 or already changed or something weird. - -//v1 -//check if trigger is down xxx -//if trigger is down, check if thumb is down xxx -//if both are down, enter 'teleport mode' xxx -//aim controller to change landing spot xxx -//visualize aim + spot (line + circle) xxx -//release trigger to teleport then exit teleport mode xxx -//if thumb is release, exit teleport mode xxx - - -//v2 -// try just thumb to teleport xxx - -//v3 -//fade in/out -//stretchy beam instead of GL line xxx - - -//try moving to final destination in 4 steps: 50% 75% 90% 100% (arrival) - -//v?: haptic feedback -//v?: show room boundaries when choosing a place to teleport - - - -//swirly thing, not sure we'll get to use it -// var TARGET_MODEL_URL='http://hifi-content.s3.amazonaws.com/alan/dev/Cyclone-4.fbx'; - -// var TARGET_MODEL_DIMENSIONS = { -// x: 0.93, -// y: 0.93, -// z: 1.22 -// }; - var inTeleportMode = false; @@ -116,20 +86,6 @@ function Teleporter() { _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); }; - this.rotateTargetTorwardMe = function() { - var cameraEuler = Quat.safeEulerAngles(Camera.orientation); - print('camera euler' + JSON.stringify(cameraEuler)) - var towardsMe = Quat.angleAxis(cameraEuler.y + 180, { - x: 0, - y: 1, - z: 0 - }); - - print('towardsMe' + JSON.stringify(towardsMe)) - return towardsMe - } - - this.createMappings = function() { print('jbp create mappings internal'); // peek at the trigger and thumbs to store their values @@ -192,6 +148,14 @@ function Teleporter() { position = _this.findMidpoint(handPosition, intersection); } + + print('AT UPDATE DIMENSIONS: ' + JSON.stringify(dimensions)) + print('AT UPDATE POSITION: ' + JSON.stringify(position)) + print('AT UPDATE ROTATION: ' + JSON.stringify(rotation)) + if (noIntersection === false) { + print('AT UPDATE NORMAL: ' + JSON.stringify(intersection.surfaceNormal)) + + } Overlays.editOverlay(_this.stretchyBeam, { dimensions: dimensions, position: position, @@ -325,15 +289,13 @@ function Teleporter() { this.rightRay = function() { + var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); - var rightRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).rotation) + var rightControllerRotation = Controller.getPoseValue(Controller.Standard.RightHand).rotation; + var rightRotation = Quat.multiply(MyAvatar.orientation, rightControllerRotation) - var rightPickRay = { - origin: rightPosition, - direction: Quat.getUp(rightRotation), - }; var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(90, { x: 1, @@ -341,6 +303,11 @@ function Teleporter() { z: 0 })); + var rightPickRay = { + origin: rightPosition, + direction: Quat.getUp(rightRotation), + }; + this.rightPickRay = rightPickRay; var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); @@ -349,6 +316,10 @@ function Teleporter() { if (rightIntersection.intersects) { + if (rightIntersection.distance > 250) { + print('VERY FAR INTERSECTION') + + } if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { @@ -368,10 +339,6 @@ function Teleporter() { var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) - var leftPickRay = { - origin: leftPosition, - direction: Quat.getUp(leftRotation), - }; var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { x: 1, @@ -379,6 +346,12 @@ function Teleporter() { z: 0 })); + + var leftPickRay = { + origin: leftPosition, + direction: Quat.getUp(leftRotation), + }; + this.leftPickRay = leftPickRay; var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); @@ -386,6 +359,11 @@ function Teleporter() { var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { + if (leftIntersection.distance > 250) { + print('VERY FAR INTERSECTION') + this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); + return + } if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { @@ -404,11 +382,8 @@ function Teleporter() { this.updateTargetOverlay = function(intersection) { this.intersection = intersection; - print('intersection is: ' + JSON.stringify(intersection)) - // var rotation = Quat.getUp(intersection.intersection.surfaceNormal); - - var rotation = Quat.lookAt(intersection.intersection,MyAvatar.position,Vec3.UP) + var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) var euler = Quat.safeEulerAngles(rotation) var position = { x: intersection.intersection.x, @@ -417,8 +392,7 @@ function Teleporter() { } Overlays.editOverlay(this.targetOverlay, { position: position, - rotation:Quat.fromPitchYawRollDegrees(0,euler.y,0), - // rotation: this.rotateTargetTorwardMe() + rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), }); }; @@ -431,15 +405,22 @@ function Teleporter() { Messages.sendLocalMessage('Hifi-Hand-Disabler', 'none'); }; + this.triggerHaptics = function() { + var hand = this.hand === 'left' ? 0 : 1; + var haptic = Controller.triggerShortHapticPulse(0.2, hand); + }; + this.teleport = function(value) { print('TELEPORT CALLED'); - var offset = getAvatarFootOffset(); if (_this.intersection !== null) { + var offset = getAvatarFootOffset(); _this.intersection.intersection.y += offset; MyAvatar.position = _this.intersection.intersection; } + + this.triggerHaptics(); this.exitTeleportMode(); }; } From dc8e21d76ca24fad26982e29972a86e7bae4da48 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 15:16:31 -0700 Subject: [PATCH 40/78] cleanup stretchy stuff --- scripts/system/controllers/teleport.js | 182 +++++++++++++------------ 1 file changed, 98 insertions(+), 84 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 0f7403fcc3..4f0782da0c 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,12 +16,6 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; -var BEAM_MODEL_URL = "http://hifi-content.s3.amazonaws.com/james/teleporter/teleportBeam.fbx"; - -var STRETCHY_BEAM_DIMENSIONS_X = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Y = 0.04; -var STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION = 20; - var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -62,9 +56,9 @@ function Teleporter() { var _this = this; this.intersection = null; this.targetProps = null; + this.rightOverlayLine = null; + this.leftOverlayLine = null; this.targetOverlay = null; - this.stretchyBeam = null; - this.noIntersectionStretchyBeam = null; this.updateConnected = null; this.initialize = function() { @@ -120,61 +114,6 @@ function Teleporter() { return midpoint }; - this.updateOrCreateStretchyBeam = function(handPosition, intersection, rotation, direction, noIntersection) { - - if (_this.stretchyBeam === null) { - var beamProps = { - url: BEAM_MODEL_URL, - position: MyAvatar.position, - - }; - - _this.stretchyBeam = Overlays.addOverlay("model", beamProps); - } - - var dimensions = { - x: STRETCHY_BEAM_DIMENSIONS_X, - y: STRETCHY_BEAM_DIMENSIONS_Y, - z: intersection !== null ? Vec3.distance(handPosition, intersection.intersection) : STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION - }; - - var position; - if (noIntersection === true) { - this.deleteTargetOverlay(); - print('no intersection') - position = Vec3.sum(handPosition, Vec3.multiply(STRETCHY_BEAM_DIMENSIONS_Z_NO_INTESECTION / 2, direction)); - } else { - print('intersection, find midpoint') - position = _this.findMidpoint(handPosition, intersection); - } - - - print('AT UPDATE DIMENSIONS: ' + JSON.stringify(dimensions)) - print('AT UPDATE POSITION: ' + JSON.stringify(position)) - print('AT UPDATE ROTATION: ' + JSON.stringify(rotation)) - if (noIntersection === false) { - print('AT UPDATE NORMAL: ' + JSON.stringify(intersection.surfaceNormal)) - - } - Overlays.editOverlay(_this.stretchyBeam, { - dimensions: dimensions, - position: position, - rotation: Quat.multiply(rotation, Quat.angleAxis(180, { - x: 0, - y: 1, - z: 0 - })) - }) - - }; - - this.deleteStretchyBeam = function() { - if (_this.stretchyBeam !== null) { - Overlays.deleteOverlay(_this.stretchyBeam); - _this.stretchyBeam = null; - } - }; - this.createFadeSphere = function(avatarHead) { var sphereProps = { @@ -254,12 +193,17 @@ function Teleporter() { this.targetOverlay = null; } + this.turnOffOverlayBeams = function() { + this.rightOverlayOff(); + this.leftOverlayOff(); + } + this.exitTeleportMode = function(value) { print('jbp value on exit: ' + value); Script.update.disconnect(this.update); this.updateConnected = null; this.disableMappings(); - this.deleteStretchyBeam(); + this.turnOffOverlayBeams(); this.deleteTargetOverlay(); this.enableGrab(); Script.setTimeout(function() { @@ -312,24 +256,24 @@ function Teleporter() { var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); + this.rightLineOn(rightPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); + var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - if (rightIntersection.distance > 250) { - print('VERY FAR INTERSECTION') - - } if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { this.createTargetOverlay(); } - this.updateOrCreateStretchyBeam(rightPickRay.origin, rightIntersection, rightFinal, rightPickRay.direction, false); + } else { - - this.updateOrCreateStretchyBeam(rightPickRay.origin, null, rightFinal, rightPickRay.direction, true); - + this.deleteTargetOverlay(); } } @@ -354,31 +298,101 @@ function Teleporter() { this.leftPickRay = leftPickRay; - var location = Vec3.sum(leftPickRay.origin, Vec3.multiply(leftPickRay.direction, 500)); + var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 500)); + + this.leftLineOn(leftPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { - if (leftIntersection.distance > 250) { - print('VERY FAR INTERSECTION') - this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); - return - } + if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { this.createTargetOverlay(); } - this.updateOrCreateStretchyBeam(leftPickRay.origin, leftIntersection, leftFinal, leftPickRay.direction, false); + } else { - - this.updateOrCreateStretchyBeam(leftPickRay.origin, null, leftFinal, leftPickRay.direction, true); - - + this.deleteTargetOverlay(); } }; + this.rightLineOn = function(closePoint, farPoint, color) { + // draw a line + if (this.rightOverlayLine === null) { + var lineProperties = { + start: closePoint, + end: farPoint, + color: color, + ignoreRayIntersection: true, // always ignore this + visible: true, + alpha: 1, + solid: true, + glow: 1.0 + }; + + this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.rightOverlayLine, { + lineWidth: 50, + start: closePoint, + end: farPoint, + color: color, + visible: true, + ignoreRayIntersection: true, // always ignore this + alpha: 1 + }); + } + }; + + this.leftLineOn = function(closePoint, farPoint, color) { + // draw a line + print('COLOR ON LINE : ' + JSON.stringify(color)) + if (this.leftOverlayLine === null) { + var lineProperties = { + ignoreRayIntersection: true, // always ignore this + start: closePoint, + end: farPoint, + color: color, + visible: true, + alpha: 1, + solid: true, + glow: 1.0 + }; + + this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + var success = Overlays.editOverlay(this.leftOverlayLine, { + start: closePoint, + end: farPoint, + color: color, + visible: true, + alpha: 1, + solid: true, + glow: 1.0 + }); + } + }; + this.rightOverlayOff = function() { + if (this.rightOverlayLine !== null) { + Overlays.deleteOverlay(this.rightOverlayLine); + this.rightOverlayLine = null; + } + }; + + this.leftOverlayOff = function() { + if (this.leftOverlayLine !== null) { + Overlays.deleteOverlay(this.leftOverlayLine); + this.leftOverlayLine = null; + } + }; this.updateTargetOverlay = function(intersection) { this.intersection = intersection; @@ -507,8 +521,8 @@ Script.scriptEnding.connect(cleanup); function cleanup() { teleportMapping.disable(); teleporter.disableMappings(); - teleporter.deleteStretchyBeam(); teleporter.deleteTargetOverlay(); + teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); } From 696169ed3731f93d95b104c862e34313d259e488 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 15:19:10 -0700 Subject: [PATCH 41/78] terminate lines at intersection --- scripts/system/controllers/teleport.js | 33 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4f0782da0c..caa898dd76 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -256,16 +256,15 @@ function Teleporter() { var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 500)); - this.rightLineOn(rightPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - + this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, { + red: 7, + green: 36, + blue: 44 + }); if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { @@ -273,6 +272,12 @@ function Teleporter() { } } else { + + this.rightLineOn(rightPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); this.deleteTargetOverlay(); } } @@ -300,16 +305,16 @@ function Teleporter() { var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 500)); - this.leftLineOn(leftPickRay.origin, location, { - red: 7, - green: 36, - blue: 44 - }); var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity]); if (leftIntersection.intersects) { + this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, { + red: 7, + green: 36, + blue: 44 + }); if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { @@ -318,6 +323,12 @@ function Teleporter() { } else { + + this.leftLineOn(leftPickRay.origin, location, { + red: 7, + green: 36, + blue: 44 + }); this.deleteTargetOverlay(); } }; From 5cb585aec34bdc36245fadf8e39906ef8481fc1a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 15:25:01 -0700 Subject: [PATCH 42/78] remove debug prints --- scripts/system/controllers/teleport.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index caa898dd76..2345ac7384 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -29,7 +29,6 @@ function ThumbPad(hand) { var _this = this; this.buttonPress = function(value) { - print('jbp pad press: ' + value + " on: " + _this.hand) _this.buttonValue = value; }; @@ -62,14 +61,11 @@ function Teleporter() { this.updateConnected = null; this.initialize = function() { - print('jbp initialize') this.createMappings(); this.disableGrab(); }; this.createTargetOverlay = function() { - print('creating target overlay') - var targetOverlayProps = { url: TARGET_MODEL_URL, @@ -81,7 +77,6 @@ function Teleporter() { }; this.createMappings = function() { - print('jbp create mappings internal'); // peek at the trigger and thumbs to store their values teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); @@ -90,7 +85,6 @@ function Teleporter() { }; this.disableMappings = function() { - print('jbp disable mappings internal') Controller.disableMapping(teleporter.telporterMappingInternalName); }; @@ -99,7 +93,6 @@ function Teleporter() { return; } - print('jbp hand on entering teleport mode: ' + hand); inTeleportMode = true; this.teleportHand = hand; this.initialize(); @@ -199,7 +192,6 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - print('jbp value on exit: ' + value); Script.update.disconnect(this.update); this.updateConnected = null; this.disableMappings(); @@ -363,8 +355,6 @@ function Teleporter() { }; this.leftLineOn = function(closePoint, farPoint, color) { - // draw a line - print('COLOR ON LINE : ' + JSON.stringify(color)) if (this.leftOverlayLine === null) { var lineProperties = { ignoreRayIntersection: true, // always ignore this @@ -437,7 +427,6 @@ function Teleporter() { this.teleport = function(value) { - print('TELEPORT CALLED'); if (_this.intersection !== null) { var offset = getAvatarFootOffset(); From 1728878982d165b2c232c73d7e3402e98e465fd5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 17:52:18 -0700 Subject: [PATCH 43/78] working smooth arrival --- scripts/system/controllers/teleport.js | 77 +++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 2345ac7384..b2e462dd4a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,6 +16,23 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; + +//slow +var SMOOTH_ARRIVAL_SPACING = 150; +var NUMBER_OF_STEPS = 2; + +//medium-slow +var SMOOTH_ARRIVAL_SPACING = 100; +var NUMBER_OF_STEPS = 4; + +//medium-fast +var SMOOTH_ARRIVAL_SPACING = 33; +var NUMBER_OF_STEPS = 6; + +//fast +var SMOOTH_ARRIVAL_SPACING = 10; +var NUMBER_OF_STEPS = 20; + var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -101,8 +118,8 @@ function Teleporter() { }; - this.findMidpoint = function(handPosition, intersection) { - var xy = Vec3.sum(handPosition, intersection.intersection); + this.findMidpoint = function(start, end) { + var xy = Vec3.sum(start, end); var midpoint = Vec3.multiply(0.5, xy); return midpoint }; @@ -196,7 +213,7 @@ function Teleporter() { this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); - this.deleteTargetOverlay(); + this.enableGrab(); Script.setTimeout(function() { inTeleportMode = false; @@ -396,7 +413,7 @@ function Teleporter() { }; this.updateTargetOverlay = function(intersection) { - this.intersection = intersection; + _this.intersection = intersection; var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) var euler = Quat.safeEulerAngles(rotation) @@ -427,16 +444,60 @@ function Teleporter() { this.teleport = function(value) { - if (_this.intersection !== null) { + var offset = getAvatarFootOffset(); _this.intersection.intersection.y += offset; - MyAvatar.position = _this.intersection.intersection; + // MyAvatar.position = _this.intersection.intersection; + this.exitTeleportMode(); + this.smoothArrival(); + } - this.triggerHaptics(); - this.exitTeleportMode(); }; + + 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); + print('ARRIVAL POINTS: ' + JSON.stringify(_this.arrivalPoints)); + print('end point: ' + JSON.stringify(_this.intersection.intersection)) + _this.smoothArrivalInterval = Script.setInterval(function() { + print(_this.arrivalPoints.length+" arrival points remaining") + if (_this.arrivalPoints.length === 0) { + Script.clearInterval(_this.smoothArrivalInterval); + _this.triggerHaptics(); + _this.deleteTargetOverlay(); + return; + } + + var landingPoint = _this.arrivalPoints.shift(); + print('landing at: ' + JSON.stringify(landingPoint)) + MyAvatar.position =landingPoint; + + + }, SMOOTH_ARRIVAL_SPACING) + } } From 5626938fb40b97c59ff130d61a8a59bf9c5481d0 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 18:13:42 -0700 Subject: [PATCH 44/78] add instant mode --- scripts/system/controllers/teleport.js | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b2e462dd4a..c985259d21 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,22 +16,23 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; - +var NUMBER_OF_STEPS=0; +var SMOOTH_ARRIVAL_SPACING=0; //slow -var SMOOTH_ARRIVAL_SPACING = 150; -var NUMBER_OF_STEPS = 2; +// var SMOOTH_ARRIVAL_SPACING = 150; +// var NUMBER_OF_STEPS = 2; //medium-slow -var SMOOTH_ARRIVAL_SPACING = 100; -var NUMBER_OF_STEPS = 4; +// var SMOOTH_ARRIVAL_SPACING = 100; +// var NUMBER_OF_STEPS = 4; -//medium-fast -var SMOOTH_ARRIVAL_SPACING = 33; -var NUMBER_OF_STEPS = 6; +// //medium-fast +// var SMOOTH_ARRIVAL_SPACING = 33; +// var NUMBER_OF_STEPS = 6; -//fast -var SMOOTH_ARRIVAL_SPACING = 10; -var NUMBER_OF_STEPS = 20; +// //fast +// var SMOOTH_ARRIVAL_SPACING = 10; +// var NUMBER_OF_STEPS = 20; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -449,9 +450,9 @@ function Teleporter() { var offset = getAvatarFootOffset(); _this.intersection.intersection.y += offset; // MyAvatar.position = _this.intersection.intersection; - this.exitTeleportMode(); + this.exitTeleportMode(); this.smoothArrival(); - + } }; @@ -483,7 +484,7 @@ function Teleporter() { print('ARRIVAL POINTS: ' + JSON.stringify(_this.arrivalPoints)); print('end point: ' + JSON.stringify(_this.intersection.intersection)) _this.smoothArrivalInterval = Script.setInterval(function() { - print(_this.arrivalPoints.length+" arrival points remaining") + print(_this.arrivalPoints.length + " arrival points remaining") if (_this.arrivalPoints.length === 0) { Script.clearInterval(_this.smoothArrivalInterval); _this.triggerHaptics(); @@ -493,7 +494,7 @@ function Teleporter() { var landingPoint = _this.arrivalPoints.shift(); print('landing at: ' + JSON.stringify(landingPoint)) - MyAvatar.position =landingPoint; + MyAvatar.position = landingPoint; }, SMOOTH_ARRIVAL_SPACING) From 54e0c131a562474b38968041d380d4ee9b0a40c8 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Wed, 6 Jul 2016 18:30:50 -0700 Subject: [PATCH 45/78] cleanup and protect against some interval naughtiness --- scripts/system/controllers/teleport.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index c985259d21..f52cd55a84 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,11 +16,11 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; -var NUMBER_OF_STEPS=0; -var SMOOTH_ARRIVAL_SPACING=0; -//slow -// var SMOOTH_ARRIVAL_SPACING = 150; -// var NUMBER_OF_STEPS = 2; +// var NUMBER_OF_STEPS = 0; +// var SMOOTH_ARRIVAL_SPACING = 0; +// slow +var SMOOTH_ARRIVAL_SPACING = 150; +var NUMBER_OF_STEPS = 2; //medium-slow // var SMOOTH_ARRIVAL_SPACING = 100; @@ -77,6 +77,7 @@ function Teleporter() { this.leftOverlayLine = null; this.targetOverlay = null; this.updateConnected = null; + this.smoothArrivalInterval=null; this.initialize = function() { this.createMappings(); @@ -111,6 +112,9 @@ function Teleporter() { return; } + if(this.smoothArrivalInterval!==null){ + Script.clearInterval(this.smoothArrivalInterval); + } inTeleportMode = true; this.teleportHand = hand; this.initialize(); @@ -487,14 +491,17 @@ function Teleporter() { print(_this.arrivalPoints.length + " arrival points remaining") if (_this.arrivalPoints.length === 0) { Script.clearInterval(_this.smoothArrivalInterval); - _this.triggerHaptics(); - _this.deleteTargetOverlay(); return; } var landingPoint = _this.arrivalPoints.shift(); print('landing at: ' + JSON.stringify(landingPoint)) MyAvatar.position = landingPoint; + if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { + print('clear target overlay') + _this.deleteTargetOverlay(); + _this.triggerHaptics(); + } }, SMOOTH_ARRIVAL_SPACING) From 8212ada4dec4930e7b270b8ed0a342cd0e43fc73 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 11:42:41 -0700 Subject: [PATCH 46/78] introduce delay --- scripts/system/controllers/teleport.js | 39 ++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index f52cd55a84..d8dd38c15a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -16,11 +16,13 @@ var fadeSphereInterval = null; var FADE_IN_INTERVAL = 50; var FADE_OUT_INTERVAL = 50; -// var NUMBER_OF_STEPS = 0; -// var SMOOTH_ARRIVAL_SPACING = 0; -// slow -var SMOOTH_ARRIVAL_SPACING = 150; -var NUMBER_OF_STEPS = 2; +// instant +var NUMBER_OF_STEPS = 0; +var SMOOTH_ARRIVAL_SPACING = 0; + +// // slow +// var SMOOTH_ARRIVAL_SPACING = 150; +// var NUMBER_OF_STEPS = 2; //medium-slow // var SMOOTH_ARRIVAL_SPACING = 100; @@ -77,7 +79,7 @@ function Teleporter() { this.leftOverlayLine = null; this.targetOverlay = null; this.updateConnected = null; - this.smoothArrivalInterval=null; + this.smoothArrivalInterval = null; this.initialize = function() { this.createMappings(); @@ -112,7 +114,7 @@ function Teleporter() { return; } - if(this.smoothArrivalInterval!==null){ + if (this.smoothArrivalInterval !== null) { Script.clearInterval(this.smoothArrivalInterval); } inTeleportMode = true; @@ -565,6 +567,8 @@ var rightTrigger = new Trigger('right'); var mappingName, teleportMapping; +var TELEPORT_DELAY = 100; + function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); @@ -572,11 +576,24 @@ function registerMappings() { teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - teleportMapping.from(leftPad.down).to(function() { - teleporter.enterTeleportMode('left') + teleportMapping.from(leftPad.down).to(function(value) { + print('left down' + value) + if (value === 1) { + + Script.setTimeout(function() { + teleporter.enterTeleportMode('left') + }, TELEPORT_DELAY) + } + }); - teleportMapping.from(rightPad.down).to(function() { - teleporter.enterTeleportMode('right') + teleportMapping.from(rightPad.down).to(function(value) { + print('right down' + value) + if (value === 1) { + + Script.setTimeout(function() { + teleporter.enterTeleportMode('right') + }, TELEPORT_DELAY) + } }); } From 93081e73d8b784276186927f6bee65db7eeaf45a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 12:12:22 -0700 Subject: [PATCH 47/78] thumb and trigger --- scripts/system/controllers/teleport.js | 58 +++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index d8dd38c15a..147a8634e2 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -121,7 +121,7 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - Script.update.connect(this.update); + Script.update.connect(this.updateForThumbAndTrigger); }; @@ -216,11 +216,10 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - Script.update.disconnect(this.update); + Script.update.disconnect(this.updateForThumbAndTrigger); this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); - this.enableGrab(); Script.setTimeout(function() { inTeleportMode = false; @@ -247,6 +246,35 @@ function Teleporter() { }; + this.updateForThumbAndTrigger = function() { + + if (teleporter.teleportHand === 'left') { + teleporter.leftRay(); + if (leftPad.buttonValue === 0) { + _this.exitTeleportMode(); + _this.deleteTargetOverlay(); + return; + } + if (leftTrigger.buttonValue === 0 && inTeleportMode === true) { + _this.teleport(); + return; + } + + } else { + teleporter.rightRay(); + if (rightPad.buttonValue === 0) { + _this.exitTeleportMode(); + _this.deleteTargetOverlay(); + return; + } + if (rightTrigger.buttonValue === 0 && inTeleportMode === true) { + _this.teleport(); + return; + } + } + + }; + this.rightRay = function() { @@ -595,9 +623,29 @@ function registerMappings() { }, TELEPORT_DELAY) } }); + } -registerMappings(); + +function registerMappingsWithThumbAndTrigger() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); + teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); + + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); + + teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function(value) { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { + teleporter.enterTeleportMode('right') + }); +} + +//registerMappings(); +registerMappingsWithThumbAndTrigger() var teleporter = new Teleporter(); Controller.enableMapping(mappingName); @@ -610,6 +658,6 @@ function cleanup() { teleporter.deleteTargetOverlay(); teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { - Script.update.disconnect(teleporter.update); + Script.update.disconnect(teleporter.updateForThumbAndTrigger); } } \ No newline at end of file From be770c81777dea43f48f149ad2c9636bb8053f87 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 12:39:11 -0700 Subject: [PATCH 48/78] make thumb and trigger mode an option --- scripts/system/controllers/teleport.js | 33 ++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 147a8634e2..a59711b025 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -36,6 +36,8 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; +var USE_THUMB_AND_TRIGGER_MODE = true; + var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -121,8 +123,12 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - Script.update.connect(this.updateForThumbAndTrigger); + if (USE_THUMB_AND_TRIGGER_MODE === true) { + Script.update.connect(this.updateForThumbAndTrigger); + } else { + Script.update.connect(this.update); + } }; this.findMidpoint = function(start, end) { @@ -216,7 +222,14 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - Script.update.disconnect(this.updateForThumbAndTrigger); + if (USE_THUMB_AND_TRIGGER_MODE === true) { + Script.update.disconnect(this.updateForThumbAndTrigger); + + } else { + Script.update.disconnect(this.update); + + } + this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); @@ -644,8 +657,12 @@ function registerMappingsWithThumbAndTrigger() { }); } -//registerMappings(); -registerMappingsWithThumbAndTrigger() +if (USE_THUMB_AND_TRIGGER_MODE === true) { + registerMappingsWithThumbAndTrigger(); +} else { + registerMappings(); +} + var teleporter = new Teleporter(); Controller.enableMapping(mappingName); @@ -658,6 +675,12 @@ function cleanup() { teleporter.deleteTargetOverlay(); teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { - Script.update.disconnect(teleporter.updateForThumbAndTrigger); + + if (USE_THUMB_AND_TRIGGER_MODE === true) { + Script.update.disconnect(teleporter.updateForThumbAndTrigger); + + } else { + Script.update.disconnect(teleporter.update); + } } } \ No newline at end of file From 4a9f6e677c4187bb89c23b227409630ac203a987 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 12:52:24 -0700 Subject: [PATCH 49/78] add new target model --- scripts/system/controllers/teleport.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index a59711b025..577d6bf456 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -36,9 +36,9 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; -var USE_THUMB_AND_TRIGGER_MODE = true; +var USE_THUMB_AND_TRIGGER_MODE = false; -var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/Tele-destiny.fbx'; +var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, From aba6924dc7b9c844da3586776478763d62b43fc8 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 7 Jul 2016 13:33:36 -0700 Subject: [PATCH 50/78] Fix MyAvatar name and url accessors to JS. Q_INVOKABLE does not support returning references. MyAvatar.getFullAvatarURLFromPreferences() now returns a QUrl instead of a reference. MyAvatar.getFullAvatarModelName() now returns a QString instead of reference. This should fix the JS API for these methods, they will now receive a JS string instead of undefined. --- interface/src/avatar/MyAvatar.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 96fa999de5..a21922f1b1 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -212,8 +212,8 @@ public: virtual void clearJointsData() override; Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); - Q_INVOKABLE const QUrl& getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } - Q_INVOKABLE const QString& getFullAvatarModelName() const { return _fullAvatarModelName; } + Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } + Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; } void resetFullAvatarURL(); From 5c83db3b28302a817363877780dbde73dff2e574 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 14:37:51 -0700 Subject: [PATCH 51/78] take care of naughty intervals --- scripts/system/controllers/teleport.js | 73 ++++++++++++++++++-------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 577d6bf456..dfc471182d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -12,9 +12,14 @@ var inTeleportMode = false; var currentFadeSphereOpacity = 1; var fadeSphereInterval = null; +var fadeSphereUpdateInterval = null; //milliseconds between fading one-tenth -- so this is a half second fade total -var FADE_IN_INTERVAL = 50; -var FADE_OUT_INTERVAL = 50; +var USE_FADE_MODE = true; +var USE_FADE_IN = false; +var USE_FADE_OUT = true; +var FADE_IN_INTERVAL = 25; +var FADE_OUT_INTERVAL = 25; + // instant var NUMBER_OF_STEPS = 0; @@ -24,7 +29,7 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 150; // var NUMBER_OF_STEPS = 2; -//medium-slow +// medium-slow // var SMOOTH_ARRIVAL_SPACING = 100; // var NUMBER_OF_STEPS = 4; @@ -32,10 +37,13 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 33; // var NUMBER_OF_STEPS = 6; -// //fast +//fast // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; + +// var NUMBER_OF_STEPS=9; +// var SMOOTH_ARRIVAL_SPACING=25; var USE_THUMB_AND_TRIGGER_MODE = false; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; @@ -82,6 +90,7 @@ function Teleporter() { this.targetOverlay = null; this.updateConnected = null; this.smoothArrivalInterval = null; + this.fadeSphere = null; this.initialize = function() { this.createMappings(); @@ -138,10 +147,11 @@ function Teleporter() { }; + this.createFadeSphere = function(avatarHead) { var sphereProps = { position: avatarHead, - size: 0.25, + size: -1, color: { red: 0, green: 0, @@ -151,30 +161,40 @@ function Teleporter() { solid: true, visible: true, ignoreRayIntersection: true, - drawInFront: true + drawInFront: false }; - currentFadeSphereOpacity = 1; - - _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); + currentFadeSphereOpacity = 10; + _this.fadeSphere = Overlays.addOverlay("cube", sphereProps); + Script.clearInterval(fadeSphereInterval) Script.update.connect(_this.updateFadeSphere); + if (USE_FADE_OUT === true) { + this.fadeSphereOut(); + } + if (USE_FADE_IN === true) { + this.fadeSphereIn(); + } + }; this.fadeSphereOut = function() { fadeSphereInterval = Script.setInterval(function() { - if (currentFadeSphereOpacity === 0) { + if (currentFadeSphereOpacity <= 0) { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); - fadeSphereInterval = null; + fadeSphereInterval = null; + print('sphere done fading out'); return; } if (currentFadeSphereOpacity > 0) { - currentFadeSphereOpacity -= 0.1; + currentFadeSphereOpacity = currentFadeSphereOpacity - 1; } + + print('setting sphere alpha to: ' + currentFadeSphereOpacity) Overlays.editOverlay(_this.fadeSphere, { - alpha: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity / 10 }) }, FADE_OUT_INTERVAL); @@ -182,17 +202,18 @@ function Teleporter() { this.fadeSphereIn = function() { fadeSphereInterval = Script.setInterval(function() { - if (currentFadeSphereOpacity === 1) { + if (currentFadeSphereOpacity >= 1) { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); fadeSphereInterval = null; + print('sphere done fading in') return; } if (currentFadeSphereOpacity < 1) { - currentFadeSphereOpacity += 0.1; + currentFadeSphereOpacity = currentFadeSphereOpacity - 1; } Overlays.editOverlay(_this.fadeSphere, { - alpha: currentFadeSphereOpacity + alpha: currentFadeSphereOpacity / 10 }) }, FADE_IN_INTERVAL); @@ -206,8 +227,13 @@ function Teleporter() { }; this.deleteFadeSphere = function() { - Script.update.disconnect(_this.updateFadeSphere); - Overlays.deleteOverlay(_this.fadeSphere); + if (_this.fadeSphere !== null) { + print('deleting fade sphere'); + Script.update.disconnect(_this.updateFadeSphere); + Overlays.deleteOverlay(_this.fadeSphere); + _this.fadeSphere = null; + } + }; this.deleteTargetOverlay = function() { @@ -227,7 +253,6 @@ function Teleporter() { } else { Script.update.disconnect(this.update); - } this.updateConnected = null; @@ -492,10 +517,12 @@ function Teleporter() { this.teleport = function(value) { - if (_this.intersection !== null) { - + if (this.intersection !== null) { + if (USE_FADE_MODE === true) { + this.createFadeSphere(); + } var offset = getAvatarFootOffset(); - _this.intersection.intersection.y += offset; + this.intersection.intersection.y += offset; // MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); this.smoothArrival(); @@ -540,6 +567,7 @@ function Teleporter() { var landingPoint = _this.arrivalPoints.shift(); print('landing at: ' + JSON.stringify(landingPoint)) MyAvatar.position = landingPoint; + if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { print('clear target overlay') _this.deleteTargetOverlay(); @@ -674,6 +702,7 @@ function cleanup() { teleporter.disableMappings(); teleporter.deleteTargetOverlay(); teleporter.turnOffOverlayBeams(); + teleporter.deleteFadeSphere(); if (teleporter.updateConnected !== null) { if (USE_THUMB_AND_TRIGGER_MODE === true) { From 71392bc1b24eb632c0549c6d91369e35316c864d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 16:30:30 -0700 Subject: [PATCH 52/78] change to sphere now that sphere alpha works. --- scripts/system/controllers/teleport.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index dfc471182d..57806a9b36 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -6,8 +6,22 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +//FEATURES: +// ENTRY MODES +// Thumbpad only +// Thumpad + trigger +// JUMP MODES +// Instant +// Smoth arrival aka stepwise (number of steps, duration of step) + +// FADE MODE +// Cube-overlay (steps,duration) +// Model-overlay (steps, duration) +// Fade out / fade in + +// defaults with instant jump with fade. to try smooth arrival mode, change use fade to false and then switch the number of arrival steps and spacing var inTeleportMode = false; var currentFadeSphereOpacity = 1; @@ -20,7 +34,6 @@ var USE_FADE_OUT = true; var FADE_IN_INTERVAL = 25; var FADE_OUT_INTERVAL = 25; - // instant var NUMBER_OF_STEPS = 0; var SMOOTH_ARRIVAL_SPACING = 0; @@ -33,7 +46,7 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 100; // var NUMBER_OF_STEPS = 4; -// //medium-fast +//medium-fast // var SMOOTH_ARRIVAL_SPACING = 33; // var NUMBER_OF_STEPS = 6; @@ -42,8 +55,6 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var NUMBER_OF_STEPS = 20; -// var NUMBER_OF_STEPS=9; -// var SMOOTH_ARRIVAL_SPACING=25; var USE_THUMB_AND_TRIGGER_MODE = false; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; @@ -166,7 +177,7 @@ function Teleporter() { currentFadeSphereOpacity = 10; - _this.fadeSphere = Overlays.addOverlay("cube", sphereProps); + _this.fadeSphere = Overlays.addOverlay("sphere", sphereProps); Script.clearInterval(fadeSphereInterval) Script.update.connect(_this.updateFadeSphere); if (USE_FADE_OUT === true) { @@ -184,7 +195,7 @@ function Teleporter() { if (currentFadeSphereOpacity <= 0) { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); - fadeSphereInterval = null; + fadeSphereInterval = null; print('sphere done fading out'); return; } From 11542aeca2b9b01b6e2d6fb9ec58abd8c41d1dcb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 7 Jul 2016 16:36:25 -0700 Subject: [PATCH 53/78] pick against avatar's capsule and then against the T-pose mesh --- interface/src/avatar/Avatar.cpp | 9 ++ interface/src/avatar/Avatar.h | 1 + interface/src/avatar/AvatarManager.cpp | 32 ++++-- libraries/render-utils/src/Model.cpp | 137 ++++--------------------- libraries/render-utils/src/Model.h | 5 - 5 files changed, 52 insertions(+), 132 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 39bb7eac17..4d9481f002 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1085,6 +1085,15 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } +void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { + ShapeInfo shapeInfo; + computeShapeInfo(shapeInfo); + glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight + start = getPosition() - glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); + end = getPosition() + glm::vec3(0, halfExtents.y, 0) + shapeInfo.getOffset(); + radius = halfExtents.x; +} + void Avatar::setMotionState(AvatarMotionState* motionState) { _motionState = motionState; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 064f0a9533..b9f44613c7 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -154,6 +154,7 @@ public: virtual void rebuildCollisionShape(); virtual void computeShapeInfo(ShapeInfo& shapeInfo); + void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); AvatarMotionState* getMotionState() { return _motionState; } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index a5a02bdaff..31a77df0cf 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -429,18 +429,34 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& glm::vec3 surfaceNormal; SkeletonModelPointer avatarModel = avatar->getSkeletonModel(); - AABox avatarBounds = avatarModel->getRenderableMeshBound(); - if (!avatarBounds.findRayIntersection(ray.origin, normDirection, distance, face, surfaceNormal)) { - // ray doesn't intersect avatar's bounding-box + + // It's better to intersect the ray against the avatar's actual mesh, but this is currently difficult to + // do, because the transformed mesh data only exists over in GPU-land. As a compromise, this code + // intersects against the avatars capsule and then against the (T-pose) mesh. The end effect is that picking + // against the avatar is sort-of right, but you likely wont be able to pick against the arms. + + // TODO -- find a way to extract transformed avatar mesh data from the rendering engine. + + // if we weren't picking against the capsule, we would want to pick against the avatarBounds... + // AABox avatarBounds = avatarModel->getRenderableMeshBound(); + // if (!avatarBounds.findRayIntersection(ray.origin, normDirection, distance, face, surfaceNormal)) { + // // ray doesn't intersect avatar's bounding-box + // continue; + // } + + glm::vec3 start; + glm::vec3 end; + float radius; + avatar->getCapsule(start, end, radius); + bool intersects = findRayCapsuleIntersection(ray.origin, normDirection, start, end, radius, distance); + if (!intersects) { + // ray doesn't intersect avatar's capsule continue; } - avatarModel->invalidCalculatedMeshBoxes(); - avatarModel->recalculateMeshBoxes(true); - QString extraInfo; - bool intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, - distance, face, surfaceNormal, extraInfo, true); + intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, + distance, face, surfaceNormal, extraInfo, true); if (intersects && (!result.intersects || distance < result.distance)) { result.intersects = true; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 4969585af4..43eced3107 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -442,31 +442,23 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { PROFILE_RANGE(__FUNCTION__); bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; - if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || - (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { + if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { const FBXGeometry& geometry = getFBXGeometry(); int numberOfMeshes = geometry.meshes.size(); _calculatedMeshBoxes.resize(numberOfMeshes); _calculatedMeshTriangles.clear(); _calculatedMeshTriangles.resize(numberOfMeshes); _calculatedMeshPartBoxes.clear(); - - - - int okCount = 0; - int notOkCount = 0; - - - for (int meshIndex = 0; meshIndex < numberOfMeshes; meshIndex++) { - const FBXMesh& mesh = geometry.meshes.at(meshIndex); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& mesh = geometry.meshes.at(i); Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation); - _calculatedMeshBoxes[meshIndex] = AABox(scaledMeshExtents); + _calculatedMeshBoxes[i] = AABox(scaledMeshExtents); if (pickAgainstTriangles) { QVector thisMeshTriangles; - for (int partIndex = 0; partIndex < mesh.parts.size(); partIndex++) { - const FBXMeshPart& part = mesh.parts.at(partIndex); + for (int j = 0; j < mesh.parts.size(); j++) { + const FBXMeshPart& part = mesh.parts.at(j); bool atLeastOnePointInBounds = false; AABox thisPartBounds; @@ -483,63 +475,11 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; - glm::vec3 v[3]; - int ok = 0; - if (meshIndex < _meshStates.size()) { - int quadPointIndexes[ 4 ] = {i0, i1, i2, i3}; - for (int pointInQuadIndex = 0; pointInQuadIndex < 4; pointInQuadIndex++) { - int vertexIndex = quadPointIndexes[pointInQuadIndex]; - glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; - glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; - - bool vSet = false; - for (int ci = 0; ci < 4; ci++) { - int clusterIndex = (int) clusterIndices[ci]; - float clusterWeight = clusterWeights[ci]; - if (clusterIndex < 0 || - clusterIndex >= mesh.clusters.size() || - clusterWeight == 0.0f) { - continue; - } - const FBXCluster& cluster = mesh.clusters.at(clusterIndex); - auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; - glm::vec3 meshVertex = mesh.vertices[vertexIndex]; - glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); - glm::vec3 tpoint = clusterWeight * (_translation + fuh); - v[pointInQuadIndex] += tpoint; - vSet = true; - } - if (vSet) { - ok++; - } - } - } - - - glm::vec3 v0; - glm::vec3 v1; - glm::vec3 v2; - glm::vec3 v3; - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); - if (ok == 4) { - okCount++; - v0 = v[0]; - v1 = v[1]; - v2 = v[2]; - v3 = v[3]; - } else { - notOkCount++; - v0 = calculateScaledOffsetPoint(mv0); - v1 = calculateScaledOffsetPoint(mv1); - v2 = calculateScaledOffsetPoint(mv2); - v3 = calculateScaledOffsetPoint(mv3); - } - // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); @@ -551,6 +491,11 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv2; thisPartBounds += mv3; + glm::vec3 v0 = calculateScaledOffsetPoint(mv0); + glm::vec3 v1 = calculateScaledOffsetPoint(mv1); + glm::vec3 v2 = calculateScaledOffsetPoint(mv2); + glm::vec3 v3 = calculateScaledOffsetPoint(mv3); + // Sam's recommended triangle slices Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; @@ -561,6 +506,7 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisMeshTriangles.push_back(tri1); thisMeshTriangles.push_back(tri2); + } } @@ -572,58 +518,10 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; - glm::vec3 v[3]; - int ok = 0; - if (meshIndex < _meshStates.size()) { - int trianglePointIndexes[ 3 ] = {i0, i1, i2}; - for (int pointInTriIndex = 0; pointInTriIndex < 3; pointInTriIndex++) { - int vertexIndex = trianglePointIndexes[pointInTriIndex]; - glm::vec4 clusterIndices = mesh.clusterIndices[vertexIndex]; - glm::vec4 clusterWeights = mesh.clusterWeights[vertexIndex]; - - bool vSet = false; - for (int ci = 0; ci < 4; ci++) { - int clusterIndex = (int) clusterIndices[ci]; - float clusterWeight = clusterWeights[ci]; - if (clusterIndex < 0 || - clusterIndex >= mesh.clusters.size() || - clusterWeight == 0.0f) { - continue; - } - const FBXCluster& cluster = mesh.clusters.at(clusterIndex); - auto clusterMatrix = _meshStates[meshIndex].clusterMatrices[cluster.jointIndex]; - glm::vec3 meshVertex = mesh.vertices[vertexIndex]; - glm::vec3 fuh = transformPoint(clusterMatrix, meshVertex); - glm::vec3 tpoint = clusterWeight * (_translation + fuh); - v[pointInTriIndex] += tpoint; - vSet = true; - } - if (vSet) { - ok++; - } - } - } - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 v0; - glm::vec3 v1; - glm::vec3 v2; - - if (ok == 3) { - okCount++; - v0 = v[0]; - v1 = v[1]; - v2 = v[2]; - } else { - notOkCount++; - v0 = calculateScaledOffsetPoint(mv0); - v1 = calculateScaledOffsetPoint(mv1); - v2 = calculateScaledOffsetPoint(mv2); - } - // track the mesh parts in model space if (!atLeastOnePointInBounds) { thisPartBounds.setBox(mv0, 0.0f); @@ -634,20 +532,21 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { thisPartBounds += mv1; thisPartBounds += mv2; + glm::vec3 v0 = calculateScaledOffsetPoint(mv0); + glm::vec3 v1 = calculateScaledOffsetPoint(mv1); + glm::vec3 v2 = calculateScaledOffsetPoint(mv2); + Triangle tri = { v0, v1, v2 }; thisMeshTriangles.push_back(tri); } } - _calculatedMeshPartBoxes[QPair(meshIndex, partIndex)] = thisPartBounds; + _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; } - _calculatedMeshTriangles[meshIndex] = thisMeshTriangles; + _calculatedMeshTriangles[i] = thisMeshTriangles; _calculatedMeshPartBoxesValid = true; } } - - qDebug() << "ok = " << okCount << " not-ok =" << notOkCount; - _calculatedMeshBoxesValid = true; _calculatedMeshTrianglesValid = pickAgainstTriangles; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 66c5eb019e..6a7c9ec560 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -316,14 +316,11 @@ protected: float getLimbLength(int jointIndex) const; /// Allow sub classes to force invalidating the bboxes -public: void invalidCalculatedMeshBoxes() { _calculatedMeshBoxesValid = false; _calculatedMeshPartBoxesValid = false; _calculatedMeshTrianglesValid = false; } -protected: - // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; @@ -360,9 +357,7 @@ protected: bool _calculatedMeshTrianglesValid; QMutex _mutex; -public: void recalculateMeshBoxes(bool pickAgainstTriangles = false); -protected: void segregateMeshGroups(); // used to calculate our list of translucent vs opaque meshes static model::MaterialPointer _collisionHullMaterial; From 7fa1dc7053845bc8e445cfe053a7ed5d38efd6f0 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 16:36:40 -0700 Subject: [PATCH 54/78] make thumb and trigger work in any order and on either button released --- scripts/system/controllers/teleport.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 57806a9b36..66eb876e6c 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -55,7 +55,7 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var NUMBER_OF_STEPS = 20; -var USE_THUMB_AND_TRIGGER_MODE = false; +var USE_THUMB_AND_TRIGGER_MODE = true; var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; var TARGET_MODEL_DIMENSIONS = { @@ -299,24 +299,16 @@ function Teleporter() { if (teleporter.teleportHand === 'left') { teleporter.leftRay(); - if (leftPad.buttonValue === 0) { - _this.exitTeleportMode(); - _this.deleteTargetOverlay(); - return; - } - if (leftTrigger.buttonValue === 0 && inTeleportMode === true) { + + if ((leftPad.buttonValue === 0 || leftTrigger.buttonValue === 0) && inTeleportMode === true) { _this.teleport(); return; } } else { teleporter.rightRay(); - if (rightPad.buttonValue === 0) { - _this.exitTeleportMode(); - _this.deleteTargetOverlay(); - return; - } - if (rightTrigger.buttonValue === 0 && inTeleportMode === true) { + + if ((rightPad.buttonValue === 0 || rightTrigger.buttonValue === 0) && inTeleportMode === true) { _this.teleport(); return; } @@ -694,6 +686,12 @@ function registerMappingsWithThumbAndTrigger() { teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { teleporter.enterTeleportMode('right') }); + teleportMapping.from(leftTrigger.down).when(leftPad.down).to(function(value) { + teleporter.enterTeleportMode('left') + }); + teleportMapping.from(rightTrigger.down).when(rightPad.down).to(function(value) { + teleporter.enterTeleportMode('right') + }); } if (USE_THUMB_AND_TRIGGER_MODE === true) { From f8ff0da9013515e8a1623e3ae09f4fbdae407ca9 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 19:22:42 -0700 Subject: [PATCH 55/78] trying to fix interaction with grab script... --- .../utilities/tools/overlayFinder.js | 17 ++++++++++ .../system/controllers/handControllerGrab.js | 32 ++++++++++++++++--- scripts/system/controllers/teleport.js | 14 +++++--- 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 scripts/developer/utilities/tools/overlayFinder.js diff --git a/scripts/developer/utilities/tools/overlayFinder.js b/scripts/developer/utilities/tools/overlayFinder.js new file mode 100644 index 0000000000..81ee687cbe --- /dev/null +++ b/scripts/developer/utilities/tools/overlayFinder.js @@ -0,0 +1,17 @@ +function mousePressEvent(event) { + var overlay = Overlays.getOverlayAtPoint({x:event.x,y:event.y}); + print('overlay is: ' + overlay) + // var pickRay = Camera.computePickRay(event.x, event.y); + + // var intersection = Overlays.findRayIntersection(pickRay); + // print('intersection is: ' + intersection) +}; + +Controller.mouseMoveEvent.connect(function(event){ + print('mouse press') + mousePressEvent(event) +}); + +Script.scriptEnding.connect(function(){ + Controller.mousePressEvent.disconnect(mousePressEvent); +}) \ No newline at end of file diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ecece8c4f7..2c73a7a109 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -367,7 +367,6 @@ function MyController(hand) { // for lights this.spotlight = null; this.pointlight = null; - this.overlayLine = null; this.searchSphere = null; this.waitForTriggerRelease = false; @@ -400,6 +399,7 @@ function MyController(hand) { this.updateSmoothedTrigger(); if (this.ignoreInput()) { + // print('in ignore input turn off') this.turnOffVisualizations(); return; } @@ -531,6 +531,7 @@ function MyController(hand) { }; this.overlayLineOn = function(closePoint, farPoint, color) { + if (this.overlayLine === null) { var lineProperties = { lineWidth: 5, @@ -543,6 +544,7 @@ function MyController(hand) { alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); + print('CREATED OVERLAY IT IS ' + this.overlayLine ) } else { Overlays.editOverlay(this.overlayLine, { @@ -555,7 +557,9 @@ function MyController(hand) { drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); + print('edited overlay line ' + this.overlayLine ) } + }; this.searchIndicatorOn = function(distantPickRay) { @@ -758,14 +762,20 @@ function MyController(hand) { }; this.overlayLineOff = function() { + return; if (this.overlayLine !== null) { - Overlays.deleteOverlay(this.overlayLine); + Overlays.deleteOverlay(this.overlayLine); + print('REMOVING OVERLAY LINE' + this.overlayLine) + this.overlayLine = null; } - this.overlayLine = null; + + // print('overlay shoudl be null and is line is ' + this.overlayLine) + }; this.searchSphereOff = function() { if (this.searchSphere !== null) { + print('removing search sphere' + this.searchSphere) Overlays.deleteOverlay(this.searchSphere); this.searchSphere = null; this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE; @@ -792,21 +802,28 @@ function MyController(hand) { } }; - this.turnOffVisualizations = function() { + this.turnOffVisualizations = function(hand) { + // print('TURN OFF VISUALIZATIONS: ' + hand) if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); + // print('after line off') } if (USE_OVERLAY_LINES_FOR_SEARCHING === true || USE_OVERLAY_LINES_FOR_MOVING === true) { this.overlayLineOff(); + // print('after overlay line off') } if (USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); + // print('after particle beam off') + } this.searchSphereOff(); restore2DMode(); + // print('after all turn off calls') + }; this.triggerPress = function(value) { @@ -2232,11 +2249,18 @@ var handleHandMessages = function(channel, message, sender) { if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { + leftController.turnOffVisualizations('left'); handToDisable = LEFT_HAND; } if (message === 'right') { + rightController.turnOffVisualizations('right'); handToDisable = RIGHT_HAND; } + if (message === "both") { + print('disable both') + leftController.turnOffVisualizations('left'); + rightController.turnOffVisualizations('right'); + } if (message === 'both' || message === 'none') { handToDisable = message; } diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 66eb876e6c..25721aaa55 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -102,6 +102,7 @@ function Teleporter() { this.updateConnected = null; this.smoothArrivalInterval = null; this.fadeSphere = null; + this.teleportHand = null; this.initialize = function() { this.createMappings(); @@ -265,7 +266,7 @@ function Teleporter() { } else { Script.update.disconnect(this.update); } - + this.teleportHand = null; this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); @@ -301,6 +302,7 @@ function Teleporter() { teleporter.leftRay(); if ((leftPad.buttonValue === 0 || leftTrigger.buttonValue === 0) && inTeleportMode === true) { + print('TELEPORTING LEFT') _this.teleport(); return; } @@ -309,6 +311,8 @@ function Teleporter() { teleporter.rightRay(); if ((rightPad.buttonValue === 0 || rightTrigger.buttonValue === 0) && inTeleportMode === true) { + + print('TELEPORTING RIGHT') _this.teleport(); return; } @@ -429,6 +433,7 @@ function Teleporter() { visible: true, alpha: 1, solid: true, + drawInFront: true, glow: 1.0 }; @@ -457,7 +462,8 @@ function Teleporter() { visible: true, alpha: 1, solid: true, - glow: 1.0 + glow: 1.0, + drawInFront: true }; this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); @@ -506,7 +512,7 @@ function Teleporter() { }; this.disableGrab = function() { - Messages.sendLocalMessage('Hifi-Hand-Disabler', 'both'); + Messages.sendLocalMessage('Hifi-Hand-Disabler', this.teleportHand); }; this.enableGrab = function() { @@ -514,7 +520,7 @@ function Teleporter() { }; this.triggerHaptics = function() { - var hand = this.hand === 'left' ? 0 : 1; + var hand = this.teleportHand === 'left' ? 0 : 1; var haptic = Controller.triggerShortHapticPulse(0.2, hand); }; From 1a463665579c5a438c15059e8a3586468050f3e8 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 7 Jul 2016 19:23:04 -0700 Subject: [PATCH 56/78] uncomment --- scripts/system/controllers/handControllerGrab.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2c73a7a109..6e7f5c98fa 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -762,7 +762,6 @@ function MyController(hand) { }; this.overlayLineOff = function() { - return; if (this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); print('REMOVING OVERLAY LINE' + this.overlayLine) From 24832df14a7d7ce2fc7c9324c5a2de4c51295194 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 13:13:32 -0700 Subject: [PATCH 57/78] changes --- .../system/controllers/handControllerGrab.js | 35 +++++++++++++------ scripts/system/controllers/teleport.js | 3 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 6e7f5c98fa..00e9b2c699 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -520,6 +520,8 @@ function MyController(hand) { visible: true }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); + print('CREATED SEARCH OVERLAY : '+ this.searchSphere) + } else { Overlays.editOverlay(this.searchSphere, { position: location, @@ -527,6 +529,7 @@ function MyController(hand) { color: color, visible: true }); + print('EDITED SEARCH OVERLAY : '+ this.searchSphere) } }; @@ -762,10 +765,10 @@ function MyController(hand) { }; this.overlayLineOff = function() { - if (this.overlayLine !== null) { + if (_this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); print('REMOVING OVERLAY LINE' + this.overlayLine) - this.overlayLine = null; + _this.overlayLine = null; } // print('overlay shoudl be null and is line is ' + this.overlayLine) @@ -933,6 +936,7 @@ function MyController(hand) { overlay: overlay, type: "hand" }); + print('ADDED HAND SPHERE OVERLAY : '+overlay) // add larger blue sphere around the palm. overlay = Overlays.addOverlay("sphere", { @@ -945,6 +949,8 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); + print('ADDED HAND SPHERE OVERLAY : '+overlay) + this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, @@ -973,6 +979,8 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); + print('ADDED GRAB BOX OVERLAY : '+ overlay) + _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, @@ -999,6 +1007,8 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); + print('ADDED SPHERE HOTSTPOT OVERLAY : '+ overlay) + _this.hotspotOverlays.push({ entityID: hotspot.entityID, overlay: overlay, @@ -1032,6 +1042,8 @@ function MyController(hand) { this.destroyHotspots = function() { this.hotspotOverlays.forEach(function(overlayInfo) { + print('deleting overlay hotspot ' + overlayInfo.overlay) + Overlays.deleteOverlay(overlayInfo.overlay); }); this.hotspotOverlays = []; @@ -2220,8 +2232,8 @@ mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress); -mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); -mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); +// mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); +// mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); Controller.enableMapping(MAPPING_NAME); @@ -2243,25 +2255,26 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); + var handleHandMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { - leftController.turnOffVisualizations('left'); - handToDisable = LEFT_HAND; + leftController.turnOffVisualizations('left'); + handToDisable = LEFT_HAND; } if (message === 'right') { - rightController.turnOffVisualizations('right'); - handToDisable = RIGHT_HAND; + rightController.turnOffVisualizations('right'); + handToDisable = RIGHT_HAND; } if (message === "both") { print('disable both') - leftController.turnOffVisualizations('left'); - rightController.turnOffVisualizations('right'); + leftController.turnOffVisualizations('left'); + rightController.turnOffVisualizations('right'); } if (message === 'both' || message === 'none') { - handToDisable = message; + // handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { try { diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 25721aaa55..ee11d0413e 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -447,7 +447,8 @@ function Teleporter() { color: color, visible: true, ignoreRayIntersection: true, // always ignore this - alpha: 1 + alpha: 1, + glow: 1.0 }); } }; From 1cda8afa403fdb1d3a922e2ba1f12c6b60e5b46a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 13:37:59 -0700 Subject: [PATCH 58/78] simplify code --- scripts/system/controllers/teleport.js | 92 +++++++------------------- 1 file changed, 23 insertions(+), 69 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index ee11d0413e..5f6c6078c1 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -133,7 +133,9 @@ function Teleporter() { }; this.enterTeleportMode = function(hand) { + print('entered teleport from ' + hand) if (inTeleportMode === true) { + print('already in teleport mode so dont enter again') return; } @@ -144,12 +146,9 @@ function Teleporter() { this.teleportHand = hand; this.initialize(); this.updateConnected = true; - if (USE_THUMB_AND_TRIGGER_MODE === true) { - Script.update.connect(this.updateForThumbAndTrigger); + Script.update.connect(this.update); + - } else { - Script.update.connect(this.update); - } }; this.findMidpoint = function(start, end) { @@ -197,14 +196,12 @@ function Teleporter() { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); fadeSphereInterval = null; - print('sphere done fading out'); return; } if (currentFadeSphereOpacity > 0) { currentFadeSphereOpacity = currentFadeSphereOpacity - 1; } - print('setting sphere alpha to: ' + currentFadeSphereOpacity) Overlays.editOverlay(_this.fadeSphere, { alpha: currentFadeSphereOpacity / 10 }) @@ -218,7 +215,6 @@ function Teleporter() { Script.clearInterval(fadeSphereInterval); _this.deleteFadeSphere(); fadeSphereInterval = null; - print('sphere done fading in') return; } if (currentFadeSphereOpacity < 1) { @@ -240,7 +236,6 @@ function Teleporter() { this.deleteFadeSphere = function() { if (_this.fadeSphere !== null) { - print('deleting fade sphere'); Script.update.disconnect(_this.updateFadeSphere); Overlays.deleteOverlay(_this.fadeSphere); _this.fadeSphere = null; @@ -260,44 +255,24 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - if (USE_THUMB_AND_TRIGGER_MODE === true) { - Script.update.disconnect(this.updateForThumbAndTrigger); + print('exiting teleport mode') - } else { - Script.update.disconnect(this.update); - } + Script.update.disconnect(this.update); this.teleportHand = null; this.updateConnected = null; this.disableMappings(); this.turnOffOverlayBeams(); this.enableGrab(); Script.setTimeout(function() { + print('fully exited teleport mode') inTeleportMode = false; }, 100); }; + this.update = function() { - if (teleporter.teleportHand === 'left') { - teleporter.leftRay(); - if (leftPad.buttonValue === 0) { - _this.teleport(); - return; - } - - } else { - teleporter.rightRay(); - if (rightPad.buttonValue === 0) { - _this.teleport(); - return; - } - } - - }; - - this.updateForThumbAndTrigger = function() { - if (teleporter.teleportHand === 'left') { teleporter.leftRay(); @@ -526,7 +501,7 @@ function Teleporter() { }; this.teleport = function(value) { - + print('teleporting : ' + value) if (this.intersection !== null) { if (USE_FADE_MODE === true) { this.createFadeSphere(); @@ -648,39 +623,10 @@ var mappingName, teleportMapping; var TELEPORT_DELAY = 100; + function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); - - teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); - teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - - teleportMapping.from(leftPad.down).to(function(value) { - print('left down' + value) - if (value === 1) { - - Script.setTimeout(function() { - teleporter.enterTeleportMode('left') - }, TELEPORT_DELAY) - } - - }); - teleportMapping.from(rightPad.down).to(function(value) { - print('right down' + value) - if (value === 1) { - - Script.setTimeout(function() { - teleporter.enterTeleportMode('right') - }, TELEPORT_DELAY) - } - }); - -} - - -function registerMappingsWithThumbAndTrigger() { - mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); - teleportMapping = Controller.newMapping(mappingName); teleportMapping.from(Controller.Standard.RT).peek().to(rightTrigger.buttonPress); teleportMapping.from(Controller.Standard.LT).peek().to(leftTrigger.buttonPress); @@ -688,24 +634,32 @@ function registerMappingsWithThumbAndTrigger() { teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function(value) { + print('tel 1') teleporter.enterTeleportMode('left') + return; }); teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { + print('tel 2') + teleporter.enterTeleportMode('right') + return; }); teleportMapping.from(leftTrigger.down).when(leftPad.down).to(function(value) { + print('tel 3') + teleporter.enterTeleportMode('left') + return; }); teleportMapping.from(rightTrigger.down).when(rightPad.down).to(function(value) { + print('tel 4') + teleporter.enterTeleportMode('right') + return; }); } -if (USE_THUMB_AND_TRIGGER_MODE === true) { - registerMappingsWithThumbAndTrigger(); -} else { - registerMappings(); -} + +registerMappings(); var teleporter = new Teleporter(); From 38e580b211908698f155dec7ab62035fbf6d6904 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 14:50:21 -0700 Subject: [PATCH 59/78] fix some bugs and also change it to use arrival mode --- scripts/system/controllers/teleport.js | 138 +++++++++---------------- 1 file changed, 49 insertions(+), 89 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5f6c6078c1..b30fa96023 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -8,8 +8,7 @@ //FEATURES: -// ENTRY MODES -// Thumbpad only +// ENTRY MODE // Thumpad + trigger // JUMP MODES @@ -28,15 +27,13 @@ var currentFadeSphereOpacity = 1; var fadeSphereInterval = null; var fadeSphereUpdateInterval = null; //milliseconds between fading one-tenth -- so this is a half second fade total -var USE_FADE_MODE = true; -var USE_FADE_IN = false; +var USE_FADE_MODE = false; var USE_FADE_OUT = true; -var FADE_IN_INTERVAL = 25; var FADE_OUT_INTERVAL = 25; // instant -var NUMBER_OF_STEPS = 0; -var SMOOTH_ARRIVAL_SPACING = 0; +// var NUMBER_OF_STEPS = 0; +// var SMOOTH_ARRIVAL_SPACING = 0; // // slow // var SMOOTH_ARRIVAL_SPACING = 150; @@ -46,17 +43,15 @@ var SMOOTH_ARRIVAL_SPACING = 0; // var SMOOTH_ARRIVAL_SPACING = 100; // var NUMBER_OF_STEPS = 4; -//medium-fast -// var SMOOTH_ARRIVAL_SPACING = 33; -// var NUMBER_OF_STEPS = 6; +// medium-fast +var SMOOTH_ARRIVAL_SPACING = 33; +var NUMBER_OF_STEPS = 6; //fast // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; -var USE_THUMB_AND_TRIGGER_MODE = true; - var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; var TARGET_MODEL_DIMENSIONS = { x: 1.15, @@ -67,15 +62,13 @@ var TARGET_MODEL_DIMENSIONS = { function ThumbPad(hand) { this.hand = hand; - var _this = this; + var _thisPad = this; this.buttonPress = function(value) { - _this.buttonValue = value; + _thisPad.buttonValue = value; }; - this.down = function() { - return _this.buttonValue === 1 ? 1.0 : 0.0; - }; + } function Trigger(hand) { @@ -88,14 +81,14 @@ function Trigger(hand) { }; this.down = function() { - return _this.buttonValue === 1 ? 1.0 : 0.0; + var down = _this.buttonValue === 1 ? 1.0 : 0.0; + return down }; } function Teleporter() { var _this = this; this.intersection = null; - this.targetProps = null; this.rightOverlayLine = null; this.leftOverlayLine = null; this.targetOverlay = null; @@ -133,32 +126,22 @@ function Teleporter() { }; this.enterTeleportMode = function(hand) { - print('entered teleport from ' + hand) if (inTeleportMode === true) { - print('already in teleport mode so dont enter again') return; } - + inTeleportMode = true; if (this.smoothArrivalInterval !== null) { Script.clearInterval(this.smoothArrivalInterval); } - inTeleportMode = true; + if (fadeSphereInterval !== null) { + Script.clearInterval(fadeSphereInterval); + } this.teleportHand = hand; this.initialize(); - this.updateConnected = true; Script.update.connect(this.update); - - + this.updateConnected = true; }; - this.findMidpoint = function(start, end) { - var xy = Vec3.sum(start, end); - var midpoint = Vec3.multiply(0.5, xy); - return midpoint - }; - - - this.createFadeSphere = function(avatarHead) { var sphereProps = { position: avatarHead, @@ -183,9 +166,7 @@ function Teleporter() { if (USE_FADE_OUT === true) { this.fadeSphereOut(); } - if (USE_FADE_IN === true) { - this.fadeSphereIn(); - } + }; @@ -209,23 +190,6 @@ function Teleporter() { }, FADE_OUT_INTERVAL); }; - this.fadeSphereIn = function() { - fadeSphereInterval = Script.setInterval(function() { - if (currentFadeSphereOpacity >= 1) { - Script.clearInterval(fadeSphereInterval); - _this.deleteFadeSphere(); - fadeSphereInterval = null; - return; - } - if (currentFadeSphereOpacity < 1) { - currentFadeSphereOpacity = currentFadeSphereOpacity - 1; - } - Overlays.editOverlay(_this.fadeSphere, { - alpha: currentFadeSphereOpacity / 10 - }) - - }, FADE_IN_INTERVAL); - }; this.updateFadeSphere = function() { var headPosition = MyAvatar.getHeadPosition(); @@ -255,16 +219,16 @@ function Teleporter() { } this.exitTeleportMode = function(value) { - print('exiting teleport mode') - - Script.update.disconnect(this.update); - this.teleportHand = null; - this.updateConnected = null; + if (this.updateConnected === true) { + Script.update.disconnect(this.update); + } this.disableMappings(); this.turnOffOverlayBeams(); this.enableGrab(); + + this.updateConnected = null; + Script.setTimeout(function() { - print('fully exited teleport mode') inTeleportMode = false; }, 100); }; @@ -277,7 +241,6 @@ function Teleporter() { teleporter.leftRay(); if ((leftPad.buttonValue === 0 || leftTrigger.buttonValue === 0) && inTeleportMode === true) { - print('TELEPORTING LEFT') _this.teleport(); return; } @@ -286,8 +249,6 @@ function Teleporter() { teleporter.rightRay(); if ((rightPad.buttonValue === 0 || rightTrigger.buttonValue === 0) && inTeleportMode === true) { - - print('TELEPORTING RIGHT') _this.teleport(); return; } @@ -337,12 +298,12 @@ function Teleporter() { } else { + this.deleteTargetOverlay(); this.rightLineOn(rightPickRay.origin, location, { red: 7, green: 36, blue: 44 }); - this.deleteTargetOverlay(); } } @@ -388,12 +349,13 @@ function Teleporter() { } else { + + this.deleteTargetOverlay(); this.leftLineOn(leftPickRay.origin, location, { red: 7, green: 36, blue: 44 }); - this.deleteTargetOverlay(); } }; @@ -501,7 +463,9 @@ function Teleporter() { }; this.teleport = function(value) { - print('teleporting : ' + value) + if (value === undefined) { + this.exitTeleportMode(); + } if (this.intersection !== null) { if (USE_FADE_MODE === true) { this.createFadeSphere(); @@ -516,6 +480,15 @@ function Teleporter() { }; + + 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 = []; @@ -540,23 +513,17 @@ function Teleporter() { this.smoothArrival = function() { _this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection); - print('ARRIVAL POINTS: ' + JSON.stringify(_this.arrivalPoints)); - print('end point: ' + JSON.stringify(_this.intersection.intersection)) _this.smoothArrivalInterval = Script.setInterval(function() { - print(_this.arrivalPoints.length + " arrival points remaining") if (_this.arrivalPoints.length === 0) { Script.clearInterval(_this.smoothArrivalInterval); return; } var landingPoint = _this.arrivalPoints.shift(); - print('landing at: ' + JSON.stringify(landingPoint)) MyAvatar.position = landingPoint; if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { - print('clear target overlay') _this.deleteTargetOverlay(); - _this.triggerHaptics(); } @@ -633,29 +600,28 @@ function registerMappings() { teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightPad.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); - teleportMapping.from(leftPad.down).when(leftTrigger.down).to(function(value) { + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).when(leftTrigger.down).to(function(value) { print('tel 1') teleporter.enterTeleportMode('left') return; }); - teleportMapping.from(rightPad.down).when(rightTrigger.down).to(function(value) { + teleportMapping.from(Controller.Standard.RightPrimaryThumb).when(rightTrigger.down).to(function(value) { print('tel 2') - teleporter.enterTeleportMode('right') return; }); - teleportMapping.from(leftTrigger.down).when(leftPad.down).to(function(value) { + teleportMapping.from(Controller.Standard.RT).when(Controller.Standard.RightPrimaryThumb).to(function(value) { print('tel 3') - + teleporter.enterTeleportMode('right') + return; + }); + teleportMapping.from(Controller.Standard.LT).when(Controller.Standard.LeftPrimaryThumb).to(function(value) { + print('tel 4') teleporter.enterTeleportMode('left') return; }); - teleportMapping.from(rightTrigger.down).when(rightPad.down).to(function(value) { - print('tel 4') - teleporter.enterTeleportMode('right') - return; - }); + } @@ -674,12 +640,6 @@ function cleanup() { teleporter.turnOffOverlayBeams(); teleporter.deleteFadeSphere(); if (teleporter.updateConnected !== null) { - - if (USE_THUMB_AND_TRIGGER_MODE === true) { - Script.update.disconnect(teleporter.updateForThumbAndTrigger); - - } else { - Script.update.disconnect(teleporter.update); - } + Script.update.disconnect(teleporter.update); } } \ No newline at end of file From 3492367c63e27c544e900b043b159399d3b9c02e Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:03:59 -0700 Subject: [PATCH 60/78] cleanup comments etc --- scripts/system/controllers/teleport.js | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b30fa96023..8426780a7a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,24 +1,11 @@ // Created by james b. pollack @imgntn on 7/2/2016 // Copyright 2016 High Fidelity, Inc. // -// Creates a beam and target and then teleports you there when you let go of the activation button. +// Creates a beam and target and then teleports you there when you let go of either activation button. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -//FEATURES: - -// ENTRY MODE -// Thumpad + trigger - -// JUMP MODES -// Instant -// Smoth arrival aka stepwise (number of steps, duration of step) - -// FADE MODE -// Cube-overlay (steps,duration) -// Model-overlay (steps, duration) -// Fade out / fade in // defaults with instant jump with fade. to try smooth arrival mode, change use fade to false and then switch the number of arrival steps and spacing var inTeleportMode = false; @@ -68,7 +55,6 @@ function ThumbPad(hand) { _thisPad.buttonValue = value; }; - } function Trigger(hand) { @@ -114,7 +100,6 @@ function Teleporter() { }; this.createMappings = function() { - // peek at the trigger and thumbs to store their values teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); @@ -360,7 +345,6 @@ function Teleporter() { }; this.rightLineOn = function(closePoint, farPoint, color) { - // draw a line if (this.rightOverlayLine === null) { var lineProperties = { start: closePoint, @@ -472,7 +456,6 @@ function Teleporter() { } var offset = getAvatarFootOffset(); this.intersection.intersection.y += offset; - // MyAvatar.position = _this.intersection.intersection; this.exitTeleportMode(); this.smoothArrival(); @@ -621,10 +604,8 @@ function registerMappings() { return; }); - } - registerMappings(); var teleporter = new Teleporter(); From 5ca14b528bf7e5260e819517b0c24468e83d0da2 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:08:44 -0700 Subject: [PATCH 61/78] remove prints and format --- scripts/defaultScripts.js | 1 + .../{teleportDestination.fbx => teleport.fbx} | Bin 188700 -> 189244 bytes scripts/system/assets/models/teleportBeam.fbx | Bin 40172 -> 0 bytes .../system/controllers/handControllerGrab.js | 221 ++++++++++++------ scripts/system/controllers/teleport.js | 6 +- 5 files changed, 147 insertions(+), 81 deletions(-) rename scripts/system/assets/models/{teleportDestination.fbx => teleport.fbx} (94%) delete mode 100644 scripts/system/assets/models/teleportBeam.fbx diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2bf908ab57..711e64f938 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -23,5 +23,6 @@ Script.load("system/controllers/handControllerGrab.js"); Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); +Script.load("system/controllers/teleport.js"); Script.load("system/dialTone.js"); Script.load("system/firstPersonHMD.js"); diff --git a/scripts/system/assets/models/teleportDestination.fbx b/scripts/system/assets/models/teleport.fbx similarity index 94% rename from scripts/system/assets/models/teleportDestination.fbx rename to scripts/system/assets/models/teleport.fbx index c850982f563417e960d9079dbc0da29408ed01c9..831f152add043dcdb04c4a567037167ba434eb97 100644 GIT binary patch delta 4084 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 diff --git a/scripts/system/assets/models/teleportBeam.fbx b/scripts/system/assets/models/teleportBeam.fbx deleted file mode 100644 index 21b63d5843ac14a12ea7601d1bf8475c696cac93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40172 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 bNQV9Ysr(37w%}RY!{p|7QjO6owI%;QsNJ!J diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 00e9b2c699..a2738396b6 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -49,8 +49,8 @@ var DROP_WITHOUT_SHAKE = false; var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position -var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects var NO_INTERSECT_COLOR = { @@ -81,7 +81,7 @@ var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. -var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. @@ -251,13 +251,15 @@ function propsArePhysical(props) { // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; + function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, // because other scripts (such as handControllerPointer) might be using some other visualization // instead of setting Reticle.visible. return (EXTERNALLY_MANAGED_2D_MINOR_MODE && - (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); + (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); } + function restore2DMode() { if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { Reticle.setVisible(true); @@ -399,7 +401,6 @@ function MyController(hand) { this.updateSmoothedTrigger(); if (this.ignoreInput()) { - // print('in ignore input turn off') this.turnOffVisualizations(); return; } @@ -520,7 +521,7 @@ function MyController(hand) { visible: true }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); - print('CREATED SEARCH OVERLAY : '+ this.searchSphere) + } else { Overlays.editOverlay(this.searchSphere, { @@ -529,7 +530,7 @@ function MyController(hand) { color: color, visible: true }); - print('EDITED SEARCH OVERLAY : '+ this.searchSphere) + } }; @@ -547,7 +548,6 @@ function MyController(hand) { alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); - print('CREATED OVERLAY IT IS ' + this.overlayLine ) } else { Overlays.editOverlay(this.overlayLine, { @@ -560,9 +560,8 @@ function MyController(hand) { drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); - print('edited overlay line ' + this.overlayLine ) } - + }; this.searchIndicatorOn = function(distantPickRay) { @@ -577,12 +576,12 @@ function MyController(hand) { } var searchSphereLocation = Vec3.sum(distantPickRay.origin, - Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) { this.overlayLineOn(handPosition, searchSphereLocation, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } }; @@ -766,13 +765,9 @@ function MyController(hand) { this.overlayLineOff = function() { if (_this.overlayLine !== null) { - Overlays.deleteOverlay(this.overlayLine); - print('REMOVING OVERLAY LINE' + this.overlayLine) - _this.overlayLine = null; + Overlays.deleteOverlay(this.overlayLine); + _this.overlayLine = null; } - - // print('overlay shoudl be null and is line is ' + this.overlayLine) - }; this.searchSphereOff = function() { @@ -805,27 +800,21 @@ function MyController(hand) { }; this.turnOffVisualizations = function(hand) { - // print('TURN OFF VISUALIZATIONS: ' + hand) if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); - // print('after line off') } if (USE_OVERLAY_LINES_FOR_SEARCHING === true || USE_OVERLAY_LINES_FOR_MOVING === true) { this.overlayLineOff(); - // print('after overlay line off') } if (USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); - // print('after particle beam off') } this.searchSphereOff(); restore2DMode(); - // print('after all turn off calls') - }; this.triggerPress = function(value) { @@ -901,18 +890,34 @@ function MyController(hand) { this.createHotspots = function() { var _this = this; - var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_EQUIP_SPHERE_COLOR = { + red: 90, + green: 255, + blue: 90 + }; var HAND_EQUIP_SPHERE_ALPHA = 0.7; var HAND_EQUIP_SPHERE_RADIUS = 0.01; - var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var HAND_GRAB_SPHERE_COLOR = { + red: 90, + green: 90, + blue: 255 + }; var HAND_GRAB_SPHERE_ALPHA = 0.3; var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var EQUIP_SPHERE_COLOR = { + red: 90, + green: 255, + blue: 90 + }; var EQUIP_SPHERE_ALPHA = 0.3; - var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; + var GRAB_BOX_COLOR = { + red: 90, + green: 90, + blue: 255 + }; var GRAB_BOX_ALPHA = 0.1; this.hotspotOverlays = []; @@ -936,7 +941,6 @@ function MyController(hand) { overlay: overlay, type: "hand" }); - print('ADDED HAND SPHERE OVERLAY : '+overlay) // add larger blue sphere around the palm. overlay = Overlays.addOverlay("sphere", { @@ -949,13 +953,16 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - print('ADDED HAND SPHERE OVERLAY : '+overlay) this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, type: "hand", - localPosition: {x: 0, y: 0, z: 0} + localPosition: { + x: 0, + y: 0, + z: 0 + } }); } @@ -979,13 +986,16 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - print('ADDED GRAB BOX OVERLAY : '+ overlay) _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, type: "near", - localPosition: {x: 0, y: 0, z: 0} + localPosition: { + x: 0, + y: 0, + z: 0 + } }); } }); @@ -1007,7 +1017,6 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - print('ADDED SPHERE HOTSTPOT OVERLAY : '+ overlay) _this.hotspotOverlays.push({ entityID: hotspot.entityID, @@ -1023,7 +1032,9 @@ function MyController(hand) { var props; this.hotspotOverlays.forEach(function(overlayInfo) { if (overlayInfo.type === "hand") { - Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); + Overlays.editOverlay(overlayInfo.overlay, { + position: _this.getHandPosition() + }); } else if (overlayInfo.type === "equip") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); @@ -1035,15 +1046,16 @@ function MyController(hand) { } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + Overlays.editOverlay(overlayInfo.overlay, { + position: props.position, + rotation: props.rotation + }); } }); }; this.destroyHotspots = function() { this.hotspotOverlays.forEach(function(overlayInfo) { - print('deleting overlay hotspot ' + overlayInfo.overlay) - Overlays.deleteOverlay(overlayInfo.overlay); }); this.hotspotOverlays = []; @@ -1071,8 +1083,8 @@ function MyController(hand) { var pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; @@ -1157,7 +1169,11 @@ function MyController(hand) { if (wearableProps && wearableProps.joints) { result.push({ entityID: entityID, - localPosition: {x: 0, y: 0, z: 0}, + localPosition: { + x: 0, + y: 0, + z: 0 + }, worldPosition: entityXform.pos, radius: EQUIP_RADIUS, joints: wearableProps.joints @@ -1174,8 +1190,8 @@ function MyController(hand) { var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedEntity == hotspot.entityID); + this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && + this.getOtherHandController().grabbedEntity == hotspot.entityID); if (refCount > 0 && !okToEquipFromOtherHand) { if (debug) { print("equip is skipping '" + props.name + "': grabbed by someone else"); @@ -1423,8 +1439,8 @@ function MyController(hand) { // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { this.lineOn(rayPickInfo.searchRay.origin, - Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), - NO_INTERSECT_COLOR); + Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), + NO_INTERSECT_COLOR); } this.searchIndicatorOn(rayPickInfo.searchRay); @@ -1515,7 +1531,7 @@ function MyController(hand) { // controller pose is in avatar frame var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); + Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); @@ -1537,7 +1553,7 @@ function MyController(hand) { // scale delta controller hand movement by radius. var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), - radius); + radius); /// double delta controller rotation // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did @@ -1562,7 +1578,7 @@ function MyController(hand) { var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, - Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); + Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + @@ -1571,7 +1587,7 @@ function MyController(hand) { var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1689,16 +1705,28 @@ function MyController(hand) { if (this.fastHandMoveTimer < 0) { this.fastHandMoveDetected = false; } - var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_REST_TIME = 1; // sec var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { this.fastHandMoveDetected = true; this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; } - var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; + var localHandUpAxis = this.hand === RIGHT_HAND ? { + x: 1, + y: 0, + z: 0 + } : { + x: -1, + y: 0, + z: 0 + }; var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); - var DOWN = {x: 0, y: -1, z: 0}; + var DOWN = { + x: 0, + y: -1, + z: 0 + }; var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); var handIsUpsideDown = false; @@ -1800,8 +1828,16 @@ function MyController(hand) { } Entities.editEntity(this.grabbedEntity, { - velocity: {x: 0, y: 0, z: 0}, - angularVelocity: {x: 0, y: 0, z: 0}, + velocity: { + x: 0, + y: 0, + z: 0 + }, + angularVelocity: { + x: 0, + y: 0, + z: 0 + }, dynamic: false }); @@ -1868,7 +1904,7 @@ function MyController(hand) { if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + - props.parentID + " " + vec3toStr(props.position)); + props.parentID + " " + vec3toStr(props.position)); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); @@ -2110,7 +2146,9 @@ function MyController(hand) { // people are holding something and one of them will be able (if the other releases at the right time) to // bootstrap themselves with the held object. This happens because the meaning of "otherAvatar" in // the collision mask hinges on who the physics simulation owner is. - Entities.editEntity(entityID, {"collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED}); + Entities.editEntity(entityID, { + "collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED + }); } } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); @@ -2124,7 +2162,9 @@ function MyController(hand) { var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); children.forEach(function(childID) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); - Entities.editEntity(childID, {parentID: NULL_UUID}); + Entities.editEntity(childID, { + parentID: NULL_UUID + }); }); }; @@ -2170,12 +2210,24 @@ function MyController(hand) { data["dynamic"] && data["parentID"] == NULL_UUID && !data["collisionless"]) { - deactiveProps["velocity"] = {x: 0.0, y: 0.1, z: 0.0}; + deactiveProps["velocity"] = { + x: 0.0, + y: 0.1, + z: 0.0 + }; doSetVelocity = false; } if (noVelocity) { - deactiveProps["velocity"] = {x: 0.0, y: 0.0, z: 0.0}; - deactiveProps["angularVelocity"] = {x: 0.0, y: 0.0, z: 0.0}; + deactiveProps["velocity"] = { + x: 0.0, + y: 0.0, + z: 0.0 + }; + deactiveProps["angularVelocity"] = { + x: 0.0, + y: 0.0, + z: 0.0 + }; doSetVelocity = false; } @@ -2188,7 +2240,7 @@ function MyController(hand) { // be fixed. Entities.editEntity(entityID, { velocity: this.currentVelocity - // angularVelocity: this.currentAngularVelocity + // angularVelocity: this.currentAngularVelocity }); } @@ -2198,14 +2250,32 @@ function MyController(hand) { deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, - velocity: {x: 0.0, y: 0.0, z: 0.0}, - angularVelocity: {x: 0.0, y: 0.0, z: 0.0} + velocity: { + x: 0.0, + y: 0.0, + z: 0.0 + }, + angularVelocity: { + x: 0.0, + y: 0.0, + z: 0.0 + } }; Entities.editEntity(entityID, deactiveProps); } else if (noVelocity) { - Entities.editEntity(entityID, {velocity: {x: 0.0, y: 0.0, z: 0.0}, - angularVelocity: {x: 0.0, y: 0.0, z: 0.0}, - dynamic: data["dynamic"]}); + Entities.editEntity(entityID, { + velocity: { + x: 0.0, + y: 0.0, + z: 0.0 + }, + angularVelocity: { + x: 0.0, + y: 0.0, + z: 0.0 + }, + dynamic: data["dynamic"] + }); } } else { data = null; @@ -2261,20 +2331,20 @@ var handleHandMessages = function(channel, message, sender) { if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { - leftController.turnOffVisualizations('left'); - handToDisable = LEFT_HAND; + leftController.turnOffVisualizations('left'); + handToDisable = LEFT_HAND; } if (message === 'right') { - rightController.turnOffVisualizations('right'); - handToDisable = RIGHT_HAND; + rightController.turnOffVisualizations('right'); + handToDisable = RIGHT_HAND; } if (message === "both") { print('disable both') - leftController.turnOffVisualizations('left'); - rightController.turnOffVisualizations('right'); + leftController.turnOffVisualizations('left'); + rightController.turnOffVisualizations('right'); } if (message === 'both' || message === 'none') { - // handToDisable = message; + // handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { try { @@ -2352,5 +2422,4 @@ function handleMenuItemEvent(menuItem) { } } -Menu.menuItemEvent.connect(handleMenuItemEvent); - +Menu.menuItemEvent.connect(handleMenuItemEvent); \ No newline at end of file diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 8426780a7a..b92a85aaf4 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -39,7 +39,7 @@ var NUMBER_OF_STEPS = 6; // var NUMBER_OF_STEPS = 20; -var TARGET_MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/teleporter/target.fbx'; +var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport.fbx"); var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, @@ -584,22 +584,18 @@ function registerMappings() { teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftPad.buttonPress); teleportMapping.from(Controller.Standard.LeftPrimaryThumb).when(leftTrigger.down).to(function(value) { - print('tel 1') teleporter.enterTeleportMode('left') return; }); teleportMapping.from(Controller.Standard.RightPrimaryThumb).when(rightTrigger.down).to(function(value) { - print('tel 2') teleporter.enterTeleportMode('right') return; }); teleportMapping.from(Controller.Standard.RT).when(Controller.Standard.RightPrimaryThumb).to(function(value) { - print('tel 3') teleporter.enterTeleportMode('right') return; }); teleportMapping.from(Controller.Standard.LT).when(Controller.Standard.LeftPrimaryThumb).to(function(value) { - print('tel 4') teleporter.enterTeleportMode('left') return; }); From 4f899d875288e0b5e9b3dd8af4b4e14c8e242e3c Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:14:04 -0700 Subject: [PATCH 62/78] play nice with grab --- scripts/system/controllers/handControllerGrab.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index a2738396b6..a7a897aa7f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -772,7 +772,6 @@ function MyController(hand) { this.searchSphereOff = function() { if (this.searchSphere !== null) { - print('removing search sphere' + this.searchSphere) Overlays.deleteOverlay(this.searchSphere); this.searchSphere = null; this.searchSphereDistance = DEFAULT_SEARCH_SPHERE_DISTANCE; @@ -2302,8 +2301,8 @@ mapping.from([Controller.Standard.LB]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.LeftGrip]).peek().to(leftController.secondaryPress); mapping.from([Controller.Standard.RightGrip]).peek().to(rightController.secondaryPress); -// mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); -// mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); +mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress); +mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress); Controller.enableMapping(MAPPING_NAME); @@ -2339,12 +2338,11 @@ var handleHandMessages = function(channel, message, sender) { handToDisable = RIGHT_HAND; } if (message === "both") { - print('disable both') leftController.turnOffVisualizations('left'); rightController.turnOffVisualizations('right'); } if (message === 'both' || message === 'none') { - // handToDisable = message; + handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { try { From ee970c5e07a6c5ace193951a6f6f69ceb52df53a Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 15:16:26 -0700 Subject: [PATCH 63/78] cleanup --- scripts/system/controllers/teleport.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b92a85aaf4..5c881c5fcc 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -6,8 +6,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -// defaults with instant jump with fade. to try smooth arrival mode, change use fade to false and then switch the number of arrival steps and spacing var inTeleportMode = false; var currentFadeSphereOpacity = 1; @@ -567,8 +565,6 @@ var rightPad = new ThumbPad('right'); var leftTrigger = new Trigger('left'); var rightTrigger = new Trigger('right'); -//create a controller mapping and make sure to disable it when the script is stopped - var mappingName, teleportMapping; var TELEPORT_DELAY = 100; From 6249f3ed12ea4c67238ca53529bb0d480312773d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 8 Jul 2016 16:01:17 -0700 Subject: [PATCH 64/78] less changes to hand grab --- .../system/controllers/handControllerGrab.js | 238 ++++++------------ scripts/system/controllers/teleport.js | 3 +- 2 files changed, 77 insertions(+), 164 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index a7a897aa7f..81bd9cba2e 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -49,8 +49,8 @@ var DROP_WITHOUT_SHAKE = false; var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position -var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified +var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects var NO_INTERSECT_COLOR = { @@ -81,7 +81,7 @@ var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. -var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. @@ -221,6 +221,14 @@ function entityHasActions(entityID) { return Entities.getActionIDs(entityID).length > 0; } +function findRayIntersection(pickRay, precise, include, exclude) { + var entities = Entities.findRayIntersection(pickRay, precise, include, exclude); + var overlays = Overlays.findRayIntersection(pickRay); + if (!overlays.intersects || (entities.intersects && (entities.distance <= overlays.distance))) { + return entities; + } + return overlays; +} function entityIsGrabbedByOther(entityID) { // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. var actionIDs = Entities.getActionIDs(entityID); @@ -251,15 +259,17 @@ function propsArePhysical(props) { // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; - +var EDIT_SETTING = "io.highfidelity.isEditting"; +function isEditing() { + return EXTERNALLY_MANAGED_2D_MINOR_MODE && Settings.getValue(EDIT_SETTING); +} function isIn2DMode() { // In this version, we make our own determination of whether we're aimed a HUD element, // because other scripts (such as handControllerPointer) might be using some other visualization // instead of setting Reticle.visible. return (EXTERNALLY_MANAGED_2D_MINOR_MODE && - (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); + (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position))); } - function restore2DMode() { if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { Reticle.setVisible(true); @@ -369,6 +379,7 @@ function MyController(hand) { // for lights this.spotlight = null; this.pointlight = null; + this.overlayLine = null; this.searchSphere = null; this.waitForTriggerRelease = false; @@ -521,8 +532,6 @@ function MyController(hand) { visible: true }; this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); - - } else { Overlays.editOverlay(this.searchSphere, { position: location, @@ -530,12 +539,10 @@ function MyController(hand) { color: color, visible: true }); - } }; this.overlayLineOn = function(closePoint, farPoint, color) { - if (this.overlayLine === null) { var lineProperties = { lineWidth: 5, @@ -561,7 +568,6 @@ function MyController(hand) { alpha: 1 }); } - }; this.searchIndicatorOn = function(distantPickRay) { @@ -576,12 +582,12 @@ function MyController(hand) { } var searchSphereLocation = Vec3.sum(distantPickRay.origin, - Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); if ((USE_OVERLAY_LINES_FOR_SEARCHING === true) && PICK_WITH_HAND_RAY) { this.overlayLineOn(handPosition, searchSphereLocation, - (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); + (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? INTERSECT_COLOR : NO_INTERSECT_COLOR); } }; @@ -764,10 +770,10 @@ function MyController(hand) { }; this.overlayLineOff = function() { - if (_this.overlayLine !== null) { + if (this.overlayLine !== null) { Overlays.deleteOverlay(this.overlayLine); - _this.overlayLine = null; } + this.overlayLine = null; }; this.searchSphereOff = function() { @@ -798,7 +804,7 @@ function MyController(hand) { } }; - this.turnOffVisualizations = function(hand) { + this.turnOffVisualizations = function() { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); } @@ -809,7 +815,6 @@ function MyController(hand) { if (USE_PARTICLE_BEAM_FOR_MOVING === true) { this.particleBeamOff(); - } this.searchSphereOff(); restore2DMode(); @@ -889,34 +894,18 @@ function MyController(hand) { this.createHotspots = function() { var _this = this; - var HAND_EQUIP_SPHERE_COLOR = { - red: 90, - green: 255, - blue: 90 - }; + var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var HAND_EQUIP_SPHERE_ALPHA = 0.7; var HAND_EQUIP_SPHERE_RADIUS = 0.01; - var HAND_GRAB_SPHERE_COLOR = { - red: 90, - green: 90, - blue: 255 - }; + var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; var HAND_GRAB_SPHERE_ALPHA = 0.3; var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; - var EQUIP_SPHERE_COLOR = { - red: 90, - green: 255, - blue: 90 - }; + var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; var EQUIP_SPHERE_ALPHA = 0.3; - var GRAB_BOX_COLOR = { - red: 90, - green: 90, - blue: 255 - }; + var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; var GRAB_BOX_ALPHA = 0.1; this.hotspotOverlays = []; @@ -952,16 +941,11 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - this.hotspotOverlays.push({ entityID: undefined, overlay: overlay, type: "hand", - localPosition: { - x: 0, - y: 0, - z: 0 - } + localPosition: {x: 0, y: 0, z: 0} }); } @@ -985,16 +969,11 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - _this.hotspotOverlays.push({ entityID: entityID, overlay: overlay, type: "near", - localPosition: { - x: 0, - y: 0, - z: 0 - } + localPosition: {x: 0, y: 0, z: 0} }); } }); @@ -1016,7 +995,6 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false }); - _this.hotspotOverlays.push({ entityID: hotspot.entityID, overlay: overlay, @@ -1031,9 +1009,7 @@ function MyController(hand) { var props; this.hotspotOverlays.forEach(function(overlayInfo) { if (overlayInfo.type === "hand") { - Overlays.editOverlay(overlayInfo.overlay, { - position: _this.getHandPosition() - }); + Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); } else if (overlayInfo.type === "equip") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); @@ -1045,10 +1021,7 @@ function MyController(hand) { } else if (overlayInfo.type === "near") { _this.entityPropertyCache.updateEntity(overlayInfo.entityID); props = _this.entityPropertyCache.getProps(overlayInfo.entityID); - Overlays.editOverlay(overlayInfo.overlay, { - position: props.position, - rotation: props.rotation - }); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); } }); }; @@ -1082,8 +1055,8 @@ function MyController(hand) { var pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), length: PICK_MAX_DISTANCE }; @@ -1108,20 +1081,15 @@ function MyController(hand) { var intersection; if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + intersection = findRayIntersection(pickRayBacked, true, [], blacklist); } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - - var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); - if (!intersection.intersects || - (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { - intersection = overlayIntersection; + intersection = findRayIntersection(pickRayBacked, true); } if (intersection.intersects) { return { entityID: intersection.entityID, + overlayID: intersection.overlayID, searchRay: pickRay, distance: Vec3.distance(pickRay.origin, intersection.intersection) }; @@ -1168,11 +1136,7 @@ function MyController(hand) { if (wearableProps && wearableProps.joints) { result.push({ entityID: entityID, - localPosition: { - x: 0, - y: 0, - z: 0 - }, + localPosition: {x: 0, y: 0, z: 0}, worldPosition: entityXform.pos, radius: EQUIP_RADIUS, joints: wearableProps.joints @@ -1189,8 +1153,8 @@ function MyController(hand) { var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedEntity == hotspot.entityID); + this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && + this.getOtherHandController().grabbedEntity == hotspot.entityID); if (refCount > 0 && !okToEquipFromOtherHand) { if (debug) { print("equip is skipping '" + props.name + "': grabbed by someone else"); @@ -1369,6 +1333,8 @@ function MyController(hand) { if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { grabbableEntities.push(rayPickInfo.entityID); } + } else if (rayPickInfo.overlayID) { + this.intersectionDistance = rayPickInfo.distance; } else { this.intersectionDistance = 0; } @@ -1425,7 +1391,7 @@ function MyController(hand) { // TODO: highlight the far-triggerable object? } } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { - if (this.triggerSmoothedGrab()) { + if (this.triggerSmoothedGrab() && !isEditing()) { this.grabbedEntity = entity; this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); return; @@ -1438,8 +1404,8 @@ function MyController(hand) { // search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { this.lineOn(rayPickInfo.searchRay.origin, - Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), - NO_INTERSECT_COLOR); + Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), + NO_INTERSECT_COLOR); } this.searchIndicatorOn(rayPickInfo.searchRay); @@ -1530,7 +1496,7 @@ function MyController(hand) { // controller pose is in avatar frame var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); + Controller.Standard.RightHand : Controller.Standard.LeftHand); // transform it into world frame var controllerPositionVSAvatar = Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation); @@ -1552,7 +1518,7 @@ function MyController(hand) { // scale delta controller hand movement by radius. var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), - radius); + radius); /// double delta controller rotation // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did @@ -1577,7 +1543,7 @@ function MyController(hand) { var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, - Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); + Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + @@ -1586,7 +1552,7 @@ function MyController(hand) { var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1704,28 +1670,16 @@ function MyController(hand) { if (this.fastHandMoveTimer < 0) { this.fastHandMoveDetected = false; } - var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_REST_TIME = 1; // sec var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { this.fastHandMoveDetected = true; this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; } - var localHandUpAxis = this.hand === RIGHT_HAND ? { - x: 1, - y: 0, - z: 0 - } : { - x: -1, - y: 0, - z: 0 - }; + var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); - var DOWN = { - x: 0, - y: -1, - z: 0 - }; + var DOWN = {x: 0, y: -1, z: 0}; var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); var handIsUpsideDown = false; @@ -1827,16 +1781,8 @@ function MyController(hand) { } Entities.editEntity(this.grabbedEntity, { - velocity: { - x: 0, - y: 0, - z: 0 - }, - angularVelocity: { - x: 0, - y: 0, - z: 0 - }, + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0}, dynamic: false }); @@ -1903,7 +1849,7 @@ function MyController(hand) { if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + - props.parentID + " " + vec3toStr(props.position)); + props.parentID + " " + vec3toStr(props.position)); if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); @@ -2005,8 +1951,8 @@ function MyController(hand) { var now = Date.now(); if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - var intersection = Entities.findRayIntersection(pickRay, true); - if (intersection.accurate) { + var intersection = findRayIntersection(pickRay, true); + if (intersection.accurate || intersection.overlayID) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { this.callEntityMethodOnGrabbed("stopFarTrigger"); @@ -2145,9 +2091,7 @@ function MyController(hand) { // people are holding something and one of them will be able (if the other releases at the right time) to // bootstrap themselves with the held object. This happens because the meaning of "otherAvatar" in // the collision mask hinges on who the physics simulation owner is. - Entities.editEntity(entityID, { - "collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED - }); + Entities.editEntity(entityID, {"collidesWith": COLLIDES_WITH_WHILE_MULTI_GRABBED}); } } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); @@ -2161,9 +2105,7 @@ function MyController(hand) { var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); children.forEach(function(childID) { print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); - Entities.editEntity(childID, { - parentID: NULL_UUID - }); + Entities.editEntity(childID, {parentID: NULL_UUID}); }); }; @@ -2209,24 +2151,12 @@ function MyController(hand) { data["dynamic"] && data["parentID"] == NULL_UUID && !data["collisionless"]) { - deactiveProps["velocity"] = { - x: 0.0, - y: 0.1, - z: 0.0 - }; + deactiveProps["velocity"] = {x: 0.0, y: 0.1, z: 0.0}; doSetVelocity = false; } if (noVelocity) { - deactiveProps["velocity"] = { - x: 0.0, - y: 0.0, - z: 0.0 - }; - deactiveProps["angularVelocity"] = { - x: 0.0, - y: 0.0, - z: 0.0 - }; + deactiveProps["velocity"] = {x: 0.0, y: 0.0, z: 0.0}; + deactiveProps["angularVelocity"] = {x: 0.0, y: 0.0, z: 0.0}; doSetVelocity = false; } @@ -2239,7 +2169,7 @@ function MyController(hand) { // be fixed. Entities.editEntity(entityID, { velocity: this.currentVelocity - // angularVelocity: this.currentAngularVelocity + // angularVelocity: this.currentAngularVelocity }); } @@ -2249,32 +2179,14 @@ function MyController(hand) { deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, - velocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - angularVelocity: { - x: 0.0, - y: 0.0, - z: 0.0 - } + velocity: {x: 0.0, y: 0.0, z: 0.0}, + angularVelocity: {x: 0.0, y: 0.0, z: 0.0} }; Entities.editEntity(entityID, deactiveProps); } else if (noVelocity) { - Entities.editEntity(entityID, { - velocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - angularVelocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - dynamic: data["dynamic"] - }); + Entities.editEntity(entityID, {velocity: {x: 0.0, y: 0.0, z: 0.0}, + angularVelocity: {x: 0.0, y: 0.0, z: 0.0}, + dynamic: data["dynamic"]}); } } else { data = null; @@ -2324,24 +2236,24 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); - var handleHandMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { - leftController.turnOffVisualizations('left'); handToDisable = LEFT_HAND; + leftController.turnOffVisualizations(); } if (message === 'right') { - rightController.turnOffVisualizations('right'); handToDisable = RIGHT_HAND; - } - if (message === "both") { - leftController.turnOffVisualizations('left'); - rightController.turnOffVisualizations('right'); + rightController.turnOffVisualizations(); } if (message === 'both' || message === 'none') { + if (message === 'both') { + rightController.turnOffVisualizations(); + leftController.turnOffVisualizations(); + + } handToDisable = message; } } else if (channel === 'Hifi-Hand-Grab') { @@ -2420,4 +2332,4 @@ function handleMenuItemEvent(menuItem) { } } -Menu.menuItemEvent.connect(handleMenuItemEvent); \ No newline at end of file +Menu.menuItemEvent.connect(handleMenuItemEvent); diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5c881c5fcc..da0b4cb576 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -207,12 +207,13 @@ function Teleporter() { } this.disableMappings(); this.turnOffOverlayBeams(); - this.enableGrab(); + this.updateConnected = null; Script.setTimeout(function() { inTeleportMode = false; + _this.enableGrab(); }, 100); }; From 31948bce2d4a2a871dce7480e92cfd16db7b1063 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 8 Jul 2016 16:35:03 -0700 Subject: [PATCH 65/78] Support alpha and color gradients in circle overlays --- interface/src/ui/overlays/Circle3DOverlay.cpp | 313 +++++++++--------- interface/src/ui/overlays/Circle3DOverlay.h | 48 +-- libraries/render-utils/src/GeometryCache.cpp | 52 ++- libraries/render-utils/src/GeometryCache.h | 4 +- 4 files changed, 219 insertions(+), 198 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 2896ce711e..22995cbc35 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -17,28 +17,7 @@ QString const Circle3DOverlay::TYPE = "circle3d"; -Circle3DOverlay::Circle3DOverlay() : - _startAt(0.0f), - _endAt(360.0f), - _outerRadius(1.0f), - _innerRadius(0.0f), - _hasTickMarks(false), - _majorTickMarksAngle(0.0f), - _minorTickMarksAngle(0.0f), - _majorTickMarksLength(0.0f), - _minorTickMarksLength(0.0f), - _quadVerticesID(GeometryCache::UNKNOWN_ID), - _lineVerticesID(GeometryCache::UNKNOWN_ID), - _majorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _minorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _lastStartAt(-1.0f), - _lastEndAt(-1.0f), - _lastOuterRadius(-1.0f), - _lastInnerRadius(-1.0f) -{ - _majorTickMarksColor.red = _majorTickMarksColor.green = _majorTickMarksColor.blue = (unsigned char)0; - _minorTickMarksColor.red = _minorTickMarksColor.green = _minorTickMarksColor.blue = (unsigned char)0; -} +Circle3DOverlay::Circle3DOverlay() { } Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : Planar3DOverlay(circle3DOverlay), @@ -56,11 +35,7 @@ Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : _quadVerticesID(GeometryCache::UNKNOWN_ID), _lineVerticesID(GeometryCache::UNKNOWN_ID), _majorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _minorTicksVerticesID(GeometryCache::UNKNOWN_ID), - _lastStartAt(-1.0f), - _lastEndAt(-1.0f), - _lastOuterRadius(-1.0f), - _lastInnerRadius(-1.0f) + _minorTicksVerticesID(GeometryCache::UNKNOWN_ID) { } @@ -70,36 +45,25 @@ void Circle3DOverlay::render(RenderArgs* args) { } float alpha = getAlpha(); - if (alpha == 0.0f) { return; // do nothing if our alpha is 0, we're not visible } - // Create the circle in the coordinates origin - float outerRadius = getOuterRadius(); - float innerRadius = getInnerRadius(); // only used in solid case - float startAt = getStartAt(); - float endAt = getEndAt(); - - bool geometryChanged = (startAt != _lastStartAt || endAt != _lastEndAt || - innerRadius != _lastInnerRadius || outerRadius != _lastOuterRadius); - + bool geometryChanged = _dirty; + _dirty = false; const float FULL_CIRCLE = 360.0f; const float SLICES = 180.0f; // The amount of segment to create the circle const float SLICE_ANGLE = FULL_CIRCLE / SLICES; - - xColor colorX = getColor(); const float MAX_COLOR = 255.0f; - glm::vec4 color(colorX.red / MAX_COLOR, colorX.green / MAX_COLOR, colorX.blue / MAX_COLOR, alpha); - - bool colorChanged = colorX.red != _lastColor.red || colorX.green != _lastColor.green || colorX.blue != _lastColor.blue; - _lastColor = colorX; auto geometryCache = DependencyManager::get(); Q_ASSERT(args->_batch); auto& batch = *args->_batch; + if (args->_pipeline) { + batch.setPipeline(args->_pipeline->pipeline); + } // FIXME: THe line width of _lineWidth is not supported anymore, we ll need a workaround @@ -110,81 +74,89 @@ void Circle3DOverlay::render(RenderArgs* args) { // for our overlay, is solid means we draw a ring between the inner and outer radius of the circle, otherwise // we just draw a line... if (getIsSolid()) { - if (_quadVerticesID == GeometryCache::UNKNOWN_ID) { + if (!_quadVerticesID) { _quadVerticesID = geometryCache->allocateID(); } - if (geometryChanged || colorChanged) { - + if (geometryChanged) { QVector points; - - float angle = startAt; - float angleInRadians = glm::radians(angle); - glm::vec2 mostRecentInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 mostRecentOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - - while (angle < endAt) { - angleInRadians = glm::radians(angle); - glm::vec2 thisInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 thisOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); - - points << mostRecentInnerPoint << mostRecentOuterPoint << thisOuterPoint; // first triangle - points << mostRecentInnerPoint << thisInnerPoint << thisOuterPoint; // second triangle - - angle += SLICE_ANGLE; + QVector colors; - mostRecentInnerPoint = thisInnerPoint; - mostRecentOuterPoint = thisOuterPoint; + float pulseLevel = updatePulse(); + vec4 pulseModifier = vec4(1); + if (_alphaPulse != 0.0) { + pulseModifier.a = (_alphaPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); } - - // get the last slice portion.... - angle = endAt; - angleInRadians = glm::radians(angle); - glm::vec2 lastInnerPoint(cosf(angleInRadians) * innerRadius, sinf(angleInRadians) * innerRadius); - glm::vec2 lastOuterPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + if (_colorPulse != 0.0) { + float pulseValue = (_colorPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); + pulseModifier = vec4(vec3(pulseValue), pulseModifier.a); + } + vec4 innerStartColor = vec4(toGlm(_innerStartColor), _innerStartAlpha) * pulseModifier; + vec4 outerStartColor = vec4(toGlm(_outerStartColor), _outerStartAlpha) * pulseModifier; + vec4 innerEndColor = vec4(toGlm(_innerEndColor), _innerEndAlpha) * pulseModifier; + vec4 outerEndColor = vec4(toGlm(_outerEndColor), _outerEndAlpha) * pulseModifier; - points << mostRecentInnerPoint << mostRecentOuterPoint << lastOuterPoint; // first triangle - points << mostRecentInnerPoint << lastInnerPoint << lastOuterPoint; // second triangle - - geometryCache->updateVertices(_quadVerticesID, points, color); + if (_innerRadius <= 0) { + _solidPrimitive = gpu::TRIANGLE_FAN; + points << vec2(); + colors << innerStartColor; + for (float angle = _startAt; angle <= _endAt; angle += SLICE_ANGLE) { + float range = (angle - _startAt) / (_endAt - _startAt); + float angleRadians = glm::radians(angle); + points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + colors << glm::mix(outerStartColor, outerEndColor, range); + } + } else { + _solidPrimitive = gpu::TRIANGLE_STRIP; + for (float angle = _startAt; angle <= _endAt; angle += SLICE_ANGLE) { + float range = (angle - _startAt) / (_endAt - _startAt); + + float angleRadians = glm::radians(angle); + points << glm::vec2(cos(angleRadians) * _innerRadius, sin(angleRadians) * _innerRadius); + colors << glm::mix(innerStartColor, innerEndColor, range); + + points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + colors << glm::mix(outerStartColor, outerEndColor, range); + } + } + geometryCache->updateVertices(_quadVerticesID, points, colors); } - geometryCache->renderVertices(batch, gpu::TRIANGLES, _quadVerticesID); + geometryCache->renderVertices(batch, _solidPrimitive, _quadVerticesID); } else { - if (_lineVerticesID == GeometryCache::UNKNOWN_ID) { + if (!_lineVerticesID) { _lineVerticesID = geometryCache->allocateID(); } - if (geometryChanged || colorChanged) { + if (geometryChanged) { QVector points; - float angle = startAt; + float angle = _startAt; float angleInRadians = glm::radians(angle); - glm::vec2 firstPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 firstPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << firstPoint; - while (angle < endAt) { + while (angle < _endAt) { angle += SLICE_ANGLE; angleInRadians = glm::radians(angle); - glm::vec2 thisPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 thisPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << thisPoint; if (getIsDashedLine()) { angle += SLICE_ANGLE / 2.0f; // short gap angleInRadians = glm::radians(angle); - glm::vec2 dashStartPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 dashStartPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << dashStartPoint; } } // get the last slice portion.... - angle = endAt; + angle = _endAt; angleInRadians = glm::radians(angle); - glm::vec2 lastPoint(cosf(angleInRadians) * outerRadius, sinf(angleInRadians) * outerRadius); + glm::vec2 lastPoint(cosf(angleInRadians) * _outerRadius, sinf(angleInRadians) * _outerRadius); points << lastPoint; - - geometryCache->updateVertices(_lineVerticesID, points, color); + geometryCache->updateVertices(_lineVerticesID, points, vec4(toGlm(getColor()), getAlpha())); } if (getIsDashedLine()) { @@ -214,13 +186,13 @@ void Circle3DOverlay::render(RenderArgs* args) { if (getMajorTickMarksAngle() > 0.0f && getMajorTickMarksLength() != 0.0f) { float tickMarkAngle = getMajorTickMarksAngle(); - float angle = startAt - fmodf(startAt, tickMarkAngle) + tickMarkAngle; + float angle = _startAt - fmodf(_startAt, tickMarkAngle) + tickMarkAngle; float angleInRadians = glm::radians(angle); float tickMarkLength = getMajorTickMarksLength(); - float startRadius = (tickMarkLength > 0.0f) ? innerRadius : outerRadius; + float startRadius = (tickMarkLength > 0.0f) ? _innerRadius : _outerRadius; float endRadius = startRadius + tickMarkLength; - while (angle <= endAt) { + while (angle <= _endAt) { angleInRadians = glm::radians(angle); glm::vec2 thisPointA(cosf(angleInRadians) * startRadius, sinf(angleInRadians) * startRadius); @@ -236,13 +208,13 @@ void Circle3DOverlay::render(RenderArgs* args) { if (getMinorTickMarksAngle() > 0.0f && getMinorTickMarksLength() != 0.0f) { float tickMarkAngle = getMinorTickMarksAngle(); - float angle = startAt - fmodf(startAt, tickMarkAngle) + tickMarkAngle; + float angle = _startAt - fmodf(_startAt, tickMarkAngle) + tickMarkAngle; float angleInRadians = glm::radians(angle); float tickMarkLength = getMinorTickMarksLength(); - float startRadius = (tickMarkLength > 0.0f) ? innerRadius : outerRadius; + float startRadius = (tickMarkLength > 0.0f) ? _innerRadius : _outerRadius; float endRadius = startRadius + tickMarkLength; - while (angle <= endAt) { + while (angle <= _endAt) { angleInRadians = glm::radians(angle); glm::vec2 thisPointA(cosf(angleInRadians) * startRadius, sinf(angleInRadians) * startRadius); @@ -269,17 +241,10 @@ void Circle3DOverlay::render(RenderArgs* args) { geometryCache->renderVertices(batch, gpu::LINES, _minorTicksVerticesID); } - - if (geometryChanged) { - _lastStartAt = startAt; - _lastEndAt = endAt; - _lastInnerRadius = innerRadius; - _lastOuterRadius = outerRadius; - } } const render::ShapeKey Circle3DOverlay::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace(); + auto builder = render::ShapeKey::Builder().withoutCullFace().withUnlit(); if (getAlpha() != 1.0f) { builder.withTranslucent(); } @@ -289,72 +254,102 @@ const render::ShapeKey Circle3DOverlay::getShapeKey() { return builder.build(); } +template T fromVariant(const QVariant& v, bool& valid) { + valid = v.isValid(); + return qvariant_cast(v); +} + +template<> xColor fromVariant(const QVariant& v, bool& valid) { + return xColorFromVariant(v, valid); +} + +template +bool updateIfValid(const QVariantMap& properties, const char* key, T& output) { + bool valid; + T result = fromVariant(properties[key], valid); + if (!valid) { + return false; + } + + // Don't signal updates if the value was already set + if (result == output) { + return false; + } + + output = result; + return true; +} + +// Multicast, many outputs +template +bool updateIfValid(const QVariantMap& properties, const char* key, std::initializer_list> outputs) { + bool valid; + T value = fromVariant(properties[key], valid); + if (!valid) { + return false; + } + bool updated = false; + for (T& output : outputs) { + if (output != value) { + output = value; + updated = true; + } + } + return updated; +} + +// Multicast, multiple possible inputs, in order of preference +template +bool updateIfValid(const QVariantMap& properties, const std::initializer_list keys, T& output) { + for (const char* key : keys) { + if (updateIfValid(properties, key, output)) { + return true; + } + } + return false; +} + + void Circle3DOverlay::setProperties(const QVariantMap& properties) { Planar3DOverlay::setProperties(properties); + _dirty |= updateIfValid(properties, "alpha", { _innerStartAlpha, _innerEndAlpha, _outerStartAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "Alpha", { _innerStartAlpha, _innerEndAlpha, _outerStartAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "startAlpha", { _innerStartAlpha, _outerStartAlpha }); + _dirty |= updateIfValid(properties, "endAlpha", { _innerEndAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "innerAlpha", { _innerStartAlpha, _innerEndAlpha }); + _dirty |= updateIfValid(properties, "outerAlpha", { _outerStartAlpha, _outerEndAlpha }); + _dirty |= updateIfValid(properties, "innerStartAlpha", _innerStartAlpha); + _dirty |= updateIfValid(properties, "innerEndAlpha", _innerEndAlpha); + _dirty |= updateIfValid(properties, "outerStartAlpha", _outerStartAlpha); + _dirty |= updateIfValid(properties, "outerEndAlpha", _outerEndAlpha); - QVariant startAt = properties["startAt"]; - if (startAt.isValid()) { - setStartAt(startAt.toFloat()); - } + _dirty |= updateIfValid(properties, "color", { _innerStartColor, _innerEndColor, _outerStartColor, _outerEndColor }); + _dirty |= updateIfValid(properties, "startColor", { _innerStartColor, _outerStartColor } ); + _dirty |= updateIfValid(properties, "endColor", { _innerEndColor, _outerEndColor } ); + _dirty |= updateIfValid(properties, "innerColor", { _innerStartColor, _innerEndColor } ); + _dirty |= updateIfValid(properties, "outerColor", { _outerStartColor, _outerEndColor } ); + _dirty |= updateIfValid(properties, "innerStartColor", _innerStartColor); + _dirty |= updateIfValid(properties, "innerEndColor", _innerEndColor); + _dirty |= updateIfValid(properties, "outerStartColor", _outerStartColor); + _dirty |= updateIfValid(properties, "outerEndColor", _outerEndColor); - QVariant endAt = properties["endAt"]; - if (endAt.isValid()) { - setEndAt(endAt.toFloat()); - } + _dirty |= updateIfValid(properties, "startAt", _startAt); + _dirty |= updateIfValid(properties, "endAt", _endAt); - QVariant outerRadius = properties["radius"]; - if (!outerRadius.isValid()) { - outerRadius = properties["outerRadius"]; - } - if (outerRadius.isValid()) { - setOuterRadius(outerRadius.toFloat()); - } + _dirty |= updateIfValid(properties, { "radius", "outerRadius" }, _outerRadius); + _dirty |= updateIfValid(properties, "innerRadius", _innerRadius); + _dirty |= updateIfValid(properties, "hasTickMarks", _hasTickMarks); + _dirty |= updateIfValid(properties, "majorTickMarksAngle", _majorTickMarksAngle); + _dirty |= updateIfValid(properties, "minorTickMarksAngle", _minorTickMarksAngle); + _dirty |= updateIfValid(properties, "majorTickMarksLength", _majorTickMarksLength); + _dirty |= updateIfValid(properties, "minorTickMarksLength", _minorTickMarksLength); + _dirty |= updateIfValid(properties, "majorTickMarksColor", _majorTickMarksColor); + _dirty |= updateIfValid(properties, "minorTickMarksColor", _minorTickMarksColor); - QVariant innerRadius = properties["innerRadius"]; - if (innerRadius.isValid()) { - setInnerRadius(innerRadius.toFloat()); - } - - QVariant hasTickMarks = properties["hasTickMarks"]; - if (hasTickMarks.isValid()) { - setHasTickMarks(hasTickMarks.toBool()); - } - - QVariant majorTickMarksAngle = properties["majorTickMarksAngle"]; - if (majorTickMarksAngle.isValid()) { - setMajorTickMarksAngle(majorTickMarksAngle.toFloat()); - } - - QVariant minorTickMarksAngle = properties["minorTickMarksAngle"]; - if (minorTickMarksAngle.isValid()) { - setMinorTickMarksAngle(minorTickMarksAngle.toFloat()); - } - - QVariant majorTickMarksLength = properties["majorTickMarksLength"]; - if (majorTickMarksLength.isValid()) { - setMajorTickMarksLength(majorTickMarksLength.toFloat()); - } - - QVariant minorTickMarksLength = properties["minorTickMarksLength"]; - if (minorTickMarksLength.isValid()) { - setMinorTickMarksLength(minorTickMarksLength.toFloat()); - } - - bool valid; - auto majorTickMarksColor = properties["majorTickMarksColor"]; - if (majorTickMarksColor.isValid()) { - auto color = xColorFromVariant(majorTickMarksColor, valid); - if (valid) { - _majorTickMarksColor = color; - } - } - - auto minorTickMarksColor = properties["minorTickMarksColor"]; - if (minorTickMarksColor.isValid()) { - auto color = xColorFromVariant(majorTickMarksColor, valid); - if (valid) { - _minorTickMarksColor = color; - } + if (_innerStartAlpha < 1.0f || _innerEndAlpha < 1.0f || _outerStartAlpha < 1.0f || _outerEndAlpha < 1.0f) { + // Force the alpha to 0.5, since we'll ignore it in the presence of these other values, but we need + // it to be non-1 in order to get the right pipeline and non-0 in order to render at all. + _alpha = 0.5f; } } diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index c0e84ef1c6..39f1ade7e9 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -59,28 +59,34 @@ public: virtual Circle3DOverlay* createClone() const override; protected: - float _startAt; - float _endAt; - float _outerRadius; - float _innerRadius; - bool _hasTickMarks; - float _majorTickMarksAngle; - float _minorTickMarksAngle; - float _majorTickMarksLength; - float _minorTickMarksLength; - xColor _majorTickMarksColor; - xColor _minorTickMarksColor; - - int _quadVerticesID; - int _lineVerticesID; - int _majorTicksVerticesID; - int _minorTicksVerticesID; + float _startAt { 0 }; + float _endAt { 360 }; + float _outerRadius { 1 }; + float _innerRadius { 0 }; - xColor _lastColor; - float _lastStartAt; - float _lastEndAt; - float _lastOuterRadius; - float _lastInnerRadius; + xColor _innerStartColor; + xColor _innerEndColor; + xColor _outerStartColor; + xColor _outerEndColor; + float _innerStartAlpha; + float _innerEndAlpha; + float _outerStartAlpha; + float _outerEndAlpha; + + bool _hasTickMarks { false }; + float _majorTickMarksAngle { 0 }; + float _minorTickMarksAngle { 0 }; + float _majorTickMarksLength { 0 }; + float _minorTickMarksLength { 0 }; + xColor _majorTickMarksColor {}; + xColor _minorTickMarksColor {}; + gpu::Primitive _solidPrimitive { gpu::TRIANGLE_FAN }; + int _quadVerticesID { 0 }; + int _lineVerticesID { 0 }; + int _majorTicksVerticesID { 0 }; + int _minorTicksVerticesID { 0 }; + + bool _dirty { true }; }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index fd06272fa9..8be1c478be 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -432,7 +432,7 @@ void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, co renderQuad(batch, minCorner, maxCorner, MIN_TEX_COORD, MAX_TEX_COORD, color, id); } -void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { +void GeometryCache::updateVertices(int id, const QVector& points, const QVector& colors) { BatchItemDetails& details = _registeredVertices[id]; if (details.isCreated) { @@ -469,11 +469,6 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -481,16 +476,25 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorDataAt = colorData; const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - foreach(const glm::vec2& point, points) { + auto pointCount = points.size(); + auto colorCount = colors.size(); + int compactColor; + for (auto i = 0; i < pointCount; ++i) { + const auto& point = points[i]; *(vertex++) = point.x; *(vertex++) = point.y; *(vertex++) = NORMAL.x; *(vertex++) = NORMAL.y; *(vertex++) = NORMAL.z; - + if (i < colorCount) { + const auto& color = colors[i]; + compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + } *(colorDataAt++) = compactColor; } - details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData); details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData); delete[] vertexData; @@ -501,7 +505,11 @@ void GeometryCache::updateVertices(int id, const QVector& points, con #endif } -void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { +void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { + updateVertices(id, points, QVector({ color })); +} + +void GeometryCache::updateVertices(int id, const QVector& points, const QVector& colors) { BatchItemDetails& details = _registeredVertices[id]; if (details.isCreated) { details.clear(); @@ -537,11 +545,8 @@ void GeometryCache::updateVertices(int id, const QVector& points, con details.vertices = points.size(); details.vertexSize = FLOATS_PER_VERTEX; - int compactColor = ((int(color.x * 255.0f) & 0xFF)) | - ((int(color.y * 255.0f) & 0xFF) << 8) | - ((int(color.z * 255.0f) & 0xFF) << 16) | - ((int(color.w * 255.0f) & 0xFF) << 24); - + // Default to white + int compactColor = 0xFFFFFFFF; float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX]; float* vertex = vertexData; @@ -549,14 +554,23 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorDataAt = colorData; const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); - foreach(const glm::vec3& point, points) { + auto pointCount = points.size(); + auto colorCount = colors.size(); + for (auto i = 0; i < pointCount; ++i) { + const glm::vec3& point = points[i]; + if (i < colorCount) { + const glm::vec4& color = colors[i]; + compactColor = ((int(color.x * 255.0f) & 0xFF)) | + ((int(color.y * 255.0f) & 0xFF) << 8) | + ((int(color.z * 255.0f) & 0xFF) << 16) | + ((int(color.w * 255.0f) & 0xFF) << 24); + } *(vertex++) = point.x; *(vertex++) = point.y; *(vertex++) = point.z; *(vertex++) = NORMAL.x; *(vertex++) = NORMAL.y; *(vertex++) = NORMAL.z; - *(colorDataAt++) = compactColor; } @@ -570,6 +584,10 @@ void GeometryCache::updateVertices(int id, const QVector& points, con #endif } +void GeometryCache::updateVertices(int id, const QVector& points, const glm::vec4& color) { + updateVertices(id, points, QVector({ color })); +} + void GeometryCache::updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color) { BatchItemDetails& details = _registeredVertices[id]; diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 647fa9889c..1bfdc1798e 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -283,7 +283,9 @@ public: const glm::vec4& color1, const glm::vec4& color2, int id = UNKNOWN_ID); void updateVertices(int id, const QVector& points, const glm::vec4& color); + void updateVertices(int id, const QVector& points, const QVector& colors); void updateVertices(int id, const QVector& points, const glm::vec4& color); + void updateVertices(int id, const QVector& points, const QVector& colors); void updateVertices(int id, const QVector& points, const QVector& texCoords, const glm::vec4& color); void renderVertices(gpu::Batch& batch, gpu::Primitive primitiveType, int id); @@ -360,7 +362,7 @@ private: QHash _coneVBOs; - int _nextID{ 0 }; + int _nextID{ 1 }; QHash _lastRegisteredQuad3DTexture; QHash _quad3DTextures; From 2c72037e81872d61e055218aadb5773b908d5f2b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 8 Jul 2016 18:35:50 -0700 Subject: [PATCH 66/78] Fixing warnings --- interface/src/ui/overlays/Circle3DOverlay.cpp | 16 +++++++++------- interface/src/ui/overlays/Circle3DOverlay.h | 4 ++-- libraries/render-utils/src/GeometryCache.cpp | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 22995cbc35..6ebfd5c71c 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -14,10 +14,12 @@ #include #include - QString const Circle3DOverlay::TYPE = "circle3d"; -Circle3DOverlay::Circle3DOverlay() { } +Circle3DOverlay::Circle3DOverlay() { + memset(&_minorTickMarksColor, 0, sizeof(_minorTickMarksColor)); + memset(&_majorTickMarksColor, 0, sizeof(_majorTickMarksColor)); +} Circle3DOverlay::Circle3DOverlay(const Circle3DOverlay* circle3DOverlay) : Planar3DOverlay(circle3DOverlay), @@ -84,10 +86,10 @@ void Circle3DOverlay::render(RenderArgs* args) { float pulseLevel = updatePulse(); vec4 pulseModifier = vec4(1); - if (_alphaPulse != 0.0) { + if (_alphaPulse != 0.0f) { pulseModifier.a = (_alphaPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); } - if (_colorPulse != 0.0) { + if (_colorPulse != 0.0f) { float pulseValue = (_colorPulse >= 0.0f) ? pulseLevel : (1.0f - pulseLevel); pulseModifier = vec4(vec3(pulseValue), pulseModifier.a); } @@ -103,7 +105,7 @@ void Circle3DOverlay::render(RenderArgs* args) { for (float angle = _startAt; angle <= _endAt; angle += SLICE_ANGLE) { float range = (angle - _startAt) / (_endAt - _startAt); float angleRadians = glm::radians(angle); - points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + points << glm::vec2(cosf(angleRadians) * _outerRadius, sinf(angleRadians) * _outerRadius); colors << glm::mix(outerStartColor, outerEndColor, range); } } else { @@ -112,10 +114,10 @@ void Circle3DOverlay::render(RenderArgs* args) { float range = (angle - _startAt) / (_endAt - _startAt); float angleRadians = glm::radians(angle); - points << glm::vec2(cos(angleRadians) * _innerRadius, sin(angleRadians) * _innerRadius); + points << glm::vec2(cosf(angleRadians) * _innerRadius, sinf(angleRadians) * _innerRadius); colors << glm::mix(innerStartColor, innerEndColor, range); - points << glm::vec2(cos(angleRadians) * _outerRadius, sin(angleRadians) * _outerRadius); + points << glm::vec2(cosf(angleRadians) * _outerRadius, sinf(angleRadians) * _outerRadius); colors << glm::mix(outerStartColor, outerEndColor, range); } } diff --git a/interface/src/ui/overlays/Circle3DOverlay.h b/interface/src/ui/overlays/Circle3DOverlay.h index 39f1ade7e9..82c7c47dc7 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.h +++ b/interface/src/ui/overlays/Circle3DOverlay.h @@ -78,8 +78,8 @@ protected: float _minorTickMarksAngle { 0 }; float _majorTickMarksLength { 0 }; float _minorTickMarksLength { 0 }; - xColor _majorTickMarksColor {}; - xColor _minorTickMarksColor {}; + xColor _majorTickMarksColor; + xColor _minorTickMarksColor; gpu::Primitive _solidPrimitive { gpu::TRIANGLE_FAN }; int _quadVerticesID { 0 }; int _lineVerticesID { 0 }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 8be1c478be..4558b68af9 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -478,7 +478,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); auto pointCount = points.size(); auto colorCount = colors.size(); - int compactColor; + int compactColor = 0; for (auto i = 0; i < pointCount; ++i) { const auto& point = points[i]; *(vertex++) = point.x; From 207ddcea3863805597f066c23efe32192fcfa02d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 9 Jul 2016 22:25:28 -0700 Subject: [PATCH 67/78] wrap hull about each mesh part --- .../src/RenderableModelEntityItem.cpp | 84 ++++++++++++++++--- libraries/physics/src/ShapeFactory.cpp | 58 ++++++++++++- libraries/shared/src/ShapeInfo.h | 6 +- 3 files changed, 133 insertions(+), 15 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 416a38d27c..9f4ff63548 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -10,6 +10,7 @@ // #include +#include #include #include @@ -690,8 +691,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { glm::vec3 scaleToFit = dimensions / _model->getFBXGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. - for (int i = 0; i < pointCollection.size(); i++) { - for (int j = 0; j < pointCollection[i].size(); j++) { + for (int32_t i = 0; i < pointCollection.size(); i++) { + for (int32_t j = 0; j < pointCollection[i].size(); j++) { // compensate for registration pointCollection[i][j] += _model->getOffset(); // scale so the collision points match the model points @@ -708,9 +709,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // compute meshPart local transforms QVector localTransforms; const FBXGeometry& fbxGeometry = _model->getFBXGeometry(); - int numberOfMeshes = fbxGeometry.meshes.size(); - int totalNumVertices = 0; - for (int i = 0; i < numberOfMeshes; i++) { + int32_t numMeshes = (int32_t)fbxGeometry.meshes.size(); + int32_t totalNumVertices = 0; + for (int32_t i = 0; i < numMeshes; i++) { const FBXMesh& mesh = fbxGeometry.meshes.at(i); if (mesh.clusters.size() > 0) { const FBXCluster& cluster = mesh.clusters.at(0); @@ -722,7 +723,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } totalNumVertices += mesh.vertices.size(); } - const int MAX_VERTICES_PER_STATIC_MESH = 1e6; + const int32_t MAX_VERTICES_PER_STATIC_MESH = 1e6; if (totalNumVertices > MAX_VERTICES_PER_STATIC_MESH) { qWarning() << "model" << getModelURL() << "has too many vertices" << totalNumVertices << "and will collide as a box."; info.setParams(SHAPE_TYPE_BOX, 0.5f * dimensions); @@ -730,7 +731,9 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); - int32_t numMeshes = (int32_t)(meshes.size()); + + // the render geometry's mesh count should match that of the FBXGeometry + assert(numMeshes == (int32_t)(meshes.size())); ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); pointCollection.clear(); @@ -741,8 +744,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } Extents extents; - int meshCount = 0; - int pointListIndex = 0; + int32_t meshCount = 0; + int32_t pointListIndex = 0; for (auto& mesh : meshes) { const gpu::BufferView& vertices = mesh->getVertexBuffer(); const gpu::BufferView& indices = mesh->getIndexBuffer(); @@ -775,6 +778,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (type == SHAPE_TYPE_STATIC_MESH) { // copy into triangleIndices ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + triangleIndices.clear(); triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); gpu::BufferView::Iterator partItr = parts.cbegin(); while (partItr != parts.cend()) { @@ -823,6 +827,64 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } ++partItr; } + } else if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { + // for each mesh copy unique part indices, separated by special bogus (flag) index values + ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + triangleIndices.clear(); + gpu::BufferView::Iterator partItr = parts.cbegin(); + while (partItr != parts.cend()) { + // collect unique list of indices for this part + std::set uniqueIndices; + if (partItr->_topology == model::Mesh::TRIANGLES) { + assert(partItr->_numIndices % 3 == 0); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + partItr->_numIndices; + while (indexItr != indexEnd) { + uniqueIndices.insert(*indexItr); + ++indexItr; + } + } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { + assert(partItr->_numIndices > 2); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + (partItr->_numIndices - 2); + + // first triangle uses the first three indices + uniqueIndices.insert(*(indexItr++)); + uniqueIndices.insert(*(indexItr++)); + uniqueIndices.insert(*(indexItr++)); + + // the rest use previous and next index + uint32_t triangleCount = 1; + while (indexItr != indexEnd) { + if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + uniqueIndices.insert(*(indexItr - 2)); + uniqueIndices.insert(*(indexItr - 1)); + } else { + // odd triangles swap order of first two indices + uniqueIndices.insert(*(indexItr - 1)); + uniqueIndices.insert(*(indexItr - 2)); + } + uniqueIndices.insert(*indexItr); + ++triangleCount; + } + ++indexItr; + } + } + + // store uniqueIndices in triangleIndices + triangleIndices.reserve(triangleIndices.size() + uniqueIndices.size()); + for (auto index : uniqueIndices) { + triangleIndices.push_back(index); + } + // flag end of part + triangleIndices.push_back(END_OF_MESH_PART); + + ++partItr; + } + // flag end of mesh + triangleIndices.push_back(END_OF_MESH); } ++meshCount; } @@ -830,13 +892,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // scale and shift glm::vec3 extentsSize = extents.size(); glm::vec3 scaleToFit = dimensions / extentsSize; - for (int i = 0; i < 3; ++i) { + for (int32_t i = 0; i < 3; ++i) { if (extentsSize[i] < 1.0e-6f) { scaleToFit[i] = 1.0f; } } for (auto points : pointCollection) { - for (int i = 0; i < points.size(); ++i) { + for (int32_t i = 0; i < points.size(); ++i) { points[i] = (points[i] * scaleToFit); } } diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index f71711eccd..e47a0fe8a2 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -69,6 +69,7 @@ static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { // util method btConvexHullShape* createConvexHull(const ShapeInfo::PointList& points) { + //std::cout << "adebug createConvexHull() points.size() = " << points.size() << std::endl; // adebug assert(points.size() > 0); btConvexHullShape* hull = new btConvexHullShape(); @@ -240,6 +241,7 @@ void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray) { btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { btCollisionShape* shape = NULL; int type = info.getType(); + //std::cout << "adebug createShapeFromInfo() type = " << type << std::endl; // adebug switch(type) { case SHAPE_TYPE_BOX: { shape = new btBoxShape(glmToBullet(info.getHalfExtents())); @@ -258,8 +260,7 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } break; case SHAPE_TYPE_COMPOUND: - case SHAPE_TYPE_SIMPLE_HULL: - case SHAPE_TYPE_SIMPLE_COMPOUND: { + case SHAPE_TYPE_SIMPLE_HULL: { const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); uint32_t numSubShapes = info.getNumSubShapes(); if (numSubShapes == 1) { @@ -270,12 +271,63 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { trans.setIdentity(); foreach (const ShapeInfo::PointList& hullPoints, pointCollection) { btConvexHullShape* hull = createConvexHull(hullPoints); - compound->addChildShape (trans, hull); + compound->addChildShape(trans, hull); } shape = compound; } } break; + case SHAPE_TYPE_SIMPLE_COMPOUND: { + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + const ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + uint32_t numIndices = triangleIndices.size(); + uint32_t numMeshes = info.getNumSubShapes(); + const uint32_t MIN_NUM_SIMPLE_COMPOUND_INDICES = 2; // END_OF_MESH_PART + END_OF_MESH + if (numMeshes > 0 && numIndices > MIN_NUM_SIMPLE_COMPOUND_INDICES) { + uint32_t i = 0; + std::vector hulls; + for (auto& points : pointCollection) { + // build a hull around each part + while (i < numIndices) { + ShapeInfo::PointList hullPoints; + hullPoints.reserve(points.size()); + while (i < numIndices) { + int32_t j = triangleIndices[i]; + ++i; + if (j == END_OF_MESH_PART) { + // end of part + break; + } + hullPoints.push_back(points[j]); + } + if (hullPoints.size() > 0) { + btConvexHullShape* hull = createConvexHull(hullPoints); + hulls.push_back(hull); + } + + assert(i < numIndices); + if (triangleIndices[i] == END_OF_MESH) { + // end of mesh + ++i; + break; + } + } + } + uint32_t numHulls = hulls.size(); + if (numHulls == 1) { + shape = hulls[0]; + } else { + auto compound = new btCompoundShape(); + btTransform trans; + trans.setIdentity(); + for (auto hull : hulls) { + compound->addChildShape(trans, hull); + } + shape = compound; + } + } + } + break; case SHAPE_TYPE_STATIC_MESH: { btTriangleIndexVertexArray* dataArray = createStaticMeshArray(info); shape = new StaticMeshShape(dataArray); diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index 7bf145412a..a6ff8d6d4a 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -26,6 +26,10 @@ const float MIN_SHAPE_OFFSET = 0.001f; // offsets less than 1mm will be ignored // trim convex hulls with many points down to only 42 points. const int MAX_HULL_POINTS = 42; + +const int32_t END_OF_MESH_PART = -1; // bogus vertex index at end of mesh part +const int32_t END_OF_MESH = -2; // bogus vertex index at end of mesh + enum ShapeType { SHAPE_TYPE_NONE, SHAPE_TYPE_BOX, @@ -50,7 +54,7 @@ public: using PointList = QVector; using PointCollection = QVector; - using TriangleIndices = QVector; + using TriangleIndices = QVector; void clear(); From 72175ae09e1510de3adef4fcb2b90e515cc05a0a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 9 Jul 2016 22:55:55 -0700 Subject: [PATCH 68/78] fix warnings about implicit casts --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- libraries/physics/src/ShapeFactory.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 9f4ff63548..d63361538a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -874,7 +874,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { } // store uniqueIndices in triangleIndices - triangleIndices.reserve(triangleIndices.size() + uniqueIndices.size()); + triangleIndices.reserve(triangleIndices.size() + (int32_t)uniqueIndices.size()); for (auto index : uniqueIndices) { triangleIndices.push_back(index); } diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index e47a0fe8a2..944e05e571 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -313,7 +313,7 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } } } - uint32_t numHulls = hulls.size(); + uint32_t numHulls = (uint32_t)hulls.size(); if (numHulls == 1) { shape = hulls[0]; } else { From dc9c06a159810f0f3fdc039fd9896c1f172042ac Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 11 Jul 2016 15:14:19 -0700 Subject: [PATCH 69/78] clean util --- .../developer/utilities/tools/overlayFinder.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/developer/utilities/tools/overlayFinder.js b/scripts/developer/utilities/tools/overlayFinder.js index 81ee687cbe..75b8aeacfa 100644 --- a/scripts/developer/utilities/tools/overlayFinder.js +++ b/scripts/developer/utilities/tools/overlayFinder.js @@ -1,17 +1,17 @@ function mousePressEvent(event) { - var overlay = Overlays.getOverlayAtPoint({x:event.x,y:event.y}); - print('overlay is: ' + overlay) - // var pickRay = Camera.computePickRay(event.x, event.y); - - // var intersection = Overlays.findRayIntersection(pickRay); - // print('intersection is: ' + intersection) + var overlay = Overlays.getOverlayAtPoint({ + x: event.x, + y: event.y + }); + // var pickRay = Camera.computePickRay(event.x, event.y); + // var intersection = Overlays.findRayIntersection(pickRay); + // print('intersection is: ' + intersection) }; -Controller.mouseMoveEvent.connect(function(event){ - print('mouse press') +Controller.mousePressEvent.connect(function(event) { mousePressEvent(event) }); -Script.scriptEnding.connect(function(){ +Script.scriptEnding.connect(function() { Controller.mousePressEvent.disconnect(mousePressEvent); }) \ No newline at end of file From 0efc684992f44fadd687de16a90984b5225f7671 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 11 Jul 2016 14:19:27 -0700 Subject: [PATCH 70/78] Add glow to the termination point of UI pointers --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 253 ++++++++++++++++-- .../display-plugins/hmd/HmdDisplayPlugin.h | 18 ++ 2 files changed, 244 insertions(+), 27 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c2497e5740..2dc7df341a 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include #include @@ -34,6 +36,9 @@ static const QString REPROJECTION = "Allow Reprojection"; static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; static const QString DEVELOPER_MENU_PATH = "Developer>" + DisplayPlugin::MENU_PATH(); static const bool DEFAULT_MONO_VIEW = true; +static const int NUMBER_OF_HANDS = 2; +static const glm::mat4 IDENTITY_MATRIX; + glm::uvec2 HmdDisplayPlugin::getRecommendedUiSize() const { return CompositorHelper::VIRTUAL_SCREEN_SIZE; @@ -241,6 +246,9 @@ void main() { )VS"; +static const char * LASER_GS = R"GS( +)GS"; + static const char * LASER_FS = R"FS(#version 410 core uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); @@ -254,6 +262,85 @@ void main() { )FS"; + +static const char * LASER_GLOW_VS = R"VS(#version 410 core + +uniform mat4 mvp = mat4(1); + +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} + +)VS"; + +static const char * LASER_GLOW_FS = R"FS(#version 410 core +#line 286 +uniform sampler2D sampler; +uniform float alpha = 1.0; +uniform vec4 glowPoints = vec4(-1); +uniform vec4 glowColor1 = vec4(0.01, 0.7, 0.9, 1.0); +uniform vec4 glowColor2 = vec4(0.9, 0.7, 0.01, 1.0); +uniform vec2 resolution = vec2(3960.0, 1188.0); +uniform float radius = 0.005; + +in vec3 vPosition; +in vec2 vTexCoord; +in vec4 vGlowPoints; + +out vec4 FragColor; + +float easeInOutCubic(float f) { + const float d = 1.0; + const float b = 0.0; + const float c = 1.0; + float t = f; + if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; + return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; +} + +void main() { + vec2 aspect = resolution; + aspect /= resolution.x; + FragColor = texture(sampler, vTexCoord); + + float glowIntensity = 0.0; + float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); + float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); + float dist = min(dist1, dist2); + vec3 glowColor = glowColor1.rgb; + if (dist2 < dist1) { + glowColor = glowColor2.rgb; + } + + if (dist <= radius) { + glowIntensity = 1.0 - (dist / radius); + glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); + glowIntensity = easeInOutCubic(glowIntensity); + glowIntensity = pow(glowIntensity, 0.5); + } + + if (alpha <= 0.0) { + if (glowIntensity <= 0.0) { + discard; + } + + FragColor = vec4(glowColor, glowIntensity); + return; + } + + FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); + FragColor.a *= alpha; +} +)FS"; + void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled @@ -263,7 +350,6 @@ void HmdDisplayPlugin::customizeContext() { #endif _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); - using namespace oglplus; if (!_enablePreview) { const std::string version("#version 410 core\n"); @@ -271,6 +357,7 @@ void HmdDisplayPlugin::customizeContext() { PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); } + updateGlowProgram(); compileProgram(_laserProgram, LASER_VS, LASER_FS); _laserGeometry = loadLaser(_laserProgram); @@ -280,7 +367,67 @@ void HmdDisplayPlugin::customizeContext() { PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "projections").Location(); } +#if 0 +static QString readFile(const char* filename) { + QFile file(filename); + file.open(QFile::Text | QFile::ReadOnly); + QString result; + result.append(QTextStream(&file).readAll()); + return result; +} +#endif + + +void HmdDisplayPlugin::updateGlowProgram() { +#if 0 + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + static const char* vsFile = "H:/glow_vert.glsl"; + static const char* fsFile = "H:/glow_frag.glsl"; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!_overlayProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr newOverlayProgram; + compileProgram(newOverlayProgram, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (newOverlayProgram) { + using namespace oglplus; + _overlayProgram = newOverlayProgram; + auto uniforms = _overlayProgram->ActiveUniforms(); + _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); + _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); + _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); + _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); + _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); + useProgram(_overlayProgram); + Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); + } + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; + + } +#else + if (!_overlayProgram) { + compileProgram(_overlayProgram, LASER_GLOW_VS, LASER_GLOW_FS); + using namespace oglplus; + auto uniforms = _overlayProgram->ActiveUniforms(); + _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); + _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); + _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); + _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); + _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); + useProgram(_overlayProgram); + Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); + } + +#endif +} + void HmdDisplayPlugin::uncustomizeContext() { + _overlayProgram.reset(); _sphereSection.reset(); _compositeFramebuffer.reset(); _previewProgram.reset(); @@ -327,19 +474,83 @@ void HmdDisplayPlugin::compositeOverlay() { auto compositorHelper = DependencyManager::get(); glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - useProgram(_program); - // set the alpha - Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); + withPresentThreadLock([&] { + _presentHandLasers = _handLasers; + _presentHandPoses = _handPoses; + _presentUiModelTransform = _uiModelTransform; + }); + std::array handGlowPoints { { vec2(-1), vec2(-1) } }; + + // compute the glow point interesections + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + if (_presentHandPoses[i] == IDENTITY_MATRIX) { + continue; + } + const auto& handLaser = _presentHandLasers[i]; + if (!handLaser.valid()) { + continue; + } + + const auto& laserDirection = handLaser.direction; + auto model = _presentHandPoses[i]; + auto castDirection = glm::quat_cast(model) * laserDirection; + if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { + castDirection = glm::normalize(castDirection); + castDirection = glm::inverse(_presentUiModelTransform.getRotation()) * castDirection; + } + + // FIXME fetch the actual UI radius from... somewhere? + float uiRadius = 1.0f; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance; + if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + continue; + } + + vec3 intersectionPosition = vec3(_presentHandPoses[i][3]) + (castDirection * distance) - _presentUiModelTransform.getTranslation(); + intersectionPosition = glm::inverse(_presentUiModelTransform.getRotation()) * intersectionPosition; + + // Take the interesection normal and convert it to a texture coordinate + vec2 yawPitch; + { + vec2 xdir = glm::normalize(vec2(intersectionPosition.x, -intersectionPosition.z)); + yawPitch.x = glm::atan(xdir.x, xdir.y); + yawPitch.y = (acosf(intersectionPosition.y) * -1.0f) + M_PI_2; + } + vec2 halfFov = CompositorHelper::VIRTUAL_UI_TARGET_FOV / 2.0f; + + // Are we out of range + if (glm::any(glm::greaterThan(glm::abs(yawPitch), halfFov))) { + continue; + } + + yawPitch /= CompositorHelper::VIRTUAL_UI_TARGET_FOV; + yawPitch += 0.5f; + handGlowPoints[i] = yawPitch; + } + + updateGlowProgram(); + useProgram(_overlayProgram); + // Setup the uniforms + { + if (_overlayUniforms.alpha >= 0) { + Uniform(*_overlayProgram, _overlayUniforms.alpha).Set(_compositeOverlayAlpha); + } + if (_overlayUniforms.glowPoints >= 0) { + vec4 glowPoints(handGlowPoints[0], handGlowPoints[1]); + Uniform(*_overlayProgram, _overlayUniforms.glowPoints).Set(glowPoints); + } + } + _sphereSection->Use(); for_each_eye([&](Eye eye) { eyeViewport(eye); auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; auto mvp = _eyeProjections[eye] * modelView; - Uniform(*_program, _mvpUniform).Set(mvp); + Uniform(*_overlayProgram, _overlayUniforms.mvp).Set(mvp); _sphereSection->Draw(); }); - // restore the alpha - Uniform(*_program, _alphaUniform).Set(1.0); } void HmdDisplayPlugin::compositePointer() { @@ -478,23 +689,12 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve } void HmdDisplayPlugin::compositeExtra() { - const int NUMBER_OF_HANDS = 2; - std::array handLasers; - std::array renderHandPoses; - Transform uiModelTransform; - withPresentThreadLock([&] { - handLasers = _handLasers; - renderHandPoses = _handPoses; - uiModelTransform = _uiModelTransform; - }); - // If neither hand laser is activated, exit - if (!handLasers[0].valid() && !handLasers[1].valid()) { + if (!_presentHandLasers[0].valid() && !_presentHandLasers[1].valid()) { return; } - static const glm::mat4 identity; - if (renderHandPoses[0] == identity && renderHandPoses[1] == identity) { + if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX) { return; } @@ -505,16 +705,16 @@ void HmdDisplayPlugin::compositeExtra() { std::array handLaserModelMatrices; for (int i = 0; i < NUMBER_OF_HANDS; ++i) { - if (renderHandPoses[i] == identity) { + if (_presentHandPoses[i] == IDENTITY_MATRIX) { continue; } - const auto& handLaser = handLasers[i]; + const auto& handLaser = _presentHandLasers[i]; if (!handLaser.valid()) { continue; } const auto& laserDirection = handLaser.direction; - auto model = renderHandPoses[i]; + auto model = _presentHandPoses[i]; auto castDirection = glm::quat_cast(model) * laserDirection; if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { castDirection = glm::normalize(castDirection); @@ -525,7 +725,7 @@ void HmdDisplayPlugin::compositeExtra() { // Find the intersection of the laser with he UI and use it to scale the model matrix float distance; - if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, uiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { continue; } @@ -545,13 +745,12 @@ void HmdDisplayPlugin::compositeExtra() { auto view = glm::inverse(eyePose); const auto& projection = _eyeProjections[eye]; for (int i = 0; i < NUMBER_OF_HANDS; ++i) { - if (handLaserModelMatrices[i] == identity) { + if (handLaserModelMatrices[i] == IDENTITY_MATRIX) { continue; } Uniform(*_laserProgram, "mvp").Set(projection * view * handLaserModelMatrices[i]); - Uniform(*_laserProgram, "color").Set(handLasers[i].color); + Uniform(*_laserProgram, "color").Set(_presentHandLasers[i].color); _laserGeometry->Draw(); - // TODO render some kind of visual indicator at the intersection point with the UI. } }); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 8e48690fd1..be2811076d 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -61,13 +61,29 @@ protected: } }; + ProgramPtr _overlayProgram; + struct OverlayUniforms { + int32_t mvp { -1 }; + int32_t alpha { -1 }; + int32_t glowPoints { -1 }; + int32_t resolution { -1 }; + int32_t radius { -1 }; + } _overlayUniforms; + Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; + + Transform _presentUiModelTransform; + std::array _presentHandLasers; + std::array _presentHandPoses; + std::array _eyeOffsets; std::array _eyeProjections; std::array _eyeInverseProjections; + + glm::mat4 _cullingProjection; glm::uvec2 _renderTargetSize; float _ipd { 0.064f }; @@ -87,6 +103,8 @@ protected: FrameInfo _currentRenderFrameInfo; private: + void updateGlowProgram(); + bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; From 6242816f68b47c74f20479bcc5fdabac5188f21f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 11 Jul 2016 18:59:22 -0700 Subject: [PATCH 71/78] Adding glow to hand lasers --- .../resources/shaders/hmd_hand_lasers.frag | 35 ++ .../resources/shaders/hmd_hand_lasers.geom | 70 +++ .../resources/shaders/hmd_hand_lasers.vert | 15 + .../resources/shaders/hmd_reproject.frag | 78 ++++ .../resources/shaders/hmd_reproject.vert | 20 + interface/resources/shaders/hmd_ui_glow.frag | 65 +++ interface/resources/shaders/hmd_ui_glow.vert | 23 + .../display-plugins/hmd/HmdDisplayPlugin.cpp | 425 ++++++------------ .../display-plugins/hmd/HmdDisplayPlugin.h | 39 +- libraries/gl/src/gl/OglplusHelpers.cpp | 30 ++ libraries/gl/src/gl/OglplusHelpers.h | 2 + 11 files changed, 493 insertions(+), 309 deletions(-) create mode 100644 interface/resources/shaders/hmd_hand_lasers.frag create mode 100644 interface/resources/shaders/hmd_hand_lasers.geom create mode 100644 interface/resources/shaders/hmd_hand_lasers.vert create mode 100644 interface/resources/shaders/hmd_reproject.frag create mode 100644 interface/resources/shaders/hmd_reproject.vert create mode 100644 interface/resources/shaders/hmd_ui_glow.frag create mode 100644 interface/resources/shaders/hmd_ui_glow.vert diff --git a/interface/resources/shaders/hmd_hand_lasers.frag b/interface/resources/shaders/hmd_hand_lasers.frag new file mode 100644 index 0000000000..6fffb1c521 --- /dev/null +++ b/interface/resources/shaders/hmd_hand_lasers.frag @@ -0,0 +1,35 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); + +layout(location = 0) in vec3 inLineDistance; + +out vec4 FragColor; + +void main() { + vec2 d = inLineDistance.xy; + d.y = abs(d.y); + d.x = abs(d.x); + if (d.x > 1.0) { + d.x = (d.x - 1.0) / 0.02; + } else { + d.x = 0.0; + } + float alpha = 1.0 - length(d); + if (alpha <= 0.0) { + discard; + } + alpha = pow(alpha, 10.0); + if (alpha < 0.05) { + discard; + } + FragColor = vec4(color.rgb, alpha); +} diff --git a/interface/resources/shaders/hmd_hand_lasers.geom b/interface/resources/shaders/hmd_hand_lasers.geom new file mode 100644 index 0000000000..16b5dafadd --- /dev/null +++ b/interface/resources/shaders/hmd_hand_lasers.geom @@ -0,0 +1,70 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#version 410 core +#extension GL_EXT_geometry_shader4 : enable + +layout(location = 0) out vec3 outLineDistance; + +layout(lines) in; +layout(triangle_strip, max_vertices = 24) out; + +vec3[2] getOrthogonals(in vec3 n, float scale) { + float yDot = abs(dot(n, vec3(0, 1, 0))); + + vec3 result[2]; + if (yDot < 0.9) { + result[0] = normalize(cross(n, vec3(0, 1, 0))); + } else { + result[0] = normalize(cross(n, vec3(1, 0, 0))); + } + // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other + result[1] = cross(result[0], n); + result[0] *= scale; + result[1] *= scale; + return result; +} + + +vec2 orthogonal(vec2 v) { + vec2 result = v.yx; + result.y *= -1.0; + return result; +} + +void main() { + vec2 endpoints[2]; + for (int i = 0; i < 2; ++i) { + endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; + } + vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); + vec2 lineOrthogonal = orthogonal(lineNormal); + lineNormal *= 0.02; + lineOrthogonal *= 0.02; + + gl_Position = gl_PositionIn[0]; + gl_Position.xy -= lineOrthogonal; + outLineDistance = vec3(-1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[0]; + gl_Position.xy += lineOrthogonal; + outLineDistance = vec3(-1.02, 1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy -= lineOrthogonal; + outLineDistance = vec3(1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy += lineOrthogonal; + outLineDistance = vec3(1.02, 1, gl_Position.z); + EmitVertex(); + + EndPrimitive(); +} diff --git a/interface/resources/shaders/hmd_hand_lasers.vert b/interface/resources/shaders/hmd_hand_lasers.vert new file mode 100644 index 0000000000..db5c7c1ecd --- /dev/null +++ b/interface/resources/shaders/hmd_hand_lasers.vert @@ -0,0 +1,15 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#version 410 core +uniform mat4 mvp = mat4(1); + +in vec3 Position; + +void main() { + gl_Position = mvp * vec4(Position, 1); +} diff --git a/interface/resources/shaders/hmd_reproject.frag b/interface/resources/shaders/hmd_reproject.frag new file mode 100644 index 0000000000..adda0315a3 --- /dev/null +++ b/interface/resources/shaders/hmd_reproject.frag @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform sampler2D sampler; +uniform mat3 reprojection = mat3(1); +uniform mat4 inverseProjections[2]; +uniform mat4 projections[2]; + +in vec2 vTexCoord; +in vec3 vPosition; + +out vec4 FragColor; + +void main() { + vec2 uv = vTexCoord; + + mat4 eyeInverseProjection; + mat4 eyeProjection; + + float xoffset = 1.0; + vec2 uvmin = vec2(0.0); + vec2 uvmax = vec2(1.0); + // determine the correct projection and inverse projection to use. + if (vTexCoord.x < 0.5) { + uvmax.x = 0.5; + eyeInverseProjection = inverseProjections[0]; + eyeProjection = projections[0]; + } else { + xoffset = -1.0; + uvmin.x = 0.5; + uvmax.x = 1.0; + eyeInverseProjection = inverseProjections[1]; + eyeProjection = projections[1]; + } + + // Account for stereo in calculating the per-eye NDC coordinates + vec4 ndcSpace = vec4(vPosition, 1.0); + ndcSpace.x *= 2.0; + ndcSpace.x += xoffset; + + // Convert from NDC to eyespace + vec4 eyeSpace = eyeInverseProjection * ndcSpace; + eyeSpace /= eyeSpace.w; + + // Convert to a noramlized ray + vec3 ray = eyeSpace.xyz; + ray = normalize(ray); + + // Adjust the ray by the rotation + ray = reprojection * ray; + + // Project back on to the texture plane + ray *= eyeSpace.z / ray.z; + + // Update the eyespace vector + eyeSpace.xyz = ray; + + // Reproject back into NDC + ndcSpace = eyeProjection * eyeSpace; + ndcSpace /= ndcSpace.w; + ndcSpace.x -= xoffset; + ndcSpace.x /= 2.0; + + // Calculate the new UV coordinates + uv = (ndcSpace.xy / 2.0) + 0.5; + if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + FragColor = texture(sampler, uv); + } +} \ No newline at end of file diff --git a/interface/resources/shaders/hmd_reproject.vert b/interface/resources/shaders/hmd_reproject.vert new file mode 100644 index 0000000000..923375613a --- /dev/null +++ b/interface/resources/shaders/hmd_reproject.vert @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} diff --git a/interface/resources/shaders/hmd_ui_glow.frag b/interface/resources/shaders/hmd_ui_glow.frag new file mode 100644 index 0000000000..733f32d718 --- /dev/null +++ b/interface/resources/shaders/hmd_ui_glow.frag @@ -0,0 +1,65 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform sampler2D sampler; +uniform float alpha = 1.0; +uniform vec4 glowPoints = vec4(-1); +uniform vec4 glowColors[2]; +uniform vec2 resolution = vec2(3960.0, 1188.0); +uniform float radius = 0.005; + +in vec3 vPosition; +in vec2 vTexCoord; +in vec4 vGlowPoints; + +out vec4 FragColor; + +float easeInOutCubic(float f) { + const float d = 1.0; + const float b = 0.0; + const float c = 1.0; + float t = f; + if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; + return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; +} + +void main() { + vec2 aspect = resolution; + aspect /= resolution.x; + FragColor = texture(sampler, vTexCoord); + + float glowIntensity = 0.0; + float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); + float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); + float dist = min(dist1, dist2); + vec3 glowColor = glowColors[0].rgb; + if (dist2 < dist1) { + glowColor = glowColors[1].rgb; + } + + if (dist <= radius) { + glowIntensity = 1.0 - (dist / radius); + glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); + glowIntensity = easeInOutCubic(glowIntensity); + glowIntensity = pow(glowIntensity, 0.5); + } + + if (alpha <= 0.0) { + if (glowIntensity <= 0.0) { + discard; + } + + FragColor = vec4(glowColor, glowIntensity); + return; + } + + FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); + FragColor.a *= alpha; +} \ No newline at end of file diff --git a/interface/resources/shaders/hmd_ui_glow.vert b/interface/resources/shaders/hmd_ui_glow.vert new file mode 100644 index 0000000000..5defec085f --- /dev/null +++ b/interface/resources/shaders/hmd_ui_glow.vert @@ -0,0 +1,23 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform mat4 mvp = mat4(1); + +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 2dc7df341a..1bfa6c7921 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -95,252 +95,6 @@ void HmdDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); } - -static const char * REPROJECTION_VS = R"VS(#version 410 core -in vec3 Position; -in vec2 TexCoord; - -out vec3 vPosition; -out vec2 vTexCoord; - -void main() { - gl_Position = vec4(Position, 1); - vTexCoord = TexCoord; - vPosition = Position; -} - -)VS"; - -static GLint REPROJECTION_MATRIX_LOCATION = -1; -static GLint INVERSE_PROJECTION_MATRIX_LOCATION = -1; -static GLint PROJECTION_MATRIX_LOCATION = -1; -static const char * REPROJECTION_FS = R"FS(#version 410 core - -uniform sampler2D sampler; -uniform mat3 reprojection = mat3(1); -uniform mat4 inverseProjections[2]; -uniform mat4 projections[2]; - -in vec2 vTexCoord; -in vec3 vPosition; - -out vec4 FragColor; - -void main() { - vec2 uv = vTexCoord; - - mat4 eyeInverseProjection; - mat4 eyeProjection; - - float xoffset = 1.0; - vec2 uvmin = vec2(0.0); - vec2 uvmax = vec2(1.0); - // determine the correct projection and inverse projection to use. - if (vTexCoord.x < 0.5) { - uvmax.x = 0.5; - eyeInverseProjection = inverseProjections[0]; - eyeProjection = projections[0]; - } else { - xoffset = -1.0; - uvmin.x = 0.5; - uvmax.x = 1.0; - eyeInverseProjection = inverseProjections[1]; - eyeProjection = projections[1]; - } - - // Account for stereo in calculating the per-eye NDC coordinates - vec4 ndcSpace = vec4(vPosition, 1.0); - ndcSpace.x *= 2.0; - ndcSpace.x += xoffset; - - // Convert from NDC to eyespace - vec4 eyeSpace = eyeInverseProjection * ndcSpace; - eyeSpace /= eyeSpace.w; - - // Convert to a noramlized ray - vec3 ray = eyeSpace.xyz; - ray = normalize(ray); - - // Adjust the ray by the rotation - ray = reprojection * ray; - - // Project back on to the texture plane - ray *= eyeSpace.z / ray.z; - - // Update the eyespace vector - eyeSpace.xyz = ray; - - // Reproject back into NDC - ndcSpace = eyeProjection * eyeSpace; - ndcSpace /= ndcSpace.w; - ndcSpace.x -= xoffset; - ndcSpace.x /= 2.0; - - // Calculate the new UV coordinates - uv = (ndcSpace.xy / 2.0) + 0.5; - if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { - FragColor = vec4(0.0, 0.0, 0.0, 1.0); - } else { - FragColor = texture(sampler, uv); - } -} -)FS"; - -#ifdef DEBUG_REPROJECTION_SHADER -#include -#include -#include -#include - -static const QString REPROJECTION_FS_FILE = "c:/Users/bdavis/Git/hifi/interface/resources/shaders/reproject.frag"; - -static ProgramPtr getReprojectionProgram() { - static ProgramPtr _currentProgram; - uint64_t now = usecTimestampNow(); - static uint64_t _lastFileCheck = now; - - bool modified = false; - if ((now - _lastFileCheck) > USECS_PER_MSEC * 100) { - QFileInfo info(REPROJECTION_FS_FILE); - QDateTime lastModified = info.lastModified(); - static QDateTime _lastModified = lastModified; - qDebug() << lastModified.toTime_t(); - qDebug() << _lastModified.toTime_t(); - if (lastModified > _lastModified) { - _lastModified = lastModified; - modified = true; - } - } - - if (!_currentProgram || modified) { - _currentProgram.reset(); - try { - QFile shaderFile(REPROJECTION_FS_FILE); - shaderFile.open(QIODevice::ReadOnly); - QString fragment = shaderFile.readAll(); - compileProgram(_currentProgram, REPROJECTION_VS, fragment.toLocal8Bit().data()); - } catch (const std::runtime_error& error) { - qDebug() << "Failed to build: " << error.what(); - } - if (!_currentProgram) { - _currentProgram = loadDefaultShader(); - } - } - return _currentProgram; -} -#endif - -static GLint PREVIEW_TEXTURE_LOCATION = -1; - -static const char * LASER_VS = R"VS(#version 410 core -uniform mat4 mvp = mat4(1); - -in vec3 Position; - -out vec3 vPosition; - -void main() { - gl_Position = mvp * vec4(Position, 1); - vPosition = Position; -} - -)VS"; - -static const char * LASER_GS = R"GS( -)GS"; - -static const char * LASER_FS = R"FS(#version 410 core - -uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); -in vec3 vPosition; - -out vec4 FragColor; - -void main() { - FragColor = color; -} - -)FS"; - - -static const char * LASER_GLOW_VS = R"VS(#version 410 core - -uniform mat4 mvp = mat4(1); - -in vec3 Position; -in vec2 TexCoord; - -out vec3 vPosition; -out vec2 vTexCoord; - -void main() { - gl_Position = mvp * vec4(Position, 1); - vTexCoord = TexCoord; - vPosition = Position; -} - -)VS"; - -static const char * LASER_GLOW_FS = R"FS(#version 410 core -#line 286 -uniform sampler2D sampler; -uniform float alpha = 1.0; -uniform vec4 glowPoints = vec4(-1); -uniform vec4 glowColor1 = vec4(0.01, 0.7, 0.9, 1.0); -uniform vec4 glowColor2 = vec4(0.9, 0.7, 0.01, 1.0); -uniform vec2 resolution = vec2(3960.0, 1188.0); -uniform float radius = 0.005; - -in vec3 vPosition; -in vec2 vTexCoord; -in vec4 vGlowPoints; - -out vec4 FragColor; - -float easeInOutCubic(float f) { - const float d = 1.0; - const float b = 0.0; - const float c = 1.0; - float t = f; - if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; - return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; -} - -void main() { - vec2 aspect = resolution; - aspect /= resolution.x; - FragColor = texture(sampler, vTexCoord); - - float glowIntensity = 0.0; - float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); - float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); - float dist = min(dist1, dist2); - vec3 glowColor = glowColor1.rgb; - if (dist2 < dist1) { - glowColor = glowColor2.rgb; - } - - if (dist <= radius) { - glowIntensity = 1.0 - (dist / radius); - glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); - glowIntensity = easeInOutCubic(glowIntensity); - glowIntensity = pow(glowIntensity, 0.5); - } - - if (alpha <= 0.0) { - if (glowIntensity <= 0.0) { - discard; - } - - FragColor = vec4(glowColor, glowIntensity); - return; - } - - FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); - FragColor.a *= alpha; -} -)FS"; - void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled @@ -354,76 +108,139 @@ void HmdDisplayPlugin::customizeContext() { if (!_enablePreview) { const std::string version("#version 410 core\n"); compileProgram(_previewProgram, version + DrawUnitQuadTexcoord_vert, version + DrawTexture_frag); - PREVIEW_TEXTURE_LOCATION = Uniform(*_previewProgram, "colorMap").Location(); + _previewUniforms.previewTexture = Uniform(*_previewProgram, "colorMap").Location(); } - updateGlowProgram(); - compileProgram(_laserProgram, LASER_VS, LASER_FS); + updateReprojectionProgram(); + updateOverlayProgram(); + updateLaserProgram(); + _laserGeometry = loadLaser(_laserProgram); - - compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); - REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); - INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); - PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "projections").Location(); } +//#define LIVE_SHADER_RELOAD 1 -#if 0 -static QString readFile(const char* filename) { +static QString readFile(const QString& filename) { QFile file(filename); file.open(QFile::Text | QFile::ReadOnly); QString result; result.append(QTextStream(&file).readAll()); return result; } -#endif - -void HmdDisplayPlugin::updateGlowProgram() { -#if 0 +void HmdDisplayPlugin::updateReprojectionProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.vert"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.frag"; +#if LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!_reprojectionProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; +#else + if (!_reprojectionProgram) { +#endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _reprojectionUniforms.reprojectionMatrix = Uniform(*program, "reprojection").Location(); + _reprojectionUniforms.inverseProjectionMatrix = Uniform(*program, "inverseProjections").Location(); + _reprojectionUniforms.projectionMatrix = Uniform(*program, "projections").Location(); + _reprojectionProgram = program; + } + } catch (std::runtime_error& error) { + qWarning() << "Error building reprojection shader " << error.what(); + } + } + +} + +void HmdDisplayPlugin::updateLaserProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.vert"; + static const QString gsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.geom"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.frag"; + +#if LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 gsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + QFileInfo gsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + auto gsAge = gsInfo.lastModified().toMSecsSinceEpoch(); + if (!_laserProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge || gsAge > gsBuiltAge) { + vsBuiltAge = vsAge; + gsBuiltAge = gsAge; + fsBuiltAge = fsAge; +#else + if (!_laserProgram) { +#endif + + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + QString gsSource = readFile(gsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), gsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _laserUniforms.color = Uniform(*program, "color").Location(); + _laserUniforms.mvp = Uniform(*program, "mvp").Location(); + _laserProgram = program; + } + } catch (std::runtime_error& error) { + qWarning() << "Error building hand laser composite shader " << error.what(); + } + } +} + +void HmdDisplayPlugin::updateOverlayProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; + +#if LIVE_SHADER_RELOAD static qint64 vsBuiltAge = 0; static qint64 fsBuiltAge = 0; - static const char* vsFile = "H:/glow_vert.glsl"; - static const char* fsFile = "H:/glow_frag.glsl"; QFileInfo vsInfo(vsFile); QFileInfo fsInfo(fsFile); auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); if (!_overlayProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { - QString vsSource = readFile(vsFile); - QString fsSource = readFile(fsFile); - ProgramPtr newOverlayProgram; - compileProgram(newOverlayProgram, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); - if (newOverlayProgram) { - using namespace oglplus; - _overlayProgram = newOverlayProgram; - auto uniforms = _overlayProgram->ActiveUniforms(); - _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); - _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); - _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); - _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); - _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); - useProgram(_overlayProgram); - Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); - } vsBuiltAge = vsAge; fsBuiltAge = fsAge; - - } #else if (!_overlayProgram) { - compileProgram(_overlayProgram, LASER_GLOW_VS, LASER_GLOW_FS); - using namespace oglplus; - auto uniforms = _overlayProgram->ActiveUniforms(); - _overlayUniforms.mvp = Uniform(*_overlayProgram, "mvp").Location(); - _overlayUniforms.alpha = Uniform(*_overlayProgram, "alpha").Location(); - _overlayUniforms.glowPoints = Uniform(*_overlayProgram, "glowPoints").Location(); - _overlayUniforms.resolution = Uniform(*_overlayProgram, "resolution").Location(); - _overlayUniforms.radius = Uniform(*_overlayProgram, "radius").Location(); - useProgram(_overlayProgram); - Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); - } - #endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _overlayUniforms.mvp = Uniform(*program, "mvp").Location(); + _overlayUniforms.alpha = Uniform(*program, "alpha").Location(); + _overlayUniforms.glowColors = Uniform(*program, "glowColors").Location(); + _overlayUniforms.glowPoints = Uniform(*program, "glowPoints").Location(); + _overlayUniforms.resolution = Uniform(*program, "resolution").Location(); + _overlayUniforms.radius = Uniform(*program, "radius").Location(); + _overlayProgram = program; + useProgram(_overlayProgram); + Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); + } + } catch (std::runtime_error& error) { + qWarning() << "Error building overlay composite shader " << error.what(); + } + } } void HmdDisplayPlugin::uncustomizeContext() { @@ -459,12 +276,12 @@ void HmdDisplayPlugin::compositeScene() { using namespace oglplus; Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - Uniform(*_reprojectionProgram, REPROJECTION_MATRIX_LOCATION).Set(_currentPresentFrameInfo.presentReprojection); + Uniform(*_reprojectionProgram, _reprojectionUniforms.reprojectionMatrix).Set(_currentPresentFrameInfo.presentReprojection); //Uniform(*_reprojectionProgram, PROJECTION_MATRIX_LOCATION).Set(_eyeProjections); //Uniform(*_reprojectionProgram, INVERSE_PROJECTION_MATRIX_LOCATION).Set(_eyeInverseProjections); // FIXME what's the right oglplus mechanism to do this? It's not that ^^^ ... better yet, switch to a uniform buffer - glUniformMatrix4fv(INVERSE_PROJECTION_MATRIX_LOCATION, 2, GL_FALSE, &(_eyeInverseProjections[0][0][0])); - glUniformMatrix4fv(PROJECTION_MATRIX_LOCATION, 2, GL_FALSE, &(_eyeProjections[0][0][0])); + glUniformMatrix4fv(_reprojectionUniforms.inverseProjectionMatrix, 2, GL_FALSE, &(_eyeInverseProjections[0][0][0])); + glUniformMatrix4fv(_reprojectionUniforms.projectionMatrix, 2, GL_FALSE, &(_eyeProjections[0][0][0])); _plane->UseInProgram(*_reprojectionProgram); _plane->Draw(); } @@ -530,7 +347,11 @@ void HmdDisplayPlugin::compositeOverlay() { handGlowPoints[i] = yawPitch; } - updateGlowProgram(); + updateOverlayProgram(); + if (!_overlayProgram) { + return; + } + useProgram(_overlayProgram); // Setup the uniforms { @@ -541,6 +362,12 @@ void HmdDisplayPlugin::compositeOverlay() { vec4 glowPoints(handGlowPoints[0], handGlowPoints[1]); Uniform(*_overlayProgram, _overlayUniforms.glowPoints).Set(glowPoints); } + if (_overlayUniforms.glowColors >= 0) { + std::array glowColors; + glowColors[0] = _presentHandLasers[0].color; + glowColors[1] = _presentHandLasers[1].color; + glProgramUniform4fv(GetName(*_overlayProgram), _overlayUniforms.glowColors, 2, &glowColors[0].r); + } } _sphereSection->Use(); @@ -634,7 +461,7 @@ void HmdDisplayPlugin::internalPresent() { glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); glViewport(targetViewportPosition.x, targetViewportPosition.y, targetViewportSize.x, targetViewportSize.y); - glUniform1i(PREVIEW_TEXTURE_LOCATION, 0); + glUniform1i(_previewUniforms.previewTexture, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _previewTextureID); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); @@ -698,6 +525,8 @@ void HmdDisplayPlugin::compositeExtra() { return; } + updateLaserProgram(); + // Render hand lasers using namespace oglplus; useProgram(_laserProgram); @@ -739,6 +568,7 @@ void HmdDisplayPlugin::compositeExtra() { handLaserModelMatrices[i] = model; } + glEnable(GL_BLEND); for_each_eye([&](Eye eye) { eyeViewport(eye); auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); @@ -753,4 +583,5 @@ void HmdDisplayPlugin::compositeExtra() { _laserGeometry->Draw(); } }); + glDisable(GL_BLEND); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index be2811076d..f168ec9607 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -61,15 +61,6 @@ protected: } }; - ProgramPtr _overlayProgram; - struct OverlayUniforms { - int32_t mvp { -1 }; - int32_t alpha { -1 }; - int32_t glowPoints { -1 }; - int32_t resolution { -1 }; - int32_t radius { -1 }; - } _overlayUniforms; - Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; @@ -82,8 +73,6 @@ protected: std::array _eyeProjections; std::array _eyeInverseProjections; - - glm::mat4 _cullingProjection; glm::uvec2 _renderTargetSize; float _ipd { 0.064f }; @@ -103,23 +92,49 @@ protected: FrameInfo _currentRenderFrameInfo; private: - void updateGlowProgram(); + void updateOverlayProgram(); + void updateLaserProgram(); + void updateReprojectionProgram(); bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; bool _firstPreview { true }; + ProgramPtr _overlayProgram; + struct OverlayUniforms { + int32_t mvp { -1 }; + int32_t alpha { -1 }; + int32_t glowColors { -1 }; + int32_t glowPoints { -1 }; + int32_t resolution { -1 }; + int32_t radius { -1 }; + } _overlayUniforms; + ProgramPtr _previewProgram; + struct PreviewUniforms { + int32_t previewTexture { -1 }; + } _previewUniforms; + float _previewAspect { 0 }; GLuint _previewTextureID { 0 }; glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; ProgramPtr _reprojectionProgram; + struct ReprojectionUniforms { + int32_t reprojectionMatrix { -1 }; + int32_t inverseProjectionMatrix { -1 }; + int32_t projectionMatrix { -1 }; + } _reprojectionUniforms; + ShapeWrapperPtr _sphereSection; ProgramPtr _laserProgram; + struct LaserUniforms { + int32_t mvp { -1 }; + int32_t color { -1 }; + } _laserUniforms; ShapeWrapperPtr _laserGeometry; }; diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp index 7a535a806d..1154042b4a 100644 --- a/libraries/gl/src/gl/OglplusHelpers.cpp +++ b/libraries/gl/src/gl/OglplusHelpers.cpp @@ -87,6 +87,36 @@ ProgramPtr loadCubemapShader() { return result; } +void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& gs, const std::string& fs) { + using namespace oglplus; + try { + result = std::make_shared(); + // attach the shaders to the program + result->AttachShader( + VertexShader() + .Source(GLSLSource(vs)) + .Compile() + ); + result->AttachShader( + GeometryShader() + .Source(GLSLSource(gs)) + .Compile() + ); + result->AttachShader( + FragmentShader() + .Source(GLSLSource(fs)) + .Compile() + ); + result->Link(); + } catch (ProgramBuildError& err) { + Q_UNUSED(err); + qWarning() << err.Log().c_str(); + Q_ASSERT_X(false, "compileProgram", "Failed to build shader program"); + qFatal("%s", (const char*)err.Message); + result.reset(); + } +} + void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs) { using namespace oglplus; try { diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h index 8940205b21..c453fbad28 100644 --- a/libraries/gl/src/gl/OglplusHelpers.h +++ b/libraries/gl/src/gl/OglplusHelpers.h @@ -62,6 +62,8 @@ using Mat4Uniform = oglplus::Uniform; ProgramPtr loadDefaultShader(); ProgramPtr loadCubemapShader(); void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs); +void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& gs, const std::string& fs); + ShapeWrapperPtr loadSkybox(ProgramPtr program); ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f); ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 128, int stacks = 128); From 2f07f0d24f53096c735e06109511e38cfa7afb91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Brisset?= Date: Fri, 8 Jul 2016 18:32:59 -0700 Subject: [PATCH 72/78] Revert "Revert "Fix crash in packet list"" --- domain-server/src/DomainServer.cpp | 2 ++ libraries/networking/src/LimitedNodeList.cpp | 1 + libraries/networking/src/LimitedNodeList.h | 4 ++-- libraries/networking/src/NodeList.cpp | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d75a2c3245..8b3f09d1f7 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -389,6 +389,8 @@ void DomainServer::setupNodeListAndAssignments() { const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); + } else { + nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID } connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d7a2d47fab..a03fa43093 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -522,6 +522,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret) { + QReadLocker readLocker(&_nodeMutex); NodeHash::const_iterator it = _nodeHash.find(uuid); if (it != _nodeHash.end()) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 483aa0734c..c9054ac6d7 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -131,7 +131,7 @@ public: std::function linkedDataCreateCallback; - size_t size() const { return _nodeHash.size(); } + size_t size() const { QReadLocker readLock(&_nodeMutex); return _nodeHash.size(); } SharedNodePointer nodeWithUUID(const QUuid& nodeUUID); @@ -287,7 +287,7 @@ protected: QUuid _sessionUUID; NodeHash _nodeHash; - QReadWriteLock _nodeMutex; + mutable QReadWriteLock _nodeMutex; udt::Socket _nodeSocket; QUdpSocket* _dtlsSocket; HifiSockAddr _localSockAddr; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index fd1442d639..02350ac23c 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -513,9 +513,16 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { if (_domainHandler.getSockAddr().isNull()) { + qWarning() << "IGNORING DomainList packet while not connected to a Domain Server"; // refuse to process this packet if we aren't currently connected to the DS return; } + QUuid domainHandlerUUID = _domainHandler.getUUID(); + QUuid messageSourceUUID = message->getSourceID(); + if (!domainHandlerUUID.isNull() && domainHandlerUUID != messageSourceUUID) { + qWarning() << "IGNORING DomainList packet from" << messageSourceUUID << "while connected to" << domainHandlerUUID; + return; + } // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; From 1a91a8e20a0113eaa83b5541e7e190e5306cfa64 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 8 Jul 2016 18:38:25 -0700 Subject: [PATCH 73/78] Make DomainList a sourced packet --- libraries/networking/src/udt/PacketHeaders.cpp | 4 ++-- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6359ad0aff..bab33e1158 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -31,7 +31,7 @@ const QSet NON_VERIFIED_PACKETS = QSet() const QSet NON_SOURCED_PACKETS = QSet() << PacketType::StunResponse << PacketType::CreateAssignment << PacketType::RequestAssignment << PacketType::DomainServerRequireDTLS << PacketType::DomainConnectRequest - << PacketType::DomainList << PacketType::DomainConnectionDenied + << PacketType::DomainConnectionDenied << PacketType::DomainServerPathQuery << PacketType::DomainServerPathResponse << PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken << PacketType::DomainSettingsRequest << PacketType::DomainSettings @@ -45,7 +45,7 @@ const QSet RELIABLE_PACKETS = QSet(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return static_cast(DomainListVersion::PermissionsGrid); + return static_cast(DomainListVersion::IsSourcedPacket); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 9140cf8738..67a6a4462d 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -208,7 +208,8 @@ enum class DomainServerAddedNodeVersion : PacketVersion { enum class DomainListVersion : PacketVersion { PrePermissionsGrid = 18, - PermissionsGrid + PermissionsGrid, + IsSourcedPacket }; #endif // hifi_PacketHeaders_h From a7d631b7e1203c6f8754a7f216b36585dca80f00 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 12 Jul 2016 11:43:31 -0700 Subject: [PATCH 74/78] Revert packet version change --- libraries/networking/src/udt/PacketHeaders.cpp | 4 ++-- libraries/networking/src/udt/PacketHeaders.h | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index bab33e1158..6359ad0aff 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -31,7 +31,7 @@ const QSet NON_VERIFIED_PACKETS = QSet() const QSet NON_SOURCED_PACKETS = QSet() << PacketType::StunResponse << PacketType::CreateAssignment << PacketType::RequestAssignment << PacketType::DomainServerRequireDTLS << PacketType::DomainConnectRequest - << PacketType::DomainConnectionDenied + << PacketType::DomainList << PacketType::DomainConnectionDenied << PacketType::DomainServerPathQuery << PacketType::DomainServerPathResponse << PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken << PacketType::DomainSettingsRequest << PacketType::DomainSettings @@ -45,7 +45,7 @@ const QSet RELIABLE_PACKETS = QSet(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return static_cast(DomainListVersion::IsSourcedPacket); + return static_cast(DomainListVersion::PermissionsGrid); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 67a6a4462d..9140cf8738 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -208,8 +208,7 @@ enum class DomainServerAddedNodeVersion : PacketVersion { enum class DomainListVersion : PacketVersion { PrePermissionsGrid = 18, - PermissionsGrid, - IsSourcedPacket + PermissionsGrid }; #endif // hifi_PacketHeaders_h From 7a7b64e87d58d2039323ea0e4fad387f71bfdc02 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 12 Jul 2016 11:43:46 -0700 Subject: [PATCH 75/78] Use domain ID in the stream --- libraries/networking/src/NodeList.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 02350ac23c..d3fc93b991 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -517,12 +517,6 @@ void NodeList::processDomainServerList(QSharedPointer message) // refuse to process this packet if we aren't currently connected to the DS return; } - QUuid domainHandlerUUID = _domainHandler.getUUID(); - QUuid messageSourceUUID = message->getSourceID(); - if (!domainHandlerUUID.isNull() && domainHandlerUUID != messageSourceUUID) { - qWarning() << "IGNORING DomainList packet from" << messageSourceUUID << "while connected to" << domainHandlerUUID; - return; - } // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; @@ -542,6 +536,10 @@ void NodeList::processDomainServerList(QSharedPointer message) if (!_domainHandler.isConnected()) { _domainHandler.setUUID(domainUUID); _domainHandler.setIsConnected(true); + } else if (_domainHandler.getUUID() != domainUUID) { + // Recieved packet from different domain. + qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to" << _domainHandler.getUUID(); + return; } // pull our owner UUID from the packet, it's always the first thing From 87ba1ffecabef45a7bd390ec1f4acc1ca5da996e Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Tue, 12 Jul 2016 16:05:46 -0700 Subject: [PATCH 76/78] Fix orientation being reset to 0,0,0,1 --- libraries/networking/src/AddressManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index df9b4094b0..7ed3888be0 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -564,10 +564,10 @@ bool AddressManager::handleViewpoint(const QString& viewpointString, bool should if (viewpointString[positionRegex.matchedLength() - 1] == QChar('/') && orientationRegex.indexIn(viewpointString, positionRegex.matchedLength() - 1) != -1) { - glm::quat newOrientation = glm::normalize(glm::quat(orientationRegex.cap(4).toFloat(), - orientationRegex.cap(1).toFloat(), - orientationRegex.cap(2).toFloat(), - orientationRegex.cap(3).toFloat())); + newOrientation = glm::normalize(glm::quat(orientationRegex.cap(4).toFloat(), + orientationRegex.cap(1).toFloat(), + orientationRegex.cap(2).toFloat(), + orientationRegex.cap(3).toFloat())); if (!isNaN(newOrientation.x) && !isNaN(newOrientation.y) && !isNaN(newOrientation.z) && !isNaN(newOrientation.w)) { From f2220288fb17e0fad0d582a290e1dc00975047c3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Jul 2016 14:54:12 -0700 Subject: [PATCH 77/78] Expand frame decoration when mouse is over a window in HMD mode --- .../resources/qml/hifi/toolbars/Toolbar.qml | 14 ++- .../resources/qml/windows/Decoration.qml | 47 +++++++ .../resources/qml/windows/DefaultFrame.qml | 100 +-------------- .../qml/windows/DefaultFrameDecoration.qml | 115 ++++++++++++++++++ interface/resources/qml/windows/Frame.qml | 16 ++- interface/resources/qml/windows/ToolFrame.qml | 78 +----------- .../qml/windows/ToolFrameDecoration.qml | 98 +++++++++++++++ interface/resources/qml/windows/Window.qml | 8 ++ tests/ui/qml/main.qml | 33 ++++- 9 files changed, 325 insertions(+), 184 deletions(-) create mode 100644 interface/resources/qml/windows/Decoration.qml create mode 100644 interface/resources/qml/windows/DefaultFrameDecoration.qml create mode 100644 interface/resources/qml/windows/ToolFrameDecoration.qml diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 75c06e4199..c5c15a6406 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -21,7 +21,19 @@ Window { height: content.height visible: true // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' - activator: Item {} + activator: MouseArea { + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + enabled: window.visible + hoverEnabled: true + onPressed: mouse.accepted = false; + onEntered: window.mouseEntered(); + onExited: window.mouseExited(); + } property bool horizontal: true property real buttonSize: 50; property var buttons: [] diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml new file mode 100644 index 0000000000..628a4d3370 --- /dev/null +++ b/interface/resources/qml/windows/Decoration.qml @@ -0,0 +1,47 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Rectangle { + HifiConstants { id: hifi } + + property int frameMargin: 9 + property int frameMarginLeft: frameMargin + property int frameMarginRight: frameMargin + property int frameMarginTop: 2 * frameMargin + iconSize + property int frameMarginBottom: iconSize + 11 + + anchors { + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius + + // Enable dragging of the window, + // detect mouseover of the window (including decoration) + MouseArea { + anchors.fill: parent + drag.target: window + } +} + diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index 242209dbe0..33c2818849 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -16,104 +16,6 @@ import "../styles-uit" Frame { HifiConstants { id: hifi } - - Rectangle { - // Dialog frame - id: frameContent - - readonly property int iconSize: hifi.dimensions.frameIconSize - readonly property int frameMargin: 9 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: 2 * frameMargin + iconSize - readonly property int frameMarginBottom: iconSize + 11 - - anchors { - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } - - Row { - id: controlsRow - anchors { - right: parent.right; - top: parent.top; - topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title - rightMargin: frameContent.frameMarginRight; - } - spacing: frameContent.iconSize / 4 - - HiFiGlyphs { - // "Pin" button - visible: window.pinnable - text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin - color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: pinClickArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - onClicked: window.pinned = !window.pinned; - } - } - - HiFiGlyphs { - // "Close" button - visible: window ? window.closable : false - text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close - color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: closeClickArea - anchors.fill: parent - hoverEnabled: true - onClicked: window.shown = false; - } - } - } - - RalewayRegular { - // Title - id: titleText - anchors { - left: parent.left - leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x - right: controlsRow.left - rightMargin: frameContent.iconSize - top: parent.top - topMargin: frameContent.frameMargin - } - text: window ? window.title : "" - color: hifi.colors.white - size: hifi.fontSizes.overlayTitle - } - - DropShadow { - source: titleText - anchors.fill: titleText - horizontalOffset: 2 - verticalOffset: 2 - samples: 2 - color: hifi.colors.baseGrayShadow60 - visible: (window && window.focus) - cached: true - } - } + DefaultFrameDecoration {} } diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml new file mode 100644 index 0000000000..ecb1760717 --- /dev/null +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -0,0 +1,115 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + + +Decoration { + HifiConstants { id: hifi } + + // Dialog frame + id: root + + property int iconSize: hifi.dimensions.frameIconSize + frameMargin: 9 + frameMarginLeft: frameMargin + frameMarginRight: frameMargin + frameMarginTop: 2 * frameMargin + iconSize + frameMarginBottom: iconSize + 11 + + Connections { + target: window + onMouseEntered: { + if (!HMD.active) { + return; + } + root.frameMargin = 18 + titleText.size = hifi.fontSizes.overlayTitle * 2 + root.iconSize = hifi.dimensions.frameIconSize * 2 + } + onMouseExited: { + root.frameMargin = 9 + titleText.size = hifi.fontSizes.overlayTitle + root.iconSize = hifi.dimensions.frameIconSize + } + } + + Row { + id: controlsRow + anchors { + right: parent.right; + top: parent.top; + topMargin: root.frameMargin + 1 // Move down a little to visually align with the title + rightMargin: root.frameMarginRight; + } + spacing: root.iconSize / 4 + + HiFiGlyphs { + // "Pin" button + visible: window.pinnable + text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin + color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white + size: root.iconSize + MouseArea { + id: pinClickArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onClicked: window.pinned = !window.pinned; + } + } + + HiFiGlyphs { + // "Close" button + visible: window ? window.closable : false + text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close + color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white + size: root.iconSize + MouseArea { + id: closeClickArea + anchors.fill: parent + hoverEnabled: true + onClicked: window.shown = false; + } + } + } + + RalewayRegular { + // Title + id: titleText + anchors { + left: parent.left + leftMargin: root.frameMarginLeft + hifi.dimensions.contentMargin.x + right: controlsRow.left + rightMargin: root.iconSize + top: parent.top + topMargin: root.frameMargin + } + text: window ? window.title : "" + color: hifi.colors.white + size: hifi.fontSizes.overlayTitle + } + + DropShadow { + source: titleText + anchors.fill: titleText + horizontalOffset: 2 + verticalOffset: 2 + samples: 2 + color: hifi.colors.baseGrayShadow60 + visible: (window && window.focus) + cached: true + } +} + diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 88d8c3ad41..030af974f6 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -22,10 +22,10 @@ Item { property bool gradientsSupported: desktop.gradientsSupported - readonly property int frameMarginLeft: frameContent.frameMarginLeft - readonly property int frameMarginRight: frameContent.frameMarginRight - readonly property int frameMarginTop: frameContent.frameMarginTop - readonly property int frameMarginBottom: frameContent.frameMarginBottom + readonly property int frameMarginLeft: frame.decoration ? frame.decoration.frameMarginLeft : 0 + readonly property int frameMarginRight: frame.decoration ? frame.decoration.frameMarginRight : 0 + readonly property int frameMarginTop: frame.decoration ? frame.decoration.frameMarginTop : 0 + readonly property int frameMarginBottom: frame.decoration ? frame.decoration.frameMarginBottom : 0 // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes @@ -103,16 +103,14 @@ Item { } onReleased: { if (hid) { - pane.visible = true - frameContent.visible = true + window.content.visible = true hid = false; } } onPositionChanged: { if (pressed) { - if (pane.visible) { - pane.visible = false; - frameContent.visible = false + if (window.content.visible) { + window.content.visible = false; hid = true; } var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml index eff5fc0377..20c86afb5e 100644 --- a/interface/resources/qml/windows/ToolFrame.qml +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -16,81 +16,11 @@ import "../styles-uit" Frame { HifiConstants { id: hifi } - property bool horizontalSpacers: false - property bool verticalSpacers: false + property alias horizontalSpacers: decoration.horizontalSpacers + property alias verticalSpacers: decoration.verticalSpacers - Rectangle { - // Dialog frame - id: frameContent - readonly property int frameMargin: 6 - readonly property int frameMarginLeft: frameMargin + (horizontalSpacers ? 12 : 0) - readonly property int frameMarginRight: frameMargin + (horizontalSpacers ? 12 : 0) - readonly property int frameMarginTop: frameMargin + (verticalSpacers ? 12 : 0) - readonly property int frameMarginBottom: frameMargin + (verticalSpacers ? 12 : 0) - - Rectangle { - visible: horizontalSpacers - anchors.left: parent.left - anchors.leftMargin: 6 - anchors.verticalCenter: parent.verticalCenter - width: 8 - height: window.height - color: "gray"; - radius: 4 - } - - Rectangle { - visible: horizontalSpacers - anchors.right: parent.right - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - width: 8 - height: window.height - color: "gray"; - radius: 4 - } - - Rectangle { - visible: verticalSpacers - anchors.top: parent.top - anchors.topMargin: 6 - anchors.horizontalCenter: parent.horizontalCenter - height: 8 - width: window.width - color: "gray"; - radius: 4 - } - - Rectangle { - visible: verticalSpacers - anchors.bottom: parent.bottom - anchors.bottomMargin: 6 - anchors.horizontalCenter: parent.horizontalCenter - height: 8 - width: window.width - color: "gray"; - radius: 4 - } - - anchors { - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - topMargin: -frameMarginTop - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius / 2 - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } + ToolFrameDecoration { + id: decoration } } diff --git a/interface/resources/qml/windows/ToolFrameDecoration.qml b/interface/resources/qml/windows/ToolFrameDecoration.qml new file mode 100644 index 0000000000..a7068183c1 --- /dev/null +++ b/interface/resources/qml/windows/ToolFrameDecoration.qml @@ -0,0 +1,98 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Decoration { + id: root + HifiConstants { id: hifi } + + property bool horizontalSpacers: false + property bool verticalSpacers: false + + // Dialog frame + property int spacerWidth: 8 + property int spacerRadius: 4 + property int spacerMargin: 12 + frameMargin: 6 + frameMarginLeft: frameMargin + (horizontalSpacers ? spacerMargin : 0) + frameMarginRight: frameMargin + (horizontalSpacers ? spacerMargin : 0) + frameMarginTop: frameMargin + (verticalSpacers ? spacerMargin : 0) + frameMarginBottom: frameMargin + (verticalSpacers ? spacerMargin : 0) + radius: hifi.dimensions.borderRadius / 2 + + Connections { + target: window + onMouseEntered: { + if (!HMD.active) { + return; + } + root.frameMargin = 18 + root.spacerWidth = 16 + root.spacerRadius = 8 + root.spacerMargin = 8 + } + onMouseExited: { + root.frameMargin = 6 + root.spacerWidth = 8 + root.spacerRadius = 4 + root.spacerMargin = 12 + } + } + + Rectangle { + visible: horizontalSpacers + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: root.spacerWidth + height: decoration.height - 12 + color: "gray"; + radius: root.spacerRadius + } + + Rectangle { + visible: horizontalSpacers + anchors.right: parent.right + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: root.spacerWidth + height: decoration.height - 12 + color: "gray"; + radius: root.spacerRadius + } + + Rectangle { + visible: verticalSpacers + anchors.top: parent.top + anchors.topMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: root.spacerWidth + width: decoration.width - 12 + color: "gray"; + radius: root.spacerRadius + } + + Rectangle { + visible: verticalSpacers + anchors.bottom: parent.bottom + anchors.bottomMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: root.spacerWidth + width: decoration.width - 12 + color: "gray"; + radius: root.spacerRadius + } +} + diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 82bcf011e9..ca37c55f4d 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -31,6 +31,8 @@ Fadable { // Signals // signal windowDestroyed(); + signal mouseEntered(); + signal mouseExited(); // // Native properties @@ -113,11 +115,14 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible + hoverEnabled: true onPressed: { //console.log("Pressed on activator area"); window.raise(); mouse.accepted = false; } + onEntered: window.mouseEntered(); + onExited: window.mouseExited(); } // This mouse area serves to swallow mouse events while the mouse is over the window @@ -287,4 +292,7 @@ Fadable { break; } } + + onMouseEntered: console.log("Mouse entered " + window) + onMouseExited: console.log("Mouse exited " + window) } diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 47d0f6d601..a23a1e3ade 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -28,6 +28,11 @@ ApplicationWindow { property var toolbar; property var lastButton; + Button { + text: HMD.active ? "Disable HMD" : "Enable HMD" + onClicked: HMD.active = !HMD.active + } + // Window visibility Button { text: "toggle desktop" @@ -340,13 +345,13 @@ ApplicationWindow { } */ - /* Window { id: blue closable: true visible: true resizable: true destroyOnHidden: false + title: "Blue" width: 100; height: 100 x: 1280 / 2; y: 720 / 2 @@ -366,7 +371,33 @@ ApplicationWindow { } } + Window { + id: green + closable: true + visible: true + resizable: true + title: "Green" + destroyOnHidden: false + width: 100; height: 100 + x: 1280 / 2; y: 720 / 2 + Settings { + category: "TestWindow.Green" + property alias x: green.x + property alias y: green.y + property alias width: green.width + property alias height: green.height + } + + Rectangle { + anchors.fill: parent + visible: true + color: "green" + Component.onDestruction: console.log("Green destroyed") + } + } + +/* Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } Window { From 6336362d40fbd2e5e76c7d0000f83ee07da68934 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Jul 2016 17:23:13 -0700 Subject: [PATCH 78/78] Fixing decoration inflation --- interface/resources/qml/desktop/Desktop.qml | 1 + .../resources/qml/windows/Decoration.qml | 24 +++++++++++++++ .../qml/windows/DefaultFrameDecoration.qml | 28 ++++++++--------- .../qml/windows/ToolFrameDecoration.qml | 30 +++++++++---------- .../src/scripting/HMDScriptingInterface.cpp | 7 ++++- tests/ui/qml/main.qml | 5 ++++ 6 files changed, 63 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 59d5b435ba..e5ff849df8 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -25,6 +25,7 @@ FocusScope { property rect recommendedRect: Qt.rect(0,0,0,0); property var expectedChildren; property bool repositionLocked: true + property bool hmdHandMouseActive: false onRepositionLockedChanged: { if (!repositionLocked) { diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index 628a4d3370..edfb369c0f 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -17,6 +17,9 @@ import "../styles-uit" Rectangle { HifiConstants { id: hifi } + signal inflateDecorations(); + signal deflateDecorations(); + property int frameMargin: 9 property int frameMarginLeft: frameMargin property int frameMarginRight: frameMargin @@ -43,5 +46,26 @@ Rectangle { anchors.fill: parent drag.target: window } + Connections { + target: window + onMouseEntered: { + if (desktop.hmdHandMouseActive) { + root.inflateDecorations() + } + } + onMouseExited: root.deflateDecorations(); + } + Connections { + target: desktop + onHmdHandMouseActiveChanged: { + if (desktop.hmdHandMouseActive) { + if (window.activator.containsMouse) { + root.inflateDecorations(); + } + } else { + root.deflateDecorations(); + } + } + } } diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml index ecb1760717..ce47b818b1 100644 --- a/interface/resources/qml/windows/DefaultFrameDecoration.qml +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -14,7 +14,6 @@ import QtGraphicalEffects 1.0 import "." import "../styles-uit" - Decoration { HifiConstants { id: hifi } @@ -28,23 +27,22 @@ Decoration { frameMarginTop: 2 * frameMargin + iconSize frameMarginBottom: iconSize + 11 - Connections { - target: window - onMouseEntered: { - if (!HMD.active) { - return; - } - root.frameMargin = 18 - titleText.size = hifi.fontSizes.overlayTitle * 2 - root.iconSize = hifi.dimensions.frameIconSize * 2 - } - onMouseExited: { - root.frameMargin = 9 - titleText.size = hifi.fontSizes.overlayTitle - root.iconSize = hifi.dimensions.frameIconSize + onInflateDecorations: { + if (!HMD.active) { + return; } + root.frameMargin = 18 + titleText.size = hifi.fontSizes.overlayTitle * 2 + root.iconSize = hifi.dimensions.frameIconSize * 2 } + onDeflateDecorations: { + root.frameMargin = 9 + titleText.size = hifi.fontSizes.overlayTitle + root.iconSize = hifi.dimensions.frameIconSize + } + + Row { id: controlsRow anchors { diff --git a/interface/resources/qml/windows/ToolFrameDecoration.qml b/interface/resources/qml/windows/ToolFrameDecoration.qml index a7068183c1..ba36a2a38c 100644 --- a/interface/resources/qml/windows/ToolFrameDecoration.qml +++ b/interface/resources/qml/windows/ToolFrameDecoration.qml @@ -32,23 +32,21 @@ Decoration { frameMarginBottom: frameMargin + (verticalSpacers ? spacerMargin : 0) radius: hifi.dimensions.borderRadius / 2 - Connections { - target: window - onMouseEntered: { - if (!HMD.active) { - return; - } - root.frameMargin = 18 - root.spacerWidth = 16 - root.spacerRadius = 8 - root.spacerMargin = 8 - } - onMouseExited: { - root.frameMargin = 6 - root.spacerWidth = 8 - root.spacerRadius = 4 - root.spacerMargin = 12 + onInflateDecorations: { + if (!HMD.active) { + return; } + root.frameMargin = 18 + root.spacerWidth = 16 + root.spacerRadius = 8 + root.spacerMargin = 8 + } + + onDeflateDecorations: { + root.frameMargin = 6 + root.spacerWidth = 8 + root.spacerRadius = 4 + root.spacerMargin = 12 } Rectangle { diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index e9677cc3c8..36cde378f8 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "Application.h" @@ -110,13 +111,17 @@ QString HMDScriptingInterface::preferredAudioOutput() const { } bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([offscreenUi, enabled] { + offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", enabled); + }); return qApp->getActiveDisplayPlugin()->setHandLaser(hands, enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, color, direction); } void HMDScriptingInterface::disableHandLasers(int hands) const { - qApp->getActiveDisplayPlugin()->setHandLaser(hands, DisplayPlugin::HandLaserMode::None); + setHandLasers(hands, false, vec4(0), vec3(0)); } bool HMDScriptingInterface::suppressKeyboard() { diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index a23a1e3ade..8ca9399b74 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -33,6 +33,11 @@ ApplicationWindow { onClicked: HMD.active = !HMD.active } + Button { + text: desktop.hmdHandMouseActive ? "Disable HMD HandMouse" : "Enable HMD HandMouse" + onClicked: desktop.hmdHandMouseActive = !desktop.hmdHandMouseActive + } + // Window visibility Button { text: "toggle desktop"